洗牌可以抽象为:给定一组排列,输出该排列的一个随机组合,本文代码中均以字符数组代表该排列
算法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洗牌的函数:
测试算法4的函数:
测试代码(每个算法测试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开头)数据明显有问题