这是我参与「第四届青训营 」笔记创作活动的第6天
前一阵子在学习Javascript的课程中,老师着重讲解了洗牌算法。简而言之,就是如何编写一个程序使得所有的牌都可以随机排列。这其中涉及到两个问题,一个是算法(如何保证完全随机),另一个是检验(往往被忽略,如何检验是否随机)。从音乐的随机播放器到游戏的自动生成地图都应用了洗牌算法。作者希望通过本文利用Javascript对该算法的Knuth-Shuffle方法进行一些浅显的讨论。
1.抽象模型
实际上,洗牌问题不仅仅作用于54张扑克牌,而是对一串数字随机排序。我们可以尝试化简该问题,所谓的排列实际上就是每次从剩下的牌中抽取,一张一张抽出来就是一个随机数列了,因此我们可以把问题转化为:
对于1-10的数组,如何随机抽取一个数字?
对于1-10的数组,如何随机抽取两个数字?
...
对于1-10的数组,如何随机抽取九个数字?
这里笔者第一个想到的是利用随机数,但是问题在于,想要实现上述功能,需要在下一次抽取中把抽取过的数字从列表中剔除,对于冗长的数组,这又是十分麻烦的。
因而,我们可以换一种思路,即不再抽取列表中剩余的数,而是把列表中的数字提前乱序排列再按照顺序抽取,这样子的复杂度将会小许多。
根据Knuth-Shuffle的洗牌算法,我们只需要将最后一个数和前面任意 n-1 个数中的一个数进行交换(也可以不换),然后倒数第二个数和前面任意 n-2 个数中的一个数进行交换,如此往复直到最后一个元素,就完成了洗牌,该算法保证了每个元素在每个位置的概率都是相等的。
这种方法是极其快速的,因为只有一个for循环,易知时间复杂度和空间复杂度均为O(n),这里不再计算,直接给出javascript程序,即:
function shuffle(cards) {
//抽取最后一张牌
for(let i = cards.length-1; i >= 0; i--) {
//从前面一张到第一张随机选择一张卡牌
var randomIndex = Math.floor(i * Math.random())
//调换顺序
const temp = cards[randomIndex]
cards[randomIndex] = cards[i]
cards[i] = temp
}
return cards
}
2.验证模型
下面的问题是,如何检验我们的算法是正确的,有没有直观的方法能够检验? 分析洗牌算法正确性的准则:产生的结果必须有 n! 种可能,否则就是错误的。 从数学角度,我们计算,进行第一轮迭代时,最后一张牌的换位置方式有54种,倒数第二张牌的换位置方式有53种……以此类推,我们可以知道54张牌总共有54!种方式,与n!结果相同。因此可知我们的算法是正确的。
结语
本人才疏学浅,如文章有不正确或存疑之处,欢迎评论指出!如果帮到您,希望点赞关注,鼓励作者~