固定长度的洗牌算法

1.背景

笔试时,遇到一个算法题:差不多是 在n个不同的数中随机取出不重复的m个数。洗牌算法是将原来的数组进行打散,使原数组的某个数在打散后的数组中的每个位置上等概率的出现,刚好可以解决该问题。

2.洗牌算法

由抽牌、换牌和插牌衍生出三种洗牌算法,其中抽牌和换牌分别对应Fisher-Yates Shuffle和Knuth-Durstenfeld Shhuffle算法。

2.1Fisher-Yates Shuffle

最早提出这个洗牌方法的是 Ronald A. Fisher 和 Frank Yates,即 Fisher–Yates Shuffle,其基本思想就是从原始数组中随机取一个之前没取过的数字到新的数组中,具体如下:

  1. 初始化原始数组和新数组,原始数组长度为n(已知);
  2. 从还没处理的数组(假设还剩k个),随机产生一个[0,k)的元素p(假设元素从0开始编号)
  3. 从剩下的k个数将第p个数取出
  4. 重复2和3直到数字取完
  5. 从步骤3取出的序列即是一个被打乱的序列

下面证明其随机性,即每个元素被放置在新数组中的第i个位置是1/n(假设数组大小是n)。
证明:一个元素m被放入第i个位置的概率P = 前i-1个位置选择元素时没有选中m的概率 * 第i个位置选中m的概率,即
在这里插入图片描述
例子:如果有一个5个数的序列,每个数字作为第一个数字被取出的数字的概率为1/5,就是1/n
第二个被取出的数字是放在新数组第二个的,每个数字作为第二个被取出的概率为4/5 * 1/4 = 1/5,也就是1/n

#define N 10
#define M 5
void Fisher_Yates_Shuffle(vector<int>& arr,vector<int>& res)
{
    
    
     srand((unsigned)time(NULL));
     int k;
     for (int i=0;i<M;++i)
     {
    
    
     	
     	k = rand() % arr.size();
     	res.push_back(arr[k]);
     	arr.erase(arr.begin()+k);
     }
}

时间复杂度为O(n*n),空间复杂度为O(n).

2.2Knuth-Durstenfeld Shuffle

Knuth 和 Durstenfeld 在Fisher 等人的基础上对算法进行了改进,在原始数组上对数字进行交互,省去了额外O(n)的空间。该算法的基本思想和 Fisher 类似,每次从未处理的数据中随机取出一个数字,然后把该数字放在数组的尾部,即数组尾部存放的是已经处理过的数字。

算法步骤为:

  1. 建立一个数组大小为 n 的数组 arr,分别存放 1 到 n 的数值;
  2. 生成一个0到n-1的随机数x;//最后一位也能被取到
  3. 输出下标为x的数值,即为第一个随机数
  4. 将arr当前的尾元素与第x个元素交换
  5. 同2,生成一个0到n-2的随机数x
  6. 输出下标为x的数值,即为第二个随机数
  7. 将 arr 的倒数第二个元素和下标为 x 的元素互换;

    如上,直到输出 m 个数为止

该算法是经典洗牌算法。它的proof如下:
对于arr[i],洗牌后在第n-1个位置的概率是1/n(第一次交换的随机数为i)
在n-2个位置概率是[(n-1)/n] * [1/(n-1)] = 1/n,(第一次交换的随机数不为i,第二次为arr[i]所在的位置(注意,若i=n-1,第一交换arr[n-1]会被换到一个随机的位置))
在第n-k个位置的概率是[(n-1)/n] * [(n-2)/(n-1)] [(n-k+1)/(n-k+2)] *[1/(n-k+1)] = 1/n
(第一个随机数不要为i,第二次不为arr[i]所在的位置(随着交换有可能会变)……第n-k次为arr[i]所在的位置).

void Knuth_Durstenfeld_Shuffle(vector<int>& arr){
    
    
	for(int i = arr.size() - 1; i >= 0; i--){
    
    
		srand(unsigned)time(NULL);
		swap(arr[i], arr[rand() % (i + 1)];
	} 
}

图解证明
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
时间复杂度为O(n),空间复杂度为O(1),缺点必须知道数组长度n.
原始数组被修改了,这是一个原地打乱顺序的算法,算法时间复杂度也从Fisher算法的 O(n2)提升到了O(n)。由于是从后往前扫描,无法处理不知道长度或动态增长的数组。

猜你喜欢

转载自blog.csdn.net/J_avaSmallWhite/article/details/109268439
今日推荐