【青训营】写好JS——保证正确

204 阅读1分钟

这是我参与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))

写完多运行几次:

image-20220201190308718

可以看出确实是洗牌了,但是这样随机出来的就公平吗?我们测试一下。

测试

增大测试次数为一百万次,然后记录每个位置出现的数字之和,如果公平的话,这个结果应该都很接近。

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);

测试结果如图:

image-20220201191031852

可以看到每个位置的数字之和差距极大,并没有出现接近的情况,所以这个算法确实是一个不公平的算法。

改进

之前的算法之所以出现问题,是因为sort()导致交换的位置不够随机。

我们可以使用一个经典算法进行改进(如果学过概率论应该很熟悉,就是摸球拿了不放回去):

  • 每次随机抽取一张放到最后
  • 剩下的重复第一步即可

image

而且这个算法可以通过数学归纳法证明每个位置概率都是相等的,写成代码就是这样的:

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));

还用上面的测试代码测试一下正确性:

image-20220201192954064

可以看出来数字都很接近,这个算法就是我们想要的保证正确(公平)的代码。

而且这样写还有一个好处就是:当只需要抽取少量随机牌时,我们并不需要把每张牌都随机一遍(也就是完全洗一遍),性能显然更高。