随机打乱数组及Fisher–Yates shuffle算法详解

季将
• 阅读 5567

介绍几种随机打乱数组的方法,及其利弊。

一、Array.prototype.sort 排序

注意一下,sort() 方法会改变原数组,看代码:

// ES6 写法
function randomShuffle(arr) {
  return arr.sort(() => Math.random() - 0.5)
}

// ES5 写法
function randomShuffle(arr) {
  var compareFn = function () {
    return Math.random() - 0.5
  }
  return arr.sort(compareFn)
}
但实际上这种方法并不能真正的随机打乱数组。在多次执行后,每个元素有很大几率还在它原来的位置附近出现。可看下这篇文章:常用的 sort 打乱数组方法真的有用?

二、Fisher–Yates shuffle 经典洗牌算法

这种算法思想,目前有两种稍有不同的实现方式,这里我把它们都算入 Fisher–Yates shuffle。分别是 Fisher–Yates shuffleKnuth-Durstenfeld Shuffle

著名的 Lodash 库的方法 _.shuffle() 也是使用了该算法。

1. Fisher–Yates shuffle(Fisher and Yates' original method)

由 Ronald Fisher 和 Frank Yates 提出的 Fisher–Yates shuffle 算法思想,通俗来说是这样的:

假设有一个长度为 N 的数组

  1. 从第 1 个到剩余的未删除项(包含)之间选择一个随机数 k。
  2. 从剩余的元素中将第 k 个元素删除并取出,放到新数组中。
  3. 重复第 1、2 步直到所有元素都被删除。
  4. 最终将新数组返回

实现

function shuffle(arr) {
  var random
  var newArr = []

  while (arr.length) {
    random = Math.floor(Math.random() * arr.length)
    newArr.push(arr[random])
    arr.splice(random, 1)
  }

  return newArr
}

举例

假设我们有 1 ~ 8 的数字

表格每列分别表示:范围、随机数(被移除数的位置)、剩余未删除的数、已随机排列的数。
RangeRollScratchResult
 1 2 3 4 5 6 7 8 

现在,我们从 1 ~ 8 中随机选择一个数,得到随机数 k 为 3,然后在 Scratch 上删除第 k 个数字(即数字 3),并将其放到 Result 中:

RangeRollScratchResult
1 - 8 31 2 3 4 5 6 7 83 

现在我们从 1 ~ 7 选择第二个随机数 k 为 4,然后在 Scratch 上删除第 k 个数字(即数字 5),并将其放到 Result 中:

RangeRollScratchResult
1 - 7 41 2 3 4 5 6 7 83 5 

现在我们从 1 ~ 6 选择下一个随机数,然后从 1 ~ 5 选择依此类推,总是重复上述过程:

RangeRollScratchResult
1–651 2 3 4 5 6 7 83 5 7
1–531 2 3 4 5 6 7 83 5 7 4
1–441 2 3 4 5 6 7 83 5 7 4 8
1–311 2 3 4 5 6 7 83 5 7 4 8 1
1–221 2 3 4 5 6 7 83 5 7 4 8 1 6
  1 2 3 4 5 6 7 83 5 7 4 8 1 6 2
2. Knuth-Durstenfeld Shuffle(The modern algorithm)

Richard Durstenfeld 于 1964 年推出了现代版本的 Fisher–Yates shuffle,并由 Donald E. Knuth 在 The Art of Computer Programming 以 “Algorithm P (Shuffling)” 进行了推广。Durstenfeld 所描述的算法与 Fisher 和 Yates 所给出的算法有很小的差异,但意义重大。

-- To shuffle an array a of n elements (indices 0..n-1):  
for i from n−1 downto 1 do  // 数组从 n-1 到 0 循环执行 n 次
  j ← random integer such that 0 ≤ j ≤ i  // 生成一个 0 到 n-1 之间的随机索引
  exchange a[j] and a[i] // 将交换之后剩余的序列中最后一个元素与随机选取的元素交换

Durstenfeld 的解决方案是将“删除”的数字移至数组末尾,即将每个被删除数字与最后一个未删除的数字进行交换

实现

// 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]]
  }

  return arr
}


// ES5 写法
function shuffle(arr) {
  var i = arr.length
  var j
  var t

  while (--i) {
    j = Math.floor(Math.random() * i)
    t = arr[i]
    arr[i] = arr[j]
    arr[j] = t
  }
  
  return arr
}

Knuth-Durstenfeld Shuffle 将算法的时间复杂度降低到 O(n),而 Fisher–Yates shuffle 的时间复杂度为 O(n2)。后者在计算机实现过程中,将花费不必要的时间来计算每次剩余的数字(可以理解成数组长度)。

举例

同样,假设我们有 1 ~ 8 的数字

表格每列分别表示:范围、当前随机数(即随机交互的位置)、剩余未交换的数、已随机排列的数。
RangeRollScratchResult
 1 2 3 4 5 6 7 8 

我们从 1 ~ 8 中随机选择一个数,得到随机数 k 为 6,然后交换 Scratch 中的第 6 和第 8 个数字:

RangeRollScratchResult
1 - 8 61 2 3 4 5 8 76 

接着,从 1 ~ 7 中随机选择一个数,得到随机数 k 为 2,然后交换 Scratch 中的第 2 和第 7 个数字:

RangeRollScratchResult
1 - 7 61 7 3 4 5 82

继续,下一个随机数是1 ~ 6,得到的随机数恰好是 6,这意味着我们将列表中的第 6 个数字保留下来(经过上面的交换,现在是 8),然后移到下一个步。同样,我们以相同的方式进行操作,直到完成排列:

RangeRollScratchResult
1 - 6 61 7 3 4 58 2 6 
1 - 5 15 7 3 41 8 2 6 
1 - 4 35 7 43 1 8 2 6 
1 - 3 35 74 3 1 8 2 6 
1 - 2 175 4 3 1 8 2 6 

因此,结果是 7 5 4 3 1 8 2 6

三、总结

若要实现随机打乱数组的需求,不要再使用 arr.sort(() => Math.random() - 0.5) 这种方法了。目前用得较多的是 Knuth-Durstenfeld Shuffle 算法。

四、参考

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Stella981 Stella981
3年前
Python 如何随机打乱列表(List)排序
场景:现在有一个list:1,2,3,4,5,6,我需要把这个list在输出的时候,是以一种随机打乱的形式输出。专业点的术语:将一个容器中的数据每次随机逐个遍历一遍。注意:不是生成一个随机的list集。环境:Python3.6解决方案:方案一:有人可能会通过Random内
Stella981 Stella981
3年前
JS 对象数组Array 根据对象object key的值排序sort,很风骚哦
有个js对象数组varary\{id:1,name:"b"},{id:2,name:"b"}\需求是根据name或者id的值来排序,这里有个风骚的函数函数定义:function keysrt(key,desc) {  return function(a,b){    return desc ? ~~(ak
Wesley13 Wesley13
3年前
ES6 新增的数组的方法
给定一个数组letlist\//wu:武力zhi:智力{id:1,name:'张飞',wu:97,zhi:10},{id:2,name:'诸葛亮',wu:55,zhi:99},{id:3,name:'赵云',wu:97,zhi:66},{id:4,na
达里尔 达里尔
1年前
给数组添加新数据,判断数据是否重复
多选要进行数组拼接,希望判断往原数组里添的新数据是否重复,封装个简易方法languageconstdataArrayname:'aaa',id:1,name:'bbb',id:2;constnewDataname:'ccc',id:2;//要添加的新数
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
分布式系统的主键生成方案对比 | 京东云技术团队
UUID​UUID(通用唯一识别码)是由32个十六进制数组成的无序字符串,通过一定的算法计算出来。为了保证其唯一性,UUID规范定义了包括网卡MAC地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,以及从这些元素生成UUID的算法。