洗牌算法
这是我参与「第四届青训营 」笔记创作活动的的第4天。
前几天,在跟着月影学JavaScript课程中,讲了四个小算法,昨天已经发了前两个,有兴趣的可以去看一下Leftpad快速幂与位运算 | 青训营笔记 - 掘金 (juejin.cn),今天再讲一下一个有意思的算法~
洗牌算法
现在有0 ~ 9 一共 10 张牌,我们希望将其完全打乱(完全随机),你有什么方法可以完成。
错误写法
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle(cards) {
return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}
这里利用randmo函数随机到0~1的数,生成1或者-1的随机数,js中 sort 我不是很熟这种写法,理解就相当于如果是 1 的话就正常排序交换,-1 不交换...
看上去好像是随机的没错,但是一想,要想第一个数交换到最后面去,需要交换的次数肯定比第二、三个数交换的次数要多,同理越后面的数想要交换到前面来肯定也会越难
这里我们随机100w次,将100w次上每个位上的分别相加,按理说如果足够随机的话那每个位上的和应该是差不多的值(0 + 9) / 2 * 1000000) = 4500000
但实际上:
很明显其值相差很多,也印证上面所说的,越小的数,越难交换到后面,越大的数越难交换到前面。
正确写法
暴力做法
要想足够随机,不难想到,我们可以将所有情况都记录下来,然后随机里面的一个值,这种肯定是足够随机的。然后就成了一个很经典回溯全排列算法。
#include <iostream>
#include <vector>
#include <ctime>
#include <algorithm>
using namespace std;
vector<int> temp;
vector<vector<int> > res;
void dfs(vector<bool>& isUsed,vector<int>& nums){
if(temp.size() == nums.size()){
res.push_back(temp);
return ;
}
for(int i = 0; i < nums.size(); i++){
if(isUsed[i]) continue;
temp.push_back(nums[i]);
isUsed[i] = true;
dfs(isUsed,nums);
temp.pop_back();
isUsed[i] = false;
}
}
int main()
{
srand((unsigned)time(NULL));
vector<int> nums;
nums.push_back(0), nums.push_back(1), nums.push_back(2), nums.push_back(3);
vector<bool> isUsed(nums.size(),false);
dfs(isUsed, nums);
int t = rand() % res.size();
cout << "随机: ";
for(int i = 0; i < nums.size(); i ++)
cout << res[t][i] << " ";
return 0;
}
但是,很明显,这种做法,需要把所有情况都列举出来,如果有10个数,则有10! = 3628800 种情况,还能应付,如果是一副牌五十多张,那么将所有情况数是一个很恐怖的数字,我们肯定不可能将所有情况存下来。
交换做法
在这里,我们每次不抽取牌出来,而是在任意一种情况下(哪怕升序、降序也可以),随机出两个,然后做交换,交换次数大于等于牌的长度。核心代码如下:
int a = rand() % len, b = rand() % len;
swap(num[a], num[b]);
模拟一下:
初始为 1 2 3 4 5
第一次随机到的下标为 3、1,对应的数为4 和 2,那么将其做交换:1 4 3 2 5
第二次随机到的下标为 0、2,对应的数为1 和 3,那么将其做交换:3 4 1 2 5
第三次随机到4、3,交换:3 4 1 5 2
第四次随机到1、1,交换:3 4 1 5 2
第五次随机到3、2,交换:3 4 5 1 2
交换次数超过牌的长度之后就可以结束了,当然再交换也不会影响其随机性,用数学归纳法也不难证明其合理性。
同样是模拟 100w 次,将100w次上每个位上的分别相加,其结果为:
最大的误差与理想预估值相差不到 0.1%,可见其合理性。
附代码,有兴趣可以运行试试:
#include <iostream>
#include <algorithm>
#include <ctime>
using namespace std;
int nums[10], now[10] = {0,1,2,3,4,5,6,7,8,9};
int randTwo(){ // 交换算法
srand((unsigned)time(NULL));
for(int i = 0; i < 1000000; i ++){
for(int j = 0; j < 10; j ++){
int a = rand() % 10, b = rand() % 10;
swap(now[a], now[b]);
}
for(int j = 0; j < 10; j ++) nums[j] += now[j];
}
for(int i = 0; i < 10; i ++) cout << now[i] << " ";
cout << "\nidex value\n";
for(int i = 0; i < 10; i ++)
cout << " " << i << "\t" << nums[i] << endl;
return 0;
}
int main(){
randTwo();
return 0;
}
今天的分享就到这里了,还有一个是抢红包算法,不出意外是下一篇文章了hh。文章如有不对之处,欢迎指出~