关于JavaScript的数组随机排序

昨天了解了一下Fisher–Yates shuffle费雪耶兹随机置乱算法,现在再来看看下面这个曾经网上常见的一个写法:

function shuffle(arr) { arr.sort(function () { return Math.random() - 0.5; }); 
}

或者使用更简洁的 ES6 的写法:

function shuffle(arr) { arr.sort(() => Math.random() - 0.5); } 

但是这种写法是有问题的,它并不能真正地随机打乱数组。

问题

看下面的代码,我们生成一个长度为 10 的数组['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'],使用上面的方法将数组乱序,执行多次后,会发现每个元素仍然有很大机率在它原来的位置附近出现。

let n = 10000; let count = (new Array(10)).fill(0); for (let i = 0; i < n; i ++) { let arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; arr.sort(() => Math.random() - 0.5); count[arr.indexOf('a')]++; } console.log(count); 

在 浏览器控制台 中执行,输出[ 2891, 2928, 1927, 1125, 579, 270, 151, 76, 34, 19 ](带有一定随机性,每次结果都不同,但大致分布应该一致),即进行 10000 次排序后,字母'a'(数组中的第一个元素)有约 2891 次出现在第一个位置、2928 次出现在第二个位置,与之对应的只有 19 次出现在最后一个位置。如果把这个分布绘制成图像,会是下面这样:

类似地,我们可以算出字母'f'(数组中的第六个元素)在各个位置出现的分布为[ 312, 294, 579, 1012, 1781, 2232, 1758, 1129, 586, 317 ],图像如下:

如果排序真的是随机的,那么每个元素在每个位置出现的概率都应该一样,实验结果各个位置的数字应该很接近,而不应像现在这样明显地集中在原来位置附近。因此,我们可以认为,使用形如arr.sort(() => Math.random() - 0.5)这样的方法得到的并不是真正的随机排序。

另外,需要注意的是上面的分布仅适用于数组长度不超过 10 的情况,如果数组更长,比如长度为 11,则会是另一种分布。比如:

function newarr(){
let a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']; // 长度为11
let n = 10000; 
var count = (new Array(a.length)).fill(0); 
for (var i = 0; i < n; i ++) { var arr = [].concat(a); arr.sort(() => Math.random() - 0.5); count[arr.indexOf('a')]++; 
} 
//console.log(count);
return count;
}newarr();

在 浏览器控制台 中多次执行,其中第一个元素'a'的分布位置结果如下:

(11) [785, 826, 629, 652, 937, 1079, 960, 680, 617, 986, 1849]
newarr()
(11) [844, 816, 636, 665, 947, 1053, 901, 654, 661, 982, 1841]
newarr()
(11) [804, 829, 622, 655, 923, 1093, 916, 667, 591, 974, 1926]
newarr()
(11) [779, 793, 655, 713, 916, 1161, 911, 642, 579, 936, 1915]
newarr()
(11) [786, 783, 607, 653, 956, 1116, 954, 655, 619, 1028, 1843]
newarr()
(11) [867, 797, 647, 635, 943, 1056, 929, 652, 572, 977, 1925]

虽然数组长度大于10后比之前的分布更均匀,但是明显还有问题(最后一个最大)。

分布不同的原因是 v8 引擎中针对短数组和长数组使用了不同的排序方法(下面会讲)。可以看到,两种算法的结果虽然不同,但都明显不够均匀。

探索

看了一下ECMAScript中关于Array.prototype.sort(comparefn)的标准,其中并没有规定具体的实现算法,但是提到一点:

Calling comparefn(a,b) always returns the same value v when given a specific pair of values a and b as its two arguments.

也就是说,对同一组a、b的值,comparefn(a, b)需要总是返回相同的值。而上面的() => Math.random() - 0.5(即(a, b) => Math.random() - 0.5)显然不满足这个条件。

翻看v8引擎数组部分的源码,注意到它出于对性能的考虑,对短数组使用的是插入排序,对长数组则使用了快速排序,至此,也就能理解为什么() => Math.random() - 0.5并不能真正随机打乱数组排序了。(有一个没明白的地方:源码中说的是对长度小于等于 22 的使用插入排序,大于 22 的使用快排,但实际测试结果显示分界长度是 10。)

解决方案

知道问题所在,解决方案也就比较简单了。

方案一

既然(a, b) => Math.random() - 0.5的问题是不能保证针对同一组a、b每次返回的值相同,那么我们不妨将数组元素改造一下,比如将每个元素i改造为:

let new_i = { v: i, r: Math.random() }; 

即将它改造为一个对象,原来的值存储在键v中,同时给它增加一个键r,值为一个随机数,然后排序时比较这个随机数:

arr.sort((a, b) => a.r - b.r); 

完整代码如下:

function shuffle(arr) { let new_arr = arr.map(i => ({v: i, r: Math.random()})); new_arr.sort((a, b) => a.r - b.r); arr.splice(0, arr.length, ...new_arr.map(i => i.v)); } let a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; let n = 10000; let count = (new Array(a.length)).fill(0); for (let i = 0; i < n; i ++) { shuffle(a); count[a.indexOf('a')]++; } console.log(count); 

一次执行结果为:[ 1023, 991, 1007, 967, 990, 1032, 968, 1061, 990, 971 ]。多次验证,同时在这儿查看shuffle(arr)函数结果的可视化分布,可以看到,这个方法可以认为足够随机了。

方案二(Fisher–Yates shuffle)

需要注意的是,上面的方法虽然满足随机性要求了,但在性能上并不是很好,需要遍历几次数组,还要对数组进行splice等操作。

考察Lodash 库中的 shuffle 算法,注意到它使用的实际上是Fisher–Yates 洗牌算法,这个算法由 Ronald Fisher 和 Frank Yates 于 1938 年提出,然后在 1964 年由 Richard Durstenfeld 改编为适用于电脑编程的版本。

function shuffle(arr) { var i = arr.length, t, j; while (i) { j = Math.floor(Math.random() * i--); t = arr[i]; arr[i] = arr[j]; arr[j] = t; } } //对应的ES6如下
function shuffle(arr) { let i = arr.length; while (i) { let j = Math.floor(Math.random() * i--);  //5555
 [arr[j], arr[i]] = [arr[i], arr[j]]; } } 

小结

如果要将数组随机排序,千万不要再用(a, b) => Math.random() - 0.5这样的方法。目前而言,Fisher–Yates shuffle 算法应该是最好的选择。

转自:http://developer.51cto.com/art/201704/536457.htm

转载于:https://www.cnblogs.com/7qin/p/9710034.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/451173.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通用唯一识别码UUID

UUID是通用唯一识别码&#xff08;Universally Unique Identifier&#xff09;的缩写。UUID 的目的&#xff0c;是让分布式系统中的所有元素&#xff0c;都能有唯一的辨识资讯&#xff0c;而不需要透过中央控制端来做辨识资讯的指定。如此一来&#xff0c;每个人都可以建立不与…

java内省机制 + 内省是什么 + 内省实现方式 + 和反射的区别

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 一、内省是什么、实现方式&#xff1a; 内省&#xff08;Introspector&#xff09;是Java语言对Bean类属性、事件的一种缺省处理方法。…

百度联合长虹发布第二款云手机 售价900元以下

摘要&#xff1a;【搜狐IT消息】5月15日消息&#xff0c;百度今天宣布联合长虹发布第二款智能手机&#xff0c;采用3.5英寸屏幕、300万像素摄像头&#xff0c;650MHz主频处理器&#xff0c;零售价格在700-899元之间&#xff0c;中国联通将为其提供话费补贴。 【搜狐IT消息】5月…

vmware workstation17环境安装centos7

打开控制面板&#xff0c;搜索“服务”&#xff0c;启动vmware authorize service -------解决无法开启虚拟机问题之无法连接MKS 2.虚拟机硬盘扩展为15G------解决安装centos7时出现的“检查存储配置出错”问题 3.硬盘分区----/boot 300mb&#xff08;不能小于200mb&#xff0…

博客园中的源代码格式显示

昨天写了一篇文章&#xff0c;但是在写的时候呢&#xff0c;没有注意&#xff0c;直接将代码复制上去了&#xff0c;今天正好有人提醒&#xff0c;看到了格式的混乱&#xff0c;借此记录整理一下&#xff0c;如何能直接粘贴代码&#xff0c;而且格式&#xff08;缩进&#xff0…

static的使用

类中的静态变量在程序运行期间&#xff0c;其内存空间对所有该类的对象实例而言是共享的&#xff0c;为了节省系统内存开销、共享资源&#xff0c;应该对一些适合使用static的变量声明为静态变量。 变量声明为static的使用场景&#xff1a; &#xff08;1&#xff09;变量所…

Linux内核的裁剪和移植

linux内核的裁剪和移植具体都在这个网址里面。https://blog.csdn.net/xie0812/article/details/10816059https://blog.csdn.net/xie0812/article/details/10821779转载于:https://blog.51cto.com/13401435/2145947

李开复唱衰互联网手机:大部分公司会失败

