从技术角度带你揭露抽奖系统的黑幕

290 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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);
}

image.png

从数据看,好像没什么大问题,在仔细看,好像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);

image.png

这样看,好像也还是随机的呢,那一千次的呢?

image.png 哦,也趋于平均。难道真的是随机的吗?

就在我以为确实是随机的时候,我看到了这行代码(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);

image.png

呀儿呦,常威,你还敢说你不会武功?从结果看,越大的数字出现在后面的概率越大。为什么会造成这种结果呢?我们知道,数组的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);
}

image.png

这么看,结果平均了很多。哎,希望他日后好自为之,为避免哥们以后又改回来,摇号时我也选较大的号(手动狗头)。

总结

本文通过技术上的一些分析,揭秘了公司抽奖系统的黑幕,并于最后做了修正。算法从理论上来讲是没问题的,但在效率上不是最优的。如何优化就交给各位了。