会排序吗_洗牌算法详解:你会排序,但你会打乱吗?
预计阅读时间: 8 分钟
我知道大家会各种花式排序,但是如果叫你打乱一个数组,你是否能做到胸有成竹?即便你拍脑袋想出一个算法,怎么证明你的算法就是正确的呢?乱序算法不像排序算法,结果唯一可以很容易检验,因为「乱」可以有很多种,你怎么能证明你的算法是「真的乱」呢?
所以我们面临两个问题:
1. 什么叫做「真的乱」?
2. 设计怎样的算法来打乱数组才能做到「真的乱」?
这种算法称为「随机乱置算法」或者「洗牌算法」。
本文分两部分,第一部分详解最常用的洗牌算法。因为该算法的细节容易出错,且存在好几种变体,虽有细微差异但都是正确的,所以本文要介绍一种简单的通用思想保证你写出正确的洗牌算法。第二部分讲解使用「蒙特卡罗方法」来检验我们的打乱结果是不是真的乱。蒙特卡罗方法的思想不难,但是实现方式也各有特点的。
一、洗牌算法
此类算法都是靠随机选取元素交换来获取随机性,直接看代码(伪码),该算法有 4 种形式,都是正确的:
// 得到一个在闭区间 [min, max] 内的随机整数int randInt(int min, int max);
// 第一种写法void shuffle(int[] arr) { int n = arr.length(); /******** 区别只有这两行 ********/ for (int i = 0 ; i // 从 i 到最后随机选一个元素 int rand = randInt(i, n - 1); /*************************/ swap(arr[i], arr[rand]); }}
// 第二种写法 for (int i = 0 ; i 1; i++) int rand = randInt(i, n - 1);
// 第三种写法 for (int i = n - 1 ; i >= 0; i--) int rand = randInt(0, i);
// 第四种写法 for (int i = n - 1 ; i > 0; i--) int rand = randInt(0, i);
分析洗牌算法正确性的准则:产生的结果必须有 n! 种可能,否则就是错误的。这个很好解释,因为一个长度为 n 的数组的全排列就有 n! 种,也就是说打乱结果总共有 n! 种。算法必须能够反映这个事实,才是正确的。
我们先用这个准则分析一下第一种写法的正确性:
// 假设传入这样一个 arrint[] arr = {1,3,5,7,9};
void shuffle(int[] arr) { int n = arr.length(); // 5 for (int i = 0 ; i int rand = randInt(i, n - 1); swap(arr[i], arr[rand]); }}
for 循环第一轮迭代时,i=0,rand 的取值范围是 [0,4],有 5 个可能的取值。
for 循环第二轮迭代时,i=1,rand 的取值范围是 [1,4],有 4 个可能的取值。
后面以此类推,直到最后一次迭代,i=4,rand 的取值范围是 [4,4],只有 1 个可能的取值。
可以看到,整个过程产生的所有可能结果有 5*4*3*2*1=5!=n! 种,所以这个算法是正确的。
分析第二种写法,前面的迭代都是一样的,少了一次迭代而已。所以最后一次迭代时 i = 3,rand 的取值范围是 [3,4],有 2 个可能的取值。
// 第二种写法// arr = {1,3,5,7,9}, n = 5 for (int i = 0 ; i 1; i++) int rand = randInt(i, n - 1);
所以整个过程产生的所有可能结果仍然有 5*4*3*2=5!=n! 种,因为乘以 1 可有可无嘛。所以这种写法也是正确的。
如果以上内容你都能理解,那么你就能发现第三种写法就是第一种写法,只是将数组从后往前迭代而已;第四种写法是第二种写法从后往前来。所以它们都是正确的。
如果读者思考过洗牌算法,可能会想出如下的算法,但是这种写法是错误的:
void shuffle(int[] arr) { int n = arr.length(); for (int i = 0 ; i // 每次都从闭区间 [0, n-1] // 中随机选取元素进行交换 int rand = randInt(0, n - 1); swap(arr[i], arr[rand]); }}
现在你应该明白这种写法为什么会错误了。因为这种写法得到的所有可能结果有 n^n 种,而不是 n! 种,而且 n^n 一般不可能是 n! 的整数倍。
比如说 arr = {1,2,3},正确的结果应该有 3!=6 种可能,而这种写法总共有 3^3 = 27 种可能结果。因为 27 不能被 6 整除,也就是说总概率不可能被平分,一定有某些情况被「偏袒」了。
后文会讲到,概率均等是算法正确的衡量标准,所以这个算法是错误的。
二、蒙特卡罗方法验证正确性
洗牌算法,或者说随机乱置算法的正确性衡量标准是:对于每种可能的结果出现的概率必须相等,也就是说要足够随机。
如果不用数学严格证明概率相等,可以用蒙特卡罗方法近似地估计出概率是否相等,结果是否足够随机。
记得高中有道数学题:往一个正方形里面随机打点,这个正方形里紧贴着一个圆,告诉你打点的总数和落在圆里的点的数量,让你计算圆周率。
这其实就是利用了蒙特卡罗方法:当打的点足够多的时候,点的数量就可以近似代表图形的面积。通过面积公式,由正方形和圆的面积比值是可以很容易推出圆周率的。当然打的点越多,算出的圆周率越准确,充分体现了大力出奇迹的真理。
类似的,我们可以对同一个数组进行一百万次洗牌,统计各种结果出现的次数,把频率作为概率,可以很容易看出洗牌算法是否正确。整体思想很简单,不过实现起来也有些技巧的,下面简单分析几种实现思路。
第一种思路,我们把数组 arr 的所有排列组合都列举出来,做成一个直方图(假设 arr = {1,2,3}):
每次进行洗牌算法后,就把得到的打乱结果对应的频数加一,重复进行 100 万次,如果每种结果出现的总次数差不多,那就说明每种结果出现的概率应该是相等的。写一下这个思路的伪代码:
void shuffle(int[] arr);
// 蒙特卡罗int N = 1000000;HashMap count; // 作为直方图for (i = 0; i int[] arr = {1,2,3}; shuffle(arr); // 此时 arr 已被打乱 count[arr] += 1;}for (int feq : count.values()) print(feq / N + " "); // 频率
这种检验方案是可行的,不过可能有读者会问,arr 的全部排列有 n! 种(n 为 arr 的长度),如果 n 比较大,那岂不是空间复杂度爆炸了?
是的,不过作为一种验证方法,我们不需要 n 太大,最多用长度为 5 或 6 的 arr 试下就差不多了吧,因为我们只想比较概率验证一下正确性而已。
第二种思路,可以这样想,arr 数组中全都是 0,只有一个 1。我们对 arr 进行 100 万次打乱,记录每个索引位置出现 1 的次数,如果每个索引出现 1 的次数差不多,也可以说明每种打乱结果的概率是相等的。
void shuffle(int[] arr);
// 蒙特卡罗方法int N = 1000000; int[] arr = {1,0,0,0,0};int[] count = new int[arr.length];for (int i = 0; i shuffle(arr); // 打乱 arr for (int j = 0; j if (arr[j] == 1) { count[j]++; break; }}for (int feq : count) print(feq / N + " "); // 频率
这种思路也是可行的,而且避免了阶乘级的空间复杂度,但是多了嵌套 for 循环,时间复杂度高一点。不过由于我们的测试数据量不会有多大,这些问题都可以忽略。
另外,细心的读者可能发现一个问题,上述两种思路声明 arr 的位置不同,一个在 for 循环里,一个在 for 循环之外。其实效果都是一样的,因为我们的算法总要打乱 arr,所以 arr 的元素顺序并不重要,只要所含元素不变就行。
三、最后总结
本文第一部分介绍了洗牌算法(随机乱置算法),通过一个简单的分析技巧证明了该算法的四种正确形式,并且分析了一种常见的错误写法,相信你一定能够写出正确的洗牌算法了。
第二部分写了洗牌算法正确性的衡量标准,即每种随机结果出现的概率必须相等。如果我们不用严格的数学证明,可以通过蒙特卡罗方法大力出奇迹,粗略验证算法的正确性。蒙特卡罗方法也有不同的思路,不过要求不必太严格,因为我们只是寻求一个简单的验证。
●编号965,输入编号直达本文
●输入m获取文章目录
程序员求职面试
分享程序员找工作经验
程序员笔试、面试题
会排序吗_洗牌算法详解:你会排序,但你会打乱吗?相关推荐
- python实现洗牌算法_洗牌算法及 random 中 shuffle 方法和 sample 方法浅析
对于算法书买了一本又一本却没一本读完超过 10%,Leetcode 刷题从来没坚持超过 3 天的我来说,算法能力真的是渣渣.但是,今天决定写一篇跟算法有关的文章.起因是读了吴师兄的文章 <扫雷与 ...
- 麻将通用胡牌算法详解(拆解法)
1.背景 前几天刚好有项目需要胡牌算法,查阅资料后,大部分胡牌算法的博客都是只讲原理,实现太过简单,且没有给出测试用例.然后就有了下面的这个胡牌算法,我将从算法原理和算法实现两部分展开,想直接用的,直 ...
- raptor五个数排序流程图_数据结构与算法(一):排序(上)
做这个系列一是记录自己的学习过程,二是整合目前我所接触的比较好的资料,给出最直观,最通俗的算法解释 总体概况 十大排序算法:(比较排序):冒泡.选择.插入.归并.快速.希尔.堆排序 基数排序.桶排序. ...
- python随机森林 交叉验证_随机森林算法详解及Python实现
一 简介 随机森林是一种比较有名的集成学习方法,属于集成学习算法中弱学习器之间不存在依赖的一部分,其因为这个优点可以并行化运行,因此随机森林在一些大赛中往往是首要选择的模型. 随机森立中随机是核心,通 ...
- 洗牌算法具体指的是什么
前言: 这里是修真院前端小课堂,每篇分享文从 [背景介绍][知识剖析][常见问题][解决方案][编码实战][扩展思考][更多讨论][参考文献] 八个方面深度解析前端知识/技能,本篇分享的是: [洗牌算 ...
- 快排亲兄弟:快速选择算法详解
后台回复进群一起刷力扣???? 点击下方卡片可搜索文章???? 读完本文,可以去力扣解决如下题目: 215.数组中的第 K 个最大元素(Medium) 快速选择算法是一个非常经典的算法,和快速排序算法 ...
- js排序算法详解-堆排序
全栈工程师开发手册 (作者:栾鹏) js系列教程5-数据结构和算法全解 js排序算法详解-堆排序 这种排序方式呢,理论性太强,看动图的时候满脸写着懵逼,多看几遍似乎明白了编者的意图,但是要把这种理论的 ...
- 十大经典排序算法详解(三)-堆排序,计数排序,桶排序,基数排序
养成习惯,先赞后看!!! 你的点赞与关注真的对我非常有帮助.如果可以的话,动动手指,一键三连吧!!! 十大经典排序算法-堆排序,计数排序,桶排序,基数排序 前言 这是十大经典排序算法详解的最后一篇了. ...
- 游戏洗牌算法——常用+详解最优Knuth_Durstenfeld算法
目录 前言 基于Unity的洗牌算法代码实现 内容 抽牌洗牌 原理 复杂度 优缺点 Fisher_Yates算法 原理 复杂度 代码实现 优缺点 Knuth_Durstenfeld算法(最佳洗牌算法) ...
最新文章
- 二叉树 —— 中序遍历结点的后继
- Android鬼点子 CircleProgressView
- 豆瓣评分9.4分!这部大片你不应该错过,每一秒都是不敢看的残忍!
- 对象拷贝的工具类DeepBeanUtils
- 清华发布新版计算机学科推荐学术会议和期刊列表,与CCF有何不同?
- SpringMVC杂记(1) 使用阿里巴巴的fastjson
- Slave_SQL线程异常终止处理之跳过错误
- Python+OpenCV:图像轮廓
- 北京林业大学本科毕业论文答辩和论文选题PPT模板
- 团队管理之《带团队,就是用好你身边的人》
- 三星手机投屏电脑教程 手机和电脑同屏
- 遥感期刊论文速读2(2021年8月12日)
- 微信号名称乱码什么情况_微信号改成什么好?
- python量化期权_如何20小时搞定Python量化期权实战?
- Poly-YOLO:更快,更精确的检测(主要解决Yolov3两大问题,附源代码)
- 汉寿计算机职业中专,汉寿第一职业中专
- 张孝祥java邮件开发_张孝祥java邮件开发详解笔记(生成文本邮件)
- 解决There is not enough memory to perform the requested operation……
- DanceNN:字节自研千亿级规模文件元数据存储系统概述
- 上接头通管机械加工工艺及相关工序夹具设计毕业设计全套