洗牌算法,概率问题

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

算法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还可以用于未知原始数组大小的情况下的洗牌,从代码中可以看出,没加入一张新牌,后面的计算都和牌的总数目无关,只与当前牌的数目有关

猜你喜欢

转载自blog.csdn.net/u010325193/article/details/86554748