洗牌可以抽象為:給定一組排列,輸出該排列的一個隨機組合,本文代碼中均以字符數組代表該排列
算法1-算法3 都是在原序列的基礎上進行交換,算法空間復雜度為O(1)
算法1(錯誤):隨機交換序列中的兩張牌,交換n次(n為序列的長度),代碼如下:
1 void Shuffle_randomSwap(char *arr, const int len) 2 { 3 for(int i = 1; i <= len; i++) 4 { 5 int a = rand()%len; 6 int b = rand()%len; 7 char temp = arr[a]; 8 arr[a] = arr[b]; 9 arr[b] = temp; 10 } 11 }
算法2(錯誤):遍歷序列中的每個數,隨機選擇序列的某個數,把它和當前遍歷到的數交換,代碼如下:
1 void Shuffle_FisherYates_change1(char *arr, const int len) 2 { 3 for(int i = len - 1; i >= 0; i--) 4 { 5 int a = rand()%len; 6 int temp = arr[i]; 7 arr[i] = arr[a]; 8 arr[a] = temp; 9 } 10 }
算法3(正確):這是FisherYates洗牌算法,具體可參考wiki,算法的思想是每次從未選中的數字中隨機挑選一個加入排列,時間復雜度為O(n),wiki上的偽代碼如下
To shuffle an array a of n elements (indices 0..n-1): for i from n − 1 downto 1 do j ← random integer with 0 ≤ j ≤ i exchange a[j] and a[i]
代碼實現:
1 void Shuffle_FisherYates(char *arr, const int len) 2 { 3 for(int i = len - 1; i > 0; i--) 4 { 5 int a = rand()%(i + 1); 6 int temp = arr[i]; 7 arr[i] = arr[a]; 8 arr[a] = temp; 9 } 10 }
下面我們來證明算法3的正確性,即證明每個數字在某個位置的概率相等,都為1/n:
對於原排列最后一個數字:很顯然他在第n個位置的概率是1/n,在倒數第二個位置概率是[(n-1)/n] * [1/(n-1)] = 1/n,在倒數第k個位置的概率是[(n-1)/n] * [(n-2)/(n-1)] *...* [(n-k+1)/(n-k+2)] *[1/(n-k+1)] = 1/n
對於原排列的其他數字也可以同上求得他們在每個位置的概率都是1/n。
這樣算法2就是明顯錯誤的:因為算法2中第一次隨機選擇后,第一個數字在第一個位置的概率是1/n,后面的隨機選擇只能使這個概率逐漸變小
如果我們想保留原始的排列,洗牌后的排列放到一個額外的數組,那么改用怎么樣的洗牌算法呢
算法4(正確):inside-out算法,算法的思想就是遍歷原數組,把原數組中位置 i 的數據隨機放到新數組的前i個位置(包括第i個)中的某一個(假設放到第k個),然后把新數組的第k個位置的數放到新數組的第 i 個位置,代碼如下:
1 void Shuffle_InsideOut(char *arrSrc, const int len, char *arrDest) 2 { 3 arrDest[0] = arrSrc[0]; 4 for(int i = 1; i < len; i++) 5 { 6 int k = rand()%(i + 1); 7 arrDest[i] = arrDest[k]; 8 arrDest[k] = arrSrc[i]; 9 } 10 }
該算法空間復雜度O(n),時間復雜度O(n)
證明算法4的正確性:原數組的第 i 個元素在新數組的前 i 個位置的概率都是:(1/i) * [i/(i+1)] * [(i+1)/(i+2)] *...* [(n-1)/n] = 1/n,(即第i次剛好隨機放到了該位置,在后面的n-i 次選擇中該數字不被選中)
原數組的第 i 個元素在新數組的 i+1 (包括i + 1)以后的位置(假設是第k個位置)的概率是:(1/k) * [k/(k+1)] * [(k+1)/(k+2)] *...* [(n-1)/n] = 1/n(即第k次剛好隨機放到了該位置,在后面的n-k次選擇中該數字不被選中)
算法4還可以用於未知原始數組大小的情況下的洗牌,從代碼中可以看出,沒加入一張新牌,后面的計算都和牌的總數目無關,只與當前牌的數目有關
c++ STL中有隨機洗牌的函數,頭文件#include<algorithm>中,調用如下random_shuffle(arr, arr+len); (其中len是數組arr的元素個數),為了統一測試,我們測試該函數時使用如下調用:
1 void Shuffle_STL(char *arr, const int len) 2 { 3 random_shuffle(arr, arr+len); 4 }
測試一個洗牌程序的正確性:運行該洗牌程序m次,然后計算每張牌在每個位置出現的次數,這個次數應該接近m/n,其中n為牌的數目
測試算法1~3以及STL洗牌的函數:

1 void testShuffle(char arr[], const int len, void(*shuffle)(char *, const int), 2 const int testTime) 3 { 4 int testResult[len][len]; 5 //testResult[i][j]表示牌arr[i]在第j個位置出現的次數 6 char arrBackup[len]; 7 std::map<char, int> arrMap; //用於查找牌在原來數組中的位置 8 for(int j = 0; j <len; j++) 9 { 10 arrMap.insert(map<char, int> :: value_type(arr[j], j)); 11 memset(testResult[j], 0, len*sizeof(int)); 12 } 13 14 //對一副牌洗多次,統計每張牌在每個位置出現的次數 15 for(int i = 1; i <= testTime; i++) 16 { 17 for(int j = 0; j <len; j++) 18 arrBackup[j] = arr[j]; 19 shuffle(arrBackup, len); 20 for(int j = 0; j <len; j++) 21 { 22 testResult[arrMap[arrBackup[j]]][j] ++; 23 } 24 } 25 for(int i = 0; i < len; i++) 26 { 27 printf("%c:", arr[i]); 28 for(int j = 0; j < len; j++) 29 { 30 printf("%7d",testResult[i][j]); 31 } 32 printf("\n"); 33 } 34 printf("----------------------------------\n"); 35 }
測試算法4的函數:

1 void testShuffle(char arr[], const int len, 2 void(*shuffle)(char *, const int, char *), 3 const int testTime) 4 { 5 int testResult[len][len]; 6 //testResult[i][j]表示牌arr[i]在第j個位置出現的次數 7 std::map<char, int> arrMap; //用於查找牌在原來數組中的位置 8 for(int j = 0; j <len; j++) 9 { 10 arrMap.insert(map<char, int> :: value_type(arr[j], j)); 11 memset(testResult[j], 0, len*sizeof(int)); 12 } 13 14 //對一副牌洗多次,統計每張牌在每個位置出現的次數 15 for(int i = 1; i <= testTime; i++) 16 { 17 char arrDest[len]; 18 shuffle(arr, len, arrDest); 19 for(int j = 0; j <len; j++) 20 { 21 testResult[arrMap[arrDest[j]]][j] ++; 22 } 23 } 24 for(int i = 0; i < len; i++) 25 { 26 printf("%c:", arr[i]); 27 for(int j = 0; j < len; j++) 28 { 29 printf("%7d",testResult[i][j]); 30 } 31 printf("\n"); 32 } 33 printf("----------------------------------\n"); 34 }
測試代碼(每個算法測試100000次)
1 int main() 2 { 3 srand((unsigned)time(NULL)); 4 char arr[10] = {'A','B','C','D','E','F','G','H','I','J'}; 5 printf("算法1:\n"); 6 testShuffle(arr, 10, Shuffle_randomSwap, 100000); 7 printf("算法2:\n"); 8 testShuffle(arr, 10, Shuffle_FisherYates_change1, 100000); 9 printf("算法3:\n"); 10 testShuffle(arr, 10, Shuffle_FisherYates, 100000); 11 printf("STL洗牌:\n"); 12 testShuffle(arr, 10, Shuffle_STL, 100000); 13 printf("算法4:\n"); 14 testShuffle(arr, 10, Shuffle_InsideOut, 100000); 15 return 0; 16 }
測試結果:
算法1:主對角線上的次數明顯是有問題的
算法2:主對角線右上方第一個對角線(12798開頭)數據明顯有問題
【版權聲明】轉載請注明出處:http://www.cnblogs.com/TenosDoIt/p/3384141.html