洗牌算法(Fisher-Yates Shuffle)深度解析:从每日一题到等概率排列的实战实现

发布时间:2026/9/18 17:02:38

洗牌算法(Fisher-Yates Shuffle)深度解析:从每日一题到等概率排列的实战实现 洗牌算法Fisher-Yates Shuffle深度解析从每日一题到等概率排列的实战实现【免费下载链接】leetcodeLeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解记录自己的leetcode解题之路。)项目地址: https://gitcode.com/gh_mirrors/le/leetcode本文是仓库每日一题系列中 2019-07-19 洗牌算法题解 的完整扩展版本。题目要求在O(n)时间内随机打乱一个n元数组并保证所有n!种排列等概率出现每种概率恰为1/n!。读完本文你将掌握经典 Fisher-Yates 洗牌算法的核心思路、JavaScript 实现细节、完整的概率正确性证明以及它与仓库中蓄水池抽样、拒绝采样、三门问题等概率类专题的关联脉络。信息卡片该题解收录于 每日一题汇总 的 2019-07-19 条目属于Array与Probability两个标签的交叉内容时间2019-07-19题目链接暂无tagArrayProbability题目描述假设我们有一个 n 个元素的数组要求你实现一个函数该函数会随机地返回 n 个元素的排列要求所有排列出现的概率是一样的。即每一个排列出现的概率都是 1/n!。对题目做三点解读它们正是本题的验收标准必须随机不能引入任何确定性顺序输出由随机源驱动必须是排列洗牌是原地重排元素集合不变只是顺序改变必须等概率n!种排列每一种被产出的概率都严格等于1/n!这是最容易出错、也最需要证明的一点。朴素思路抽取到新数组的问题最直观的想法是像真实洗牌一样从原数组中随机取出一个元素放入另一个全新的数组不断重复直至取空。这种思路的正确性容易理解——每个元素在每个位置上被取到的概率相同。但它有两个明显的缺陷每次取出都涉及数组删除操作在多数语言的动态数组实现中删除中间元素需要移动后续元素导致总复杂度退化到O(n²)额外开辟了一个等长的新数组空间开销为O(n)。核心思路把取出换成交换原文档给出了一个巧妙的转换把从数组中取出的元素放入原数组的末尾用交换代替删除第 1 次随机取出一个元素时把它与原数组的倒数第 1 个元素交换第 2 次在剩余元素中随机取出时把它与原数组的倒数第 2 个元素交换依次类推第n-1次最后一次无需交换完成洗牌。这样每一轮处理区间都在缩小第i轮只在尚未固定的前缀区间[0, n-i]内做随机选择被选中的元素与区间末元素交换后即被钉死在后缀区后续不再参与随机。整个过程只做了n-1次交换时间复杂度为O(n)空间复杂度为O(1)原地操作仅需一个临时变量。注原文档特别说明第一次交换选择与倒数第 1 个而不是第 1 个交换是因为与末尾元素交换代码更简洁——倒序遍历使得循环边界与随机区间边界天然一致无需额外维护已固定前缀的游标。参考实现JavaScript原题解给出的 JS 代码如下function shuffle(list) { for (let i list.length - 1; i 1; i--) { const random (Math.random() * (i 1)) 0; const temp list[i]; list[i] list[random]; list[random] temp; } }逐行拆解代码作用for (let i list.length - 1; i 1; i--)从最后一个位置开始倒序处理i同时充当当前待固定区间的末尾下标i 1保证最后一轮只剩 1 个元素无需再交换Math.random()返回[0, 1)区间的均匀随机小数Math.random() * (i 1)映射到[0, i1)覆盖区间0..i共i1个整数候选 0位运算取整等价于Math.floor对非负数为截断得到0..i的均匀随机整数三行交换把list[random]与list[i]互换位置i从此固定关键点剖析随机区间每轮收缩第i轮只在[0, i]内取随机下标保证已经固定到后缀的元素绝不会被再次选中这是等概率成立的结构性前提 0与Math.floor的等价性Math.random() * (i 1)的结果为非负浮点数右移 0 位即向下取整可安全使用若读者希望可读性更强也可改写为Math.floor(Math.random() * (i 1))原地修改函数直接修改传入的list无返回值是典型的原地算法in-place。概率正确性证明原文档给出了完整的数学证明这里完整保留并展开推导。第一步任意一个元素放在任意位置的概率均为1/n。放在倒数第 1 个位置的概率第一次随机即在n个元素中选中它概率为1/n放在倒数第 2 个位置的概率第一次没被选中概率(n-1)/n第二次在剩余n-1个元素中被选中概率1/(n-1)两者相乘仍为1/n放在倒数第k个位置的概率为[(n-1)/n] × [(n-2)/(n-1)] × ... × [(n-k1)/(n-k2)] × [1/(n-k1)] 1/n中间的每一项都是上一轮未被选中的条件概率逐项约分后恰为1/n。因此每个元素等概率地落在每一个位置上。第二步所有排列等概率概率为1/n!。洗牌等价于依次确定第 1 位、第 2 位、……、第 n 位的元素。某一种特定排列出现的概率为1/n × 1/(n-1) × ... × 1/2 × 1 1/n!其中第k步是从剩余n-k1个元素中精确选中该排列指定的那一个。这与每一个排列出现的概率都是1/n!的题目要求完全吻合。这与仓库中 蓄水池抽样reservoid sampling 的证明思路同源蓄水池抽样把最终被选中的概率拆成被选中的概率 × 不被替换的概率逐轮约分后所有元素最终概率均为k/n洗牌则把每个位置的概率逐轮约分恒为1/n。二者都是通过逐轮收缩随机区间保证等概率思想的体现。复杂度分析指标数值说明时间复杂度O(n)恰好执行n-1轮每轮一次随机数生成与一次交换空间复杂度O(1)原地交换仅使用常数级临时变量i、random、temp扩展与变体从洗牌到更广的概率算法这道题的价值不只在于一个函数它连接了仓库中多条概率类知识线1. 蓄水池抽样随机抽样 k 个当数据规模大到无法全部载入内存如数据流场景洗牌算法无法直接使用。蓄水池抽样 用大小为k的蓄水池边遍历边替换保证每个元素被最终选中的概率都是k/n。当k n时蓄水池抽样退化为一次完整洗牌两者算法骨架一致。2. 拒绝采样用 Rand7 实现 Rand10洗牌对随机源的均匀性要求极高。仓库中的 470. 用 Rand7() 实现 Rand10 展示了如何用两次rand7()生成7 × 7 49个等概率结果再从中等概率选出 10 个并拒绝其余采样如行号列号法取idx 40再取模保证输出均匀——这与洗牌算法从均匀分布出发构造等概率结构的思维一脉相承也提醒我们当随机源不够均匀或范围不匹配时需要显式的构造或拒绝策略。3. 概率问题的实验验证仓库的每日一题系列中有多次以实验验证概率结论的先例。例如 三门问题题解 附带的 三门问题模拟程序通过Math.random()驱动 100 万次模拟验证换门胜率 2/3这一违反直觉的结论。洗牌算法同样可以照此思路用大样本模拟验证每种排列频率趋近1/n!或验证每个元素出现在每个位置上的频率趋近1/n。4. 栈混洗stack shuffle仓库 基础数据结构英文版 提到合法的栈混洗操作与合法的括号匹配表达式一一对应n个元素的栈混洗种类数恰等于n对括号的合法表达式数量即卡特兰数。注意这与本文的随机洗牌是不同概念——栈混洗只允许通过入栈/出栈重排讨论的是能产生多少种排列而非如何等概率产生排列二者可对照学习。总结算法本质Fisher-Yates 洗牌通过每轮收缩随机区间 原地交换把抽取操作改为交换操作将时间复杂度从朴素的O(n²)优化到O(n)空间复杂度降为O(1)正确性依据逐轮约分证明每个元素落在任意位置的概率均为1/n进而每种排列出现概率为1/n!严格满足题目要求实现要点倒序遍历、随机区间[0, i]每轮收缩、 0取整、与末尾元素交换保持代码简洁知识延伸与 蓄水池抽样、Rand7 实现 Rand10拒绝采样、三门问题概率实验验证共同构成仓库中一条完整的概率算法学习路径建议结合原文与相关题解对比阅读、动手验证。【免费下载链接】leetcodeLeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解记录自己的leetcode解题之路。)项目地址: https://gitcode.com/gh_mirrors/le/leetcode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/18 17:02:38

