洗牌算法
共 1517字,需浏览 4分钟
· 2020-06-25
引言
首先看一道题目:有一个大小为100的数组,里面的元素是从 1 到 100,随机从数组中选择50个不重复数。
用 Math.random() * 100
,就可以拿到一个 0 到 99 的随机数,是不是重复50次就可以了?当然不是,假如,第一次随机到5,第二次如果再一次随机到5的话,要求是选择不重复的数,所以要选出50个不重复的数的话,随机次数远远大于50,因为越到后面随机到的数与前面选出的数重复的概率越大。
怎么解决呢?大家都玩过或见过发牌,54张牌,发一张牌,发牌人手里就少一张,直至将所有牌都发完。
![d120808e7095b189bf2863ddcbc1efa0.webp](https://filescdn.proginn.com/442480bb100462c10e14e6fcf067cc2a/d120808e7095b189bf2863ddcbc1efa0.webp)
![de11460e099967c64d0162e0d3bae6ef.webp](https://filescdn.proginn.com/44238e3840997ac8e073d35e3c07bbdd/de11460e099967c64d0162e0d3bae6ef.webp)
同样上面的问题也可以这样解决,第一次随机到一个数后,将这个数取出来,再从剩下的99个数字里随机取出第二个数,这样随机50次取出的书就不会重复,这就是今天的主题:洗牌算法
洗牌算法
Fisher-Yates洗牌算法是由 Ronald A.Fisher和Frank Yates于1938年发明的,后来被Knuth在书中介绍,很多人直接称Knuth洗牌算法, Knuth大家应该比较熟悉,《The Art of Computer Programming》作者,算法理论的创始人。我们现在所使用的各种算法复杂度分析的符号,就是他发明的。
![b6bd89f8b6ec6f0b92cfa51907e2db0e.webp](https://filescdn.proginn.com/d103d4ea1bea6164b499dcae6a560303/b6bd89f8b6ec6f0b92cfa51907e2db0e.webp)
等概率:洗牌算法有些人也称等概率洗牌算法,其实发牌的过程和我们抽签一样的,大学概率论讲过抽签是等概率的,同样洗牌算法选中每个元素是等概率的。
用洗牌算法思路从1、2、3、4、5这5个数中,随机取一个数
![07f4341179edec8b6c218b0a7e7cb0c2.webp](https://filescdn.proginn.com/cff0ed2dcd925261e4c44f327a23b590/07f4341179edec8b6c218b0a7e7cb0c2.webp)
4被抽中的概率是1/5
![88a327d2cf74c80384beca0e7a7e9b39.webp](https://filescdn.proginn.com/06c9867dc47b619ce55ae41ae957981b/88a327d2cf74c80384beca0e7a7e9b39.webp)
5被抽中的概率是1/4*4/5=1/5
![4d817ac185817d7b12bdecbc05719f0c.webp](https://filescdn.proginn.com/d72a72532a6eb15ef0fd0c06e7c6b10e/4d817ac185817d7b12bdecbc05719f0c.webp)
2被抽中的概率是1/3*3/4*4/5=1/5
![7369319a31a8983668f92609137499de.webp](https://filescdn.proginn.com/3865cbb3d55ee83c1bc7b9348896ffc2/7369319a31a8983668f92609137499de.webp)
1被抽中的概率是1/2*1/3*3/4*4/5=1/5
![da5ee403ba4ff1f44540cb892652b26c.webp](https://filescdn.proginn.com/e1cd67cc02e12f78252acecab0ac9496/da5ee403ba4ff1f44540cb892652b26c.webp)
3被抽中的概率是1*1/2*1/3*3/4*4/5=1/5
时间复杂度为O(n*n),空间复杂度为O(n)
算法思路:
在上面的介绍的发牌过程中, Knuth 和 Durstenfeld 在Fisher 等人的基础上对算法进行了改进,在原始数组上对数字进行交互,省去了额外O(n)的空间。该算法的基本思想和 Fisher 类似,每次从未处理的数据中随机取出一个数字,然后把该数字放在数组的尾部,即数组尾部存放的是已经处理过的数字。
在54张牌中随机选一张,将这张牌与第一张交换顺序
![f5003e3375a9c65550dfeb7c68d8e325.webp](https://filescdn.proginn.com/2a85837f34d44282c4188276973db861/f5003e3375a9c65550dfeb7c68d8e325.webp)
在剩下的53张中继续随机选取一张与第二张牌进行交换
![a807f87bffbf26a04a6a24ba0932cad6.webp](https://filescdn.proginn.com/06b72f1c2f4a620b91da0b2e749a2c40/a807f87bffbf26a04a6a24ba0932cad6.webp)
直至最后一张。
![e10b6b13175aec2ac29d84c920021eee.webp](https://filescdn.proginn.com/546b41d4b37136a42d8bde3c0814d0da/e10b6b13175aec2ac29d84c920021eee.webp)
时间复杂度为O(n),空间复杂度为O(1),缺点必须知道数组长度n。
代码:
void Knuth_Durstenfeld_Shuffle(vector&arr)
{
for (int i=arr.size()-1;i>=1;--i)
{
srand((unsigned)time(NULL));
swap(arr[rand()%(i+1)],arr[i]);
}
}
洗牌算法生成雷区:
将排列好的雷,用洗牌算法打乱生成雷区图
for(int i=N*M-1;i>=0;i--)
{
int iX = i/M; //iX为X坐标
int iY = i%M; //iY为Y坐标
int randNumber = (int)(Math.random()*(i+1));
int randX = randNumber/M;
int randY = randNumber%M;
swap(iX,iY,randX,randY);
}
![795692ddb961c25753a8b68abd879e9f.webp](https://filescdn.proginn.com/c62c84338bca3d76a203b8b7d1ce74bc/795692ddb961c25753a8b68abd879e9f.webp)
点【在看】是最大的支持