摘要&#xff1a;互联网企业和手机制造企业之间巨大的鸿沟也被李开复鲜明地指出来&#xff1a;“两个产业差别巨大&#xff0c;企业基因不同。”百度此前也坦诚表示&#xff0c;与长虹合作的千元机&#xff0c;主要是针对2000元以下的用户体验&#xff0c;不能与四五千元的苹果…

【POJ】3268 Silver Cow Party

题目链接&#xff1a;http://poj.org/problem?id3268 题意 &#xff1a;有N头奶牛&#xff0c;M条单向路。X奶牛开party&#xff0c;其他奶牛要去它那里。每头奶牛去完X那里还要返回。去回都是走的最短路。现在问这里面哪头奶牛走的路最长。 题解&#xff1a;对每个奶牛i与X做…

java.util.ConcurrentModificationException异常分析

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 Java在操作ArrayList、HashMap、TreeMap等容器类时&#xff0c;遇到了java.util.ConcurrentModificationException异常。以ArrayList为例…

redis基本数据类型之String

redis基本数据类型之String redis一共分为5中基本数据类型&#xff1a;String,Hash,List,Set,ZSet String String类型是包含很多种类型的特殊类型&#xff0c;并且是二进制安全的。比如序列化的对象进行储存&#xff0c;比如一张图片进行二进制储存&#xff0c;比如一个简单…

Laravel5.5之事件监听、任务调度、队列

一、事件监听 流程&#xff1a; 1.1 创建event php artisan make:event UserLogin LoginController.php /*** The user has been authenticated.** param \Illuminate\Http\Request $request* param mixed $user* return mixed*/protected function authenticated(Request …

朱江洪功成身退 朱董配解体谁主格力(图)

摘要&#xff1a;中国家电营销委员会副理事长洪仕斌向时代周报记者表示&#xff1a;“朱江洪和董明珠已经完成了他们在格力发展前二十年的使命。“朱董配”解体之后&#xff0c;有人质疑格力“技术营销”的格局必将被打破&#xff0c;难以延续&#xff0c;“董氏班底”与朱江洪…

一些dos下简单命令

(1)切换盘符 d: 回车 (2)显示某目录下的所有文件或者文件夹(掌握) dir 回车 (3)创建文件夹 md 文件夹名称 回车 (4)删除文件夹 rd 文件夹名称 回车 (5)进入目录(掌握) 单级进入 cd 目录名称 多级进入 cd 目录名称1\目录名称2\... (6)回退目录(掌握) 单级回退 cd.. …

ssh服务器拒绝了密码 请再试一次 Xftp5连接失败

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 我的情况都很简单&#xff1a; 第一回主机 ip 不对&#xff0c; 第二次 是账号、密码都不对。 最后 IP、账号、密码都对了 就连上了。

后端DTO(数据传输对象)与DAO(数据库数据源对象)解耦的好处

我们在后端的开发中经常会将DO对象传到Service层直接作为DTO传给前端&#xff0c;这样做其实会有很多弊端。 &#xff08;一&#xff09;DO对象一般其成员域和数据库字段是对应的&#xff0c;所以不能添加额外的字段&#xff0c;但是有时候端就是需要这个字段。反之前端要向后…

【刷算法】字符串的全排列

题目描述 输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。 分析 没啥好分析的了&#xff0c;这个题不会&#xff0c;上网查的思路&#xff0c;大概就是&#xff1a; abc分化…

BZOJ.2741.[FOTILE模拟赛]L(分块 可持久化Trie)

题目链接 首先记\(sum\)为前缀异或和&#xff0c;那么区间\(s[l,r]sum[l-1]^{\wedge}sum[r]\)。即一个区间异或和可以转为求两个数的异或和。 那么对\([l,r]\)的询问即求\([l-1,r]\)中某两个数异或的最大值。 区间中某一个数和已知的一个数异或的最大值可以用可持久化Trie \(O(…

传腾讯人事大地震 马化腾将重整公司架构

摘要&#xff1a;5月17日消息&#xff0c;传腾讯董事长马化腾将重新组织公司架构&#xff0c;为腾讯大换血。据悉&#xff0c;腾讯之所以选择互动娱乐部门负责人接任这一重要岗位&#xff0c;也是因为互娱部门业绩持续快速发展&#xff0c;成为了“腾讯帝国”发展的核心驱动力之…

阿里云对象存储OSS与文件存储NAS的区别

一、简介 应用场景&#xff1a;选择一款存储产品&#xff0c;面向文档数据的存取&#xff0c;不会涉及到数据处理。 产品选型主要从OSS和NAS中选择一款&#xff0c;满足文档存储的需求。 二、NAS优缺点 NAS 是一种采用直接与网络介质相连的特殊设备实现数据存储的机制。由于这些…