如何快速导出并可视化微信聊天记录:WeChatMsg 完整指南

如何快速导出并可视化微信聊天记录:WeChatMsg 完整指南 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeCh…

2026/9/18 17:02:38

自适应信号处理实战:LMS与RLS算法原理、Python实现及参数调优

简介:这是一份关于自适应信号处理的PDF学习资料,系统讲解自适应系统的基本概念、信号相关矩阵及其性质、信号与噪声子空间、梯度运算等核心理论,并对比最小均方误差、最大信噪比、最大似然、最小噪声方差等性能准则,梳理最陡下降法…

2026/9/18 18:12:45

Docker与K8s关系全解:从容器镜像到K8s编排落地实战

先把一个常见误区摆到台面上:很多人刚接触容器技术时,会把 Docker 和 Kubernetes(下称 K8s)当成“二选一”的竞品,甚至在群里问“现在都上 K8s 了,Docker 是不是要被淘汰了”。这个问题我这些年被问过不下几…

2026/9/18 18:12:45

Windows批量重命名实战:cmd与PowerShell双方案精解

1. 为什么批量重命名这件事,值得你花20分钟认真读完Windows系统下批量重命名文件,听起来像个小技巧,但实际工作中它可能是你每天节省15分钟、避免3次手抖误操作、防止1次关键文件名错乱导致后续流程中断的底层能力。我做过6年IT支持、带过4届…

2026/9/18 18:12:45

太阳光不同波段如何一步步损伤皮肤?从UVA到红外线的全解析

太阳光里的“隐形刀”:不同波段怎么一点点毁掉你的皮肤我做了这么多年皮肤相关的研究和科普,最常被问到的一个问题就是:“我明明防晒了,为什么还是长斑、长皱纹、皮肤变差?”每次听到这种问题,我都想把人拉…

2026/9/18 18:12:45

Windows本地部署OpenClaw:Docker+WSL2避坑指南

如果你正在 Windows 上尝试部署 OpenClaw,又被一堆教程绕得晕头转向,这篇指南应该能帮你省下不少时间。OpenClaw(圈内习惯叫它“龙虾”)是目前很受欢迎的开源个人 AI 助手,能接微信、接知识库、挂 Skills,部…

2026/9/18 18:12:45

Kali 中 nc/netcat/Ncat 分支差异与实战排错

1. 先搞清楚 Kali 里那个nc到底是哪一个很多人第一次在 Kali 里用nc都会经历同一个瞬间:照着某篇教程敲下nc -z 192.168.1.10 22,终端回你一句nc: invalid option -- z,于是开始怀疑人生——是我打错了,还是 Kali 有问题&#xff…

2026/9/18 18:07:44

C++ const成员函数:原理、应用与最佳实践

1. const成员函数的核心定义与语法在C中,const成员函数是一种特殊的成员函数,它向编译器承诺不会修改对象的任何非静态成员变量。这种承诺通过const关键字来实现,该关键字需要放在函数参数列表之后、函数体之前的位置。语法格式如下&#xff…

2026/9/18 14:13:01

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/18 0:01:09

Google Colab 实战:运行模型、数据加载与报错排查

1. 为什么我劝你先搞懂 Colab 的运行模型1.1 Colab 到底是什么,跟本地跑代码差在哪Google Colab 简单说就是一台跑在浏览器里的 Linux 虚拟机,你打开一个 Notebook,背后就连上了一台带 GPU 的远程机器。你在单元格里敲的每一行 Python&#x…

2026/9/18 0:01:09

C语言数据类型与表达式详解

1. C语言数据与数据类型概述在C语言编程中,数据是程序处理的核心对象。理解数据的分类和特性是掌握C语言的基础。C语言中的数据主要分为四大类:常量、变量、表达式和函数。这些数据类型构成了C语言程序的基本元素,每种类型都有其独特的特性和…

2026/9/18 0:01:09

SQL时间字段指定时间段查询:区间语义、索引与时区避坑

上周排查一个线上问题&#xff0c;用户反馈"昨天的订单一条都没查到"&#xff0c;但数据库里明明躺着两千多条。最后定位下来&#xff0c;不是数据丢了&#xff0c;也不是接口挂了&#xff0c;而是那个查询条件把时间段写成了> 2024-05-20 00:00:00 AND < 2024…

2026/9/18 14:13:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行&#xff0c;Type-C接口算是典型的“看着简单&#xff0c;做起来全坑”的东西。光引脚就24个&#xff0c;高低速信号、电源、控制线全部塞在一个小小的连接器里&#xff0c;如果PCB布局不做规划&#xff0c;打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/18 14:13:02

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/18 14:13:02

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码