持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
前言
Hi,all,不知道大家有没有觉得自己是中奖绝缘体呢?反正对于我来讲,我发现我和中奖这类活动八字犯冲,非酋本酋了。还记得年会时,公司组织的抽奖活动上,写抽奖系统的哥哥抽中了大奖。本来以为吧,一切皆是巧合,事实真的这样吗,最近把他的抽奖系统代码死磨硬泡的求了过来,细细一读,好家伙,有黑幕。今天就来扒一扒吧。
啊,大家认为js中,Math.random()是真正随机的吗?随机不随机,得验证才知道。那就写点东西验证一下吧。
我们先来看看,在10w次中,0-9出现的次数有多少呢?先来看看十次结果的。
for(let j = 0; j < 10; j++){
const obj = {
0: 0,
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
6: 0,
7: 0,
8: 0,
9: 0,
};
for (let i = 0; i < 100000; i++) {
const num = Math.floor(Math.random() * 10);
obj[num] += 1;
}
console.log(obj);
}
从数据看,好像没什么大问题,在仔细看,好像0-4出现的次数比5-9出现的次数少一些,越往后超过1w次的越多。是这样吗?再来写个demo验证一下。
我们再来看看,在10w次中,>=0.5出现的次数有多少呢,<0.5的呢?在n次结果中,>=0.5出现的次数多的和<0.5出现次数多的是否接近一致呢?先来看看十次结果的。
let a = 0;
let b = 0;
for(let j = 0; j < 10; j++){
const obj = {
0: 0,
1: 0,
};
for (let i = 0; i < 100000; i++) {
const num = Math.random() >= 0.5 ? 1 : 0;
obj[num] += 1;
}
if(obj[0] > obj[1]){
b++
}else{
a++
}
console.log(obj);
}
console.log(a, b);
这样看,好像也还是随机的呢,那一千次的呢?
哦,也趋于平均。难道真的是随机的吗?
就在我以为确实是随机的时候,我看到了这行代码(a, b) => Math.random() > 0.5 ? -1 : 1。这行代码有什么问题吗?我们来验证一下。
function weightsSort(items) {
return items.sort((a, b) => Math.random() > 0.5 ? -1 : 1);
}
const weights = Array(9).fill(0);
for (let j = 0; j < 10; j++) {
const weights = Array(9).fill(0);
for(let i = 0; i < 10000; i++) {
const testItems = [1, 2, 3, 4, 5, 6, 7, 8, 9];
weightsSort(testItems);
testItems.forEach((item, idx) => weights[idx] += item);
}
console.log(weights);
}
console.log(weights);
呀儿呦,常威,你还敢说你不会武功?从结果看,越大的数字出现在后面的概率越大。为什么会造成这种结果呢?我们知道,数组的sort方法内部是一个排序算法,我们不知道它的具体实现,只清楚他是快排与插入排序的结合实现。(a, b) => Math.random() > 0.5 ? -1 : 1这个算法给排序过程一个随机的比较数,从而让数组元素的交换过程代码随机性。问题来啦,我们虽然保证了交换过程的随机性,但不能保证数学上让每个元素出现在每个位置都具有相同的几率(因为排序算法对每个位置的元素和其他元素交换的次序、次数都是有区别的)。
卧槽,破案了有没有?怪不得摇号抽奖他要选后面的。感情还有这一层原因。莫慌,待老夫悄咪咪改两行代码降服他?
我们每次从数组中随机挑选元素,将这个元素从原数组的副本中删除并放入新的数组,这样就可以保证每一个数在每个位置的概率是相同的。
function weightsSort(items) {
items = [...items];
const ret = [];
while(items.length) {
const idx = Math.floor(Math.random() * items.length);
const item = items.splice(idx, 1)[0];
ret.push(item);
}
return ret;
}
for (let j = 0; j < 10; j++) {
const weights = Array(9).fill(0);
for(let i = 0; i < 10000; i++) {
const testItems = [1, 2, 3, 4, 5, 6, 7, 8, 9];
weightsSort(testItems).forEach((item, idx) => weights[idx] += item);
}
console.log(weights);
}
这么看,结果平均了很多。哎,希望他日后好自为之,为避免哥们以后又改回来,摇号时我也选较大的号(手动狗头)。
总结
本文通过技术上的一些分析,揭秘了公司抽奖系统的黑幕,并于最后做了修正。算法从理论上来讲是没问题的,但在效率上不是最优的。如何优化就交给各位了。