昨天了解了一下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--); // [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

关于JavaScript的数组随机排序的更多相关文章

  1. 【JavaScript】数组随机排序 之 Fisher–Yates 洗牌算法

    Fisher–Yates随机置乱算法也被称做高纳德置乱算法,通俗说就是生成一个有限集合的随机排列.Fisher-Yates随机置乱算法是无偏的,所以每个排列都是等可能的,当前使用的Fisher-Yat ...

  2. 比较两种数组随机排序方法的效率 JavaScript版

    //比较2中数组随机排序方法的效率 JavaScript版 //randon1思路 //当len=5时候,从0-5中随机3一个放入i=0, // 从0-3随机一个2放入i=2 // 从0-2随机一个1 ...

  3. js 数组随机排序

    仅用于个人学习记录 javascript 数组随机排序1.最简洁的方法:function randomsort(a, b) {    return Math.random()>.5 ? -1 : ...

  4. JS数组随机排序

    var arr=[1,2,3,4,5]; arr.sort(function(a,b){ var v=Math.random()>0.5?1:-1; console.log(a,b,v); re ...

  5. java数组随机排序实现代码

    例一 代码如下 复制代码 import java.lang.Math;import java.util.Scanner;class AarrayReverse{ public static void ...

  6. Javascript中数组重排序方法详解

    在数组中有两个可以用来直接排序的方法,分别是reverse()和sort().下面通过本文给大家详细介绍,对js 数组重排序相关知识感兴趣的朋友一起看看吧. 1.数组中已存在两个可直接用来重排序的方法 ...

  7. JavaScript中数组的排序——sort()

    数组排序sort() sort()方法使数组中的元素按照一定的顺序排列. arrayObject.sort(方法函数) 1.如果不指定<方法函数>,则按unicode码顺序排列. 2.如果 ...

  8. JavaScript中数组的排序方法:1.冒泡排序 2.选择排序

      //1.选择排序: //从小到大排序:通过比较首先选出最小的数放在第一个位置上,然后在其余的数中选出次小数放在第二个位置上,依此类推,直到所有的数成为有序序列. var arr2=[19, 8, ...

  9. C# 数组 随机 排序

    ]; ; i < ; i++) { arrInt[i] = i; } arrInt = arrInt.OrderBy(c => Guid.NewGuid()).ToArray<int ...

随机推荐

  1. 基于pandas python的美团某商家的评论销售(数据分析)

    数据初步的分析 本文是该系列的第一篇 数据清洗 数据初步的统计 第二篇 数据可视化 第三篇 数据中的评论数据用于自然语言处理 from pyecharts import Bar,Pie import ...

  2. MatLab Mark Points 给点标序号

    在MatLab中,我们有时要给画出的点按一定顺序标上序号,下面这个简单的例子实现这个功能: x=rand(,); y=rand(,); plot(x,y,'r*'); text(x+:));

  3. TOP100summit 2017:小米唐沐等大咖精心挑选的100个年度研发案例实践

    2017年,机器学习.大数据.人工智能等词汇成为软件研发行业的主流,大前端.DevOps.区块链等技术方式成为热点方向:2017年,智能硬件开始成为新的焦点,这一年更被称为智能音箱井喷的一年:2017 ...

  4. 【转】(翻译)从底层了解ASP.NET体系结构

    原文地址:http://www.cnblogs.com/rijing2004/archive/2007/09/14/howaspnetwork.html 前言关于ASP.NET的底层的工作机制,最近园 ...

  5. HDU 1565 - 方格取数(1) - [状压DP][网络流 - 最大点权独立集和最小点权覆盖集]

    题目链接:https://cn.vjudge.net/problem/HDU-1565 Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 32 ...

  6. SQL Fundamentals: 表的创建和管理(表的基本操作,闪回技术flashback,表结构修改)

    SQL Fundamentals || Oracle SQL语言 1.表的基本操作 (CREATE TABLE, DROP TABLE,TRUNCATE TABLE, RENAME tablename ...

  7. supervisor control in centos 6/7 python2.6.2.7 3.4

    sudo yum install epel-releasesudo yum install python34 sudo pip install virtualenv yum -y install ep ...

  8. 经典的DOS小命令 for 网络 nbtstat

    --网络scanner · 1.最基本,最常用的,测试物理网络的 ping 192.168.10.59 -t ,参数-t是等待用户去中断测试 2.查看DNS(对猫用户),还是比较有用处的 A.Win9 ...

  9. 2018/03/09 每日一学PHP 之 require_once require include include_once 包含文件的区别

    require_once require include include_once 方法的区别 对于包含文件来说,如果只是使用框架来说的话,应该会很少碰到,因为框架底层对于文件的引用等做了很好的封装, ...

  10. LightOj 1248 - Dice (III)(几何分布+期望)

    题目链接:http://lightoj.com/volume_showproblem.php?problem=1248 题意:有一个 n 面的骰子,问至少看到所有的面一次的所需 掷骰子 的 次数的期望 ...