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

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

一、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 的数字

表格每列分别表示:范围、随机数(被移除数的位置)、剩余未删除的数、已随机排列的数。

Range Roll Scratch Result
1 2 3 4 5 6 7 8

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

Range Roll Scratch Result
1 - 8 3 1 2 3 4 5 6 7 8 3

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

Range Roll Scratch Result
1 - 7 4 1 2 3 4 5 6 7 8 3 5

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

Range Roll Scratch Result
1–6 5 1 2 3 4 5 6 7 8 3 5 7
1–5 3 1 2 3 4 5 6 7 8 3 5 7 4
1–4 4 1 2 3 4 5 6 7 8 3 5 7 4 8
1–3 1 1 2 3 4 5 6 7 8 3 5 7 4 8 1
1–2 2 1 2 3 4 5 6 7 8 3 5 7 4 8 1 6
1 2 3 4 5 6 7 8 3 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 的数字

表格每列分别表示:范围、当前随机数(即随机交互的位置)、剩余未交换的数、已随机排列的数。

Range Roll Scratch Result
1 2 3 4 5 6 7 8

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

Range Roll Scratch Result
1 - 8 6 1 2 3 4 5 8 7 6

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

Range Roll Scratch Result
1 - 7 6 1 7 3 4 5 8 2 6

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

Range Roll Scratch Result
1 - 6 6 1 7 3 4 5 8 2 6
1 - 5 1 5 7 3 4 1 8 2 6
1 - 4 3 5 7 4 3 1 8 2 6
1 - 3 3 5 7 4 3 1 8 2 6
1 - 2 1 7 5 4 3 1 8 2 6

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

三、总结

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

四、参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容