洗牌算法匯總以及測試洗牌程序的正確性


洗牌可以抽象為:給定一組排列,輸出該排列的一個隨機組合,本文代碼中均以字符數組代表該排列

算法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 ≤ ji
       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 }
View Code

測試算法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 }
View Code

測試代碼(每個算法測試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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM