这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战。
在实际项目当中我们经常会出现以下问题:
- 代码可以跑通,但是业务上总出bug
- 业务上没有异常,但是代码执行的过程中总是出现异常
- 代码没问题,测试也没问题,但不能保证正确性,或者说保证公平
一个例子:洗牌
如果让你实现一个洗牌算法,你会怎么实现?
Math.random()
如果使用Math.random()我会这样实现:
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
function shuffle(cards) {
return [...cards].sort(() =>
Math.random() > 0.5 ? -1 : 1
)
}
console.log(shuffle(cards))
写完多运行几次:
可以看出确实是洗牌了,但是这样随机出来的就公平吗?我们测试一下。
测试
增大测试次数为一百万次,然后记录每个位置出现的数字之和,如果公平的话,这个结果应该都很接近。
const result = Array(10).fill(0);
for(let i = 0; i < 1000000; i++) {
const c = shuffle(cards);
for(let j = 0; j < 10; j++) {
result[j] += c[j];
}
}
console.log(result);
测试结果如图:
可以看到每个位置的数字之和差距极大,并没有出现接近的情况,所以这个算法确实是一个不公平的算法。
改进
之前的算法之所以出现问题,是因为sort()导致交换的位置不够随机。
我们可以使用一个经典算法进行改进(如果学过概率论应该很熟悉,就是摸球拿了不放回去):
- 每次随机抽取一张放到最后
- 剩下的重复第一步即可
而且这个算法可以通过数学归纳法证明每个位置概率都是相等的,写成代码就是这样的:
const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function shuffle(cards) {
const c = [...cards];
for (let i = c.length; i > 0; i--) {
const pIdx = Math.floor(Math.random() * i);
[c[pIdx], c[i - 1]] = [c[i - 1], c[pIdx]];
}
return c;
}
console.log(shuffle(cards));
还用上面的测试代码测试一下正确性:
可以看出来数字都很接近,这个算法就是我们想要的保证正确(公平)的代码。
而且这样写还有一个好处就是:当只需要抽取少量随机牌时,我们并不需要把每张牌都随机一遍(也就是完全洗一遍),性能显然更高。