洗牌算法

洗牌算法汇总以及测试洗牌程序的正确性

洗牌可以抽象为:给定一组排列,输出该排列的一个随机组合,本文代码中均以字符数组代表该排列

算法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洗牌的函数:

 View Code

测试算法4的函数:

 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:
");
 6     testShuffle(arr, 10, Shuffle_randomSwap, 100000);
 7     printf("算法2:
");
 8     testShuffle(arr, 10, Shuffle_FisherYates_change1, 100000);
 9     printf("算法3:
");
10     testShuffle(arr, 10, Shuffle_FisherYates, 100000);
11     printf("STL洗牌:
");
12     testShuffle(arr, 10, Shuffle_STL, 100000);
13     printf("算法4:
");
14     testShuffle(arr, 10, Shuffle_InsideOut, 100000);
15     return 0;
16 }
复制代码

测试结果:

算法1:主对角线上的次数明显是有问题的

算法2:主对角线右上方第一个对角线(12798开头)数据明显有问题

原文地址:https://www.cnblogs.com/Leo_wl/p/3385060.html