【青训营】-JS(4)洗牌之我记

189 阅读1分钟

功能需求
给定一组生成好的抽奖号码,然后我们需要实现一个模块。这个模块的功能是将这组号码打散(即洗牌)然后输出一个中奖的号码 image.png

方法一

html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>

</body>
</html>

JS

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function shuffle(cards) {
  return [...cards].sort(() => Math.random() > 0.5 ? -1 : 1);
}

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.png

缺点

把1到9数字经过shuffle函数随机10000次,然后把每一位出现的数字相加,得到总和。经过多次检验,发现总和数组前面的数字总是很小,后面的数字较大。这就意味着,越大的数字出现在数组后面的概率要大一些。如果洗牌是正确的,那么每个结果大小应该差不多。

sort算法给排序过程一个随机的比较算子 (a,b)=>Math.random()>0.5?-1:1,从而让数组元素交换的过程代码随机性,但是交换过程的随机性并不能保证数学上让每个元素出现在每个位置都具有相同的几率,因为排序算法对每个位置的元素和其他元素交换的次序、次数都是有区别的。

解决:多洗几次一定程度上能解决这个问题

方法二

html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>

</body>
</html>

JS

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

const result = Array(10).fill(0);

for(let i = 0; i < 10000; i++) {
  const c = shuffle(cards);
  for(let j = 0; j < 10; j++) {
    result[j] += c[j];
  }
}

console.log(shuffle(cards));
console.log(result);

在每次抽取的时候,直接将随机到的位置的元素与数组的最后一个元素交换即可。 每次从数组的前i个元素(第0~i-1个元素)种随机挑选一个,将它和第i个元素(下标为i-1)进行交换,然后把i的值减1,直到i的值小于1

这个算法的时间复杂度是O(n),所以性能上应该更好,如果随机排列的数组很大,我们应该选择这种实现

总结:先随机抽取一张牌,然后放到最后去,然后在减少的牌里继续抽,直到没有牌

# 拓展:已抽到的人不再参与抽奖(使用生成器)

html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>

</body>
</html>

js

const cards = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function * draw(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]];
    yield c[i - 1];
  }
}

const result = draw(cards);
console.log([...result]);

将函数改成生成器,将return改成yield
return: 在程序函数中返回某个值,返回之后函数不在继续执行,彻底结束。
yield:  带有yield的函数是一个迭代器,函数返回某个值时,会停留在某个位置,返回函数值后,会在前面停留的位置继续执行,直到程序结束