[算法]洗牌算法

本文为王赟博士在知乎上的文章:10809 一种错误的洗牌算法,以及乱排常数 (1)节选

「洗牌」,或者说随机打乱一个数组中元素的顺序,是编程中的一个常见需求。标准的洗牌算法是 Fisher-Yates shuffle,用 JavaScript 实现如下:

function shuffle(A) {
    for (var i = A.length - 1; i > 0; i--) {
        var j = Math.floor(Math.random() * (i + 1));
        var t = A[i]; A[i] = A[j]; A[j] = t;
    }
}

其基本思路是,每次从未打乱的部分等可能地选一个元素,把它与未打乱部分的最后一个元素交换。

Fisher-Yates 洗牌算法的实现十分简单,并且它可以保证均匀性,即元素的各种排列顺序出现的概率都相等。但是,很多人闭门造车地发明了一些「错误」的洗牌算法实现,它们不能保证均匀。例如,最常见的一种错误实现如下:

function shuffle(A) {
   for (var i = 0; i < A.length; i++) {
       var j = Math.floor(Math.random() * A.length);
       var t = A[i]; A[i] = A[j]; A[j] = t;
   }
}

其原理是,在第 i 次循环中,从所有元素中等可能地选一个元素,与第 i 个元素交换。这种算法的错误可以如下证明:对于一个长度为 [公式] 的数组,算法创造了 [公式] 个等可能的基本事件,这些事件对应于 [公式] 种排列顺序。在非平凡情况下, [公式] 不能被 [公式] 整除,所以各种排列顺序不可能等概率。

另一种错误的洗牌算法是 @Lucas HC这篇答案中指出的:

A.sort(function() {
    return .5 - Math.random();
}); 

补充:据说网狐棋牌中洗牌就用的该算法。

JavaScript 中数组自带的 sort 方法允许提供一个比较器,其返回值的正负号代表两个元素的大小关系。在上面的代码中,比较器返回的是 -0.5 到 0.5 之间的一个随机数,也就是说每次比较的结果是随机且均匀的。但是,基于随机比较的整个洗牌算法是不均匀的:它的各种运行结果的概率都形如 [公式]([公式] 为算法执行过程中的比较次数),而我们希望每种顺序的概率都是 [公式] ,在非平凡情况下,后者不能由前者通过加法组合出来。
Lucas HC 指出,当 sort 函数采用插入排序的实现时,各个元素都有较大的概率留在初始位置,并通过统计多次运行的结果进行了验证。

如果你不熟悉编程,我在此可以用大白话把 Lucas HC答案中错误算法的流程叙述一下:设想有 [公式] 个人依次来到一个队伍。每个人到来之后,都想向前插队,但前面每一个人放他过去的概率都是 1/2。最终队伍的状态就是洗牌结果。
在这里插入图片描述5 号元素插到了 3 号位置。这个事件要发生,需要队尾的两个元素放它过去,而前面的一个元素不放它过去,故概率为 1/8。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值