热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

探讨两种常数卷积的结果与一种常见的洗牌算法错误及其影响

在编程中,随机打乱数组元素的顺序(即“洗牌”)是一个常见的需求。标准的洗牌算法是Fisher-Yatesshuffle,但许多开发者在实现时容易犯错,导致结果不均匀。本文探讨了两种常数卷积的结果,并分析了一种常见的洗牌算法错误及其对随机性的影响。通过详细的实验和理论分析,我们揭示了这些错误的具体表现和潜在危害,为开发者提供改进的建议。
8d617b995357f3ac580637cda36a96ce.png

  「洗牌」,或者说随机打乱一个数组中元素的顺序,是编程中的一个常见需求。标准的洗牌算法是 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 }

其原理是,在第 i 次循环中,从所有元素中等可能地选一个元素,与第 i 个元素交换。这种算法的错误可以如下证明:对于一个长度为

的数组,算法创造了
个等可能的基本事件,这些事件对应于
种排列顺序。在非平凡情况下,
不能被
整除,所以各种排列顺序不可能等概率。

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

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

Javascript 中数组自带的 sort 方法允许提供一个比较器,其返回值的正负号代表两个元素的大小关系。在上面的代码中,比较器返回的是 -0.5 到 0.5 之间的一个随机数,也就是说每次比较的结果是随机且均匀的。但是,基于随机比较的整个洗牌算法是不均匀的:它的各种运行结果的概率都形如

(
为算法执行过程中的比较次数),而我们希望每种顺序的概率都是
,在非平凡情况下,后者不能由前者通过加法组合出来。@Lucas HC 指出,当 sort 函数采用
插入排序的实现时,各个元素都有较大的概率留在初始位置,并通过统计多次运行的结果进行了验证。

  如果你不熟悉编程,我在此可以用大白话把 @Lucas HC 答案中错误算法的流程叙述一下:设想有

个人依次来到一个队伍。每个人到来之后,都想向前插队,但前面每一个人放他过去的概率都是 1/2。最终队伍的状态就是洗牌结果。
8c49178f3d89833ac56ba5acb88478f0.png
5 号元素插到了 3 号位置。这个事件要发生,需要队尾的两个元素放它过去,而前面的一个元素不放它过去,故概率为 1/8。

  我对这种算法的结果分布感到好奇,于是计算了一下洗牌后各个元素落在各个位置的概率。用

表示总共有
个元素时,第
个元素洗牌后落在第
个位置的概率(这里
的范围为 1 到
)。给定
,各个
可以排成一个
阶矩阵,记作
。这些矩阵可以用递推法计算:
  • 时,显然有
  • 时:
    • 首先考虑
      号元素。它有 1/2 的概率停在
      号位置,有 1/4 的概率向前插一位停在
      号位置,有 1/8 的概率向前插两位停在
      号位置 …… 一般地,若
      号元素要停在
      号位置(
      ),它就需要越过
      个元素,并被
      号位置的元素挡住,故概率为
      ,但停在 1 号位置的概率跟停在 2 号位置的概率相同,都是
    • 然后考虑
      (
      )号元素。它落在
      号位置有两种可能:
      • 在前
        个元素洗牌完毕后,
        号元素就已经落在
        号位置,并且
        号元素没能越过它。这种情况的概率为
        ;
      • 在前
        个元素洗牌完毕后,
        号元素落在
        号位置,但
        号元素越过了它,使它后移到了
        号位置。这种情况的概率为
      • 于是有递推式:

  上面的递推式用 Matlab 可以如下计算。计算过程使用了大量的矩阵运算,看不懂不必强求。

N = 100;
P = cell(1, N);
P{1} = 1;
for n = 2:Nx = 0.5 .^ [n-1:-1:1];P{n} = zeros(n, n);P{n}(1:n-1, 1:n-1) = bsxfun(@times, P{n-1}, 1 - x);P{n}(1:n-1, 2:n) = P{n}(1:n-1, 2:n) + bsxfun(@times, P{n-1}, x);P{n}(n, 1) = x(1);P{n}(n, 2:n) = x;
end

  上述代码算出的

如下。左图直接显示了整个矩阵,颜色深浅表示概率大小;右图则是画出了矩阵的每一行,即每个元素最终位置的分布。
20ea8b4d9004c3afdfc368762852d6fe.png
使用错误的算法洗牌后,10 个元素位于各个位置的概率

@Lucas HC 的结论,就是说

的对角线元素会显著大于
。上图说明,
的对角线元素都大于 0.2,印证了这个结论。从图中还能看出另外几个事实:
  • 是对称的,即
    号元素落在
    号位置的概率,等于
    号元素落在
    号位置的概率;
  • 的前两行、前两列分别相等,也就是说 1、2 号元素的地位相同,1、2 号位置的地位也相同。

  把

增大,可以发现一些更有趣的事情。例如,下图画出的是
:
2d72e2cbaccaaecdf71ef46916c6948f.png
使用错误的算法洗牌后,30 个元素位于各个位置的概率

从左图可以看到,矩阵中元素沿垂直于对角线的方向迅速衰减,所以「洗牌」后各个元素都倾向于留在初始位置或其附近。而右图则说明,除了开头和结尾几个元素,每个元素最终位置分布曲线的形状都几乎是一样的,它们的最大值(即留在原位的概率)都比 0.2 大一点,且这个值似乎与

无关!

  在 Matlab 中执行 format long 命令,可以让计算结果具有 15 位有效数字。用此方法可以观察到

的对角线中部元素趋向于一个常数
,它约等于 0.220643036096533。尽管
的绝大多数对角元都只比常数
大一丁点儿,不过如果要在矮子里面再挑矮子,那么最小的对角元大约出现在第
个位置。当
时,15 位有效数字就已经无法区分
与常数
了。

  网上并没有找到对常数

的讨论,所以我就暂且给它起个名字,叫「乱排常数」。这个常数是有理数还是无理数呢?它有什么样的数学表达式?这些有趣的问题,就留到下一篇中讨论。



推荐阅读
  • 2019年后蚂蚁集团与拼多多面试经验详述与深度剖析
    2019年后蚂蚁集团与拼多多面试经验详述与深度剖析 ... [详细]
  • Python与R语言在功能和应用场景上各有优势。尽管R语言在统计分析和数据可视化方面具有更强的专业性,但Python作为一种通用编程语言,适用于更广泛的领域,包括Web开发、自动化脚本和机器学习等。对于初学者而言,Python的学习曲线更为平缓,上手更加容易。此外,Python拥有庞大的社区支持和丰富的第三方库,使其在实际应用中更具灵活性和扩展性。 ... [详细]
  • 开发心得:利用 Redis 构建分布式系统的轻量级协调机制
    开发心得:利用 Redis 构建分布式系统的轻量级协调机制 ... [详细]
  • 本文探讨了使用JavaScript实现多种经典排序算法的高效方法,包括冒泡排序、选择排序、插入排序、归并排序和快速排序。为了确保代码的结构清晰和可维护性,我们首先定义了一个 `ArrayList` 类,该类中包含了待排序的数组声明。通过这种方式,我们不仅能够更好地组织代码,还能提高算法的执行效率和可读性。此外,我们还对每种排序算法进行了详细的性能分析和优化建议,以帮助开发者在实际应用中选择最合适的排序方法。 ... [详细]
  • Node.js 教程第五讲:深入解析 EventEmitter(事件监听与发射机制)
    本文将深入探讨 Node.js 中的 EventEmitter 模块,详细介绍其在事件监听与发射机制中的应用。内容涵盖事件驱动的基本概念、如何在 Node.js 中注册和触发自定义事件,以及 EventEmitter 的核心 API 和使用方法。通过本教程,读者将能够全面理解并熟练运用 EventEmitter 进行高效的事件处理。 ... [详细]
  • 深入解析JavaScript词法分析的具体流程与常见问题 ... [详细]
  • PHP中元素的计量单位是什么? ... [详细]
  • Java服务问题快速定位与解决策略全面指南 ... [详细]
  • 本文详细探讨了Java集合框架的使用方法及其性能特点。首先,通过关系图展示了集合接口之间的层次结构,如`Collection`接口作为对象集合的基础,其下分为`List`、`Set`和`Queue`等子接口。其中,`List`接口支持按插入顺序保存元素且允许重复,而`Set`接口则确保元素唯一性。此外,文章还深入分析了不同集合类在实际应用中的性能表现,为开发者选择合适的集合类型提供了参考依据。 ... [详细]
  • 本题库精选了Java核心知识点的练习题,旨在帮助学习者巩固和检验对Java理论基础的掌握。其中,选择题部分涵盖了访问控制权限等关键概念,例如,Java语言中仅允许子类或同一包内的类访问的访问权限为protected。此外,题库还包括其他重要知识点,如异常处理、多线程、集合框架等,全面覆盖Java编程的核心内容。 ... [详细]
  • 在启用分层编译的情况下,即时编译器(JIT)的触发条件涉及多个因素,包括方法调用频率、代码复杂度和运行时性能数据。本文将详细解析这些条件,并探讨分层编译如何优化JVM的执行效率。 ... [详细]
  • Windows环境下详细教程:如何搭建Git服务
    Windows环境下详细教程:如何搭建Git服务 ... [详细]
  • 本书详细介绍了在最新Linux 4.0内核环境下进行Java与Linux设备驱动开发的全面指南。内容涵盖设备驱动的基本概念、开发环境的搭建、操作系统对设备驱动的影响以及具体开发步骤和技巧。通过丰富的实例和深入的技术解析,帮助读者掌握设备驱动开发的核心技术和最佳实践。 ... [详细]
  • 高效排序算法是提升数据处理速度的重要技术。通过优化排序算法,可以显著提高数据处理的效率和性能。本文介绍了几种常见的高效排序算法,如快速排序、归并排序和堆排序,并通过实例代码展示了它们的具体实现。实验结果表明,这些算法在大规模数据集上的表现尤为突出,能够有效减少数据处理时间,提升系统整体性能。 ... [详细]
  • RancherOS 是由 Rancher Labs 开发的一款专为 Docker 设计的轻量级 Linux 发行版,提供了一个全面的 Docker 运行环境。其引导镜像仅 20MB,非常适合在资源受限的环境中部署。本文将详细介绍如何在 ESXi 虚拟化平台上安装和配置 RancherOS,帮助用户快速搭建高效、稳定的容器化应用环境。 ... [详细]
author-avatar
骷髅怪天堂_821
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有