原理 :

Fisher–Yates shuffle 洗牌算法是什么,为什么满足需求?

这里,我们简单借助图形来理解,非常简单直观。你接下来就会明白为什么这是理论上的完全乱序(图片来源于网络)。

首先我们有一个已经排好序的数组:

Step1
第一步需要做的就是,从数组末尾开始,选取最后一个元素。

在数组一共 9 个位置中,随机产生一个位置,该位置元素与最后一个元素进行交换。

Step2:
上一步中,我们已经把数组末尾元素进行随机置换。
接下来,对数组倒数第二个元素动手。在除去已经排好的最后一个元素位置以外的8个位置中,随机产生一个位置,该位置元素与倒数第二个元素进行交换。

Step3:
理解了前两部,接下来就是依次进行,如此简单。

作者:Lucas HC
链接:https://www.zhihu.com/question/68330851/answer/266506621
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


代码实现:

从前往后,可能保持原位

//因为可能和自己交换,相当于没交换,留在原位,同时其他项在选择时也都错开了该项,那么该项则有可能最后保持原位

//尤其是第一项,如果选择了和自己交换,那么后续其他项是不可能选择到和它交换,则第一项最后保持原位

//极端情况,每项都和自己交换,最后数组未改变

let shuffle = () =>{const arr = [0, 1, 2, 3, 4];const length = arr.length;const lastIndex = length - 1;//i小于lastIndex,即从0开始到倒二个数,因为是跟自己或者剩余的数组项换,而最后一个元素后面没有其他数组项可以交换,跟自己交换又没有意义for (let i = 0; i < lastIndex; i++) {//random的取值区间为[i,lastIndex]//Math.floor(Math.random() * (m- n + 1)) + n;//Math.random() * (m- n + 1)的取值为[0,m-n+1),再加n就是[n,m+1),向下取整就是[n,m],套取公式就获得下面的式子const random = Math.floor(Math.random() * (lastIndex - i + 1)) + i;console.log(i, random);let temp = arr[random];arr[random] = arr[i];arr[i] = temp;//[arr[random], arr[i]]=[arr[i], arr[random]] //交换2个数组项的变量解构赋值写法 https://es6.ruanyifeng.com/#docs/destructuring#%E7%94%A8%E9%80%94console.log(arr);}
};

常规情况:

第一项保持原位:

const Test = func => {// 以下均测试10000000次打乱,记录最后的结果于一个二维数组count// count[i][j]即表示数字i在j位置出现的次数const total = 10000000;const count = new Array(5).fill(0).map(() => new Array(5).fill(0));for (let i = 0; i < total; i++) {func().forEach((n, i) => count[n][i]++);}// 应题主要求输出改为百分比,四舍五入保留2位小数console.table(count.map(n => n.map(n => (n / total * 100).toFixed(2) + '%')));
};作者:troy351
链接:https://www.zhihu.com/question/68330851/answer/262111061
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

从前往后,不存在保持原位(变体)

//非完全乱序

//random的下限改为i+1

//因为是跟剩余的数组项换,后面的项被交换到前面,就没有机会再被交换回来

let shuffle = () =>{const arr = [0, 1, 2, 3, 4];const length = arr.length;const lastIndex = length - 1;//i小于lastIndex,即从0开始到倒二个数,因为是跟剩余的数组项换,而最后一个元素后面没有其他数组项可以交换for (let i = 0; i < lastIndex; i++) {//random的取值区间为[i+1,lastIndex]//Math.floor(Math.random() * (m- n + 1)) + n;//Math.random() * (m- n + 1)的取值为[0,m-n+1),再加n就是[n,m+1),向下取整就是[n,m],套取公式就获得下面的式子const random = Math.floor(Math.random() * (lastIndex - (i + 1) + 1)) + (i + 1);console.log(i, random);let temp = arr[random];arr[random] = arr[i];arr[i] = temp;//[arr[random], arr[i]]=[arr[i], arr[random]] //交换2个数组项的变量解构赋值写法 https://es6.ruanyifeng.com/#docs/destructuring#%E7%94%A8%E9%80%94console.log(arr);}
};

从后往前,可能保持原位

//与上述从前往后可能保持原位的原理一致

let shuffle = () =>{const arr = [0, 1, 2, 3, 4];let i = arr.length;//可以改为while(i>1),因为i=1时是轮到第一项,random一定会为0,自己跟自己交换没有意义while (i) {let random = Math.floor(Math.random() * i--);//相当于//let random = Math.floor(Math.random() * i);//i--;console.log(i, random);let temp = arr[i];arr[i] = arr[random];arr[random] = temp;//[arr[random], arr[i]]=[arr[i], arr[random]] //交换2个数组项的变量解构赋值写法 https://es6.ruanyifeng.com/#docs/destructuring#%E7%94%A8%E9%80%9console.log(arr)}
}

从后往前,不存在保持原位(变体)

//非完全乱序

//与上述从前往后不存在保持原位的原理一致

let shuffle = () =>{const arr = [0, 1, 2, 3, 4];let i = arr.length;//可以改为while(i>1),因为i=1时是轮到第一项,random一定会为0,自己跟自己交换没有意义while (i) {let random = Math.floor(Math.random() * (i - 1));i--;console.log(i, random);let temp = arr[i];arr[i] = arr[random];arr[random] = temp;//[arr[random], arr[i]] = [arr[i], arr[random]] //交换2个数组项的变量解构赋值写法 https://es6.ruanyifeng.com/#docs/destructuring#%E7%94%A8%E9%80%9console.log(arr);}
}


扩展:

const arr = [0, 1, 2, 3, 4];
for (let i = 1; i < arr.length; i++) {const random = Math.floor(Math.random() * (i + 1));[arr[i], arr[random]] = [arr[random], arr[i]];
}作者:troy351
链接:https://www.zhihu.com/question/68330851/answer/262111061
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

思路就是开头那张图的思路,但却是从前往后的顺序。上述的实现都是从哪边开始,则第一轮“洗牌”后,那边第一位的“牌”就已经确定下来了,就像是单独拿出来了,而这种实现则是“洗”完了下一轮继续参与“洗牌”。

我的理解是,从可能性上来看,该实例的可能性数也是5的阶乘,就是有点独特。

sort

同时,很多答案提到了:

[12,4,16,3].sort(function() {return .5 - Math.random();
});

这样使用 sort 的方法。某些场景下,这样的方法可以使用。但是这不是真正意义上的完全乱序,一些需求中(比如抽奖)这样的写法会出大问题。

为什么借助 sort 方法不是真正意义上的完全乱序?

先证明不完全性。为此实现一个脚本,我对

var letters = ['A','B','C','D','E','F','G','H','I','J'];

letters 这样一个数组使用 array.sort 方法进行了 10000 次乱序处理,并把乱序的每一次结果可视化输出。每个元素(ABCD...)出现的位置次数进行记录:

具体脚本实现:HOUCe/shuffle-array

不管点击按钮几次,你都会发现整体乱序之后的结果绝对不是“完全随机”。

比如 A 元素大概率出现在数组的头部,J 元素大概率出现在数组的尾部,所有元素大概率停留在自己初始位置。

究其原因,在Chrome v8引擎源码中,可以清晰看到,

v8 在处理 sort 方法时,使用了插入排序和快排两种方案。当目标数组长度小于10时,使用插入排序;反之,使用快排。

其实不管用什么排序方法,大多数排序算法的时间复杂度介于 O(n) 到 O(n2) 之间,元素之间的比较次数通常情况下要远小于 n(n-1)/2,也就意味着有一些元素之间根本就没机会相比较(也就没有了随机交换的可能),这些 sort 随机排序的算法自然也不能真正随机。

通俗的说,其实我们使用 array.sort 进行乱序,理想的方案或者说纯乱序的方案是:数组中每两个元素都要进行比较,这个比较有 50% 的交换位置概率。如此一来,总共比较次数一定为 n(n-1)。

而在 sort 排序算法中,大多数情况都不会满足这样的条件。因而当然不是完全随机的结果了。

lodash 库 _.shuffle

这也是正解,事实上翻开 lodash 源码相关部分,这个方法正是采用了 Fisher–Yates shuffle 洗牌算法。感兴趣的同学可以进行参阅。

作者:Lucas HC
链接:https://www.zhihu.com/question/68330851/answer/266506621
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

JS 数组打乱 Fisher–Yates shuffle(费舍尔-耶茨 洗牌)相关推荐

  1. js打乱数组内元素顺序(Fisher–Yates shuffle洗牌算法)

    如何将数组内元素顺序打乱呢?这里小shy向大家介绍一种算法. Fisher–Yates shuffle:洗牌算法. 通俗理解: 先将数组最后一位元素作为参考点,将这个参考点和数组其他位置的元素(使用随 ...

  2. 洗牌算法(Fisher–Yates Shuffle and Knuth-Durstenfeld Shuffle)

    一.Fisher–Yates Shuffle 1.算法思想: 从原始数组中随机抽取一个新的数字到新数组. 2.算法描述: 初始化原始数组和新数组,原始数组长度为n(已知): 针对未处理的原始数组元素( ...

  3. Fisher–Yates shuffle 算法

    简单来说 Fisher–Yates shuffle 算法是一个用来将一个有限集合生成一个随机排列的算法(数组随机排序).这个算法生成的随机排列是等概率的.同时这个算法非常高效. Fisher–Yate ...

  4. leetcode 519. Random Flip Matrix | 519. 随机翻转矩阵(洗牌算法Fisher–Yates shuffle)

    题目 https://leetcode.com/problems/random-flip-matrix/ 题解 看了答案: 洗牌算法 Fisher–Yates shuffle Fisher–Yates ...

  5. LeetCode Shuffle an Array(Fisher-Yates洗牌算法)

    问题:打乱一个没有重复元素的数组 思路:Fisher-Yates洗牌.首先从1到n中选取一个数,删除,然后从1到n-1中选取一个数删除,直至剩下一个元素 代码具体参考: https://github. ...

  6. shuffle洗牌算法java_洗牌算法shuffle

    洗牌算法 1.   背景 阿里的面试的时候做的一道笔试题:题目:写一个方法,入参为自然数n  (n > 0),返回一个自然数数组,数组长度为n,元素为[1,n]之间,且每个元素不重复,数组中各元 ...

  7. js调整数组某些元素到指定位置顺序_如何将一个 JavaScript 数组打乱顺序?

    1)首先,毫无疑问: @顾轶灵 轶灵大佬给出的Fisher–Yates shuffle 洗牌算法是最完美乱序的算法/方法之一了,正解无疑. 2)同时,很多答案提到了: [12,4,16,3].sort ...

  8. 如何将一个JavaScript数组打乱顺序?

    由抽牌.换牌和插牌衍生出三种洗牌算法,其中分别对应Fisher-Yates Shuffle.Knuth-Durstenfeld Shhuffle.Inside-Out Algorithm算法. 今天介 ...

  9. 从洗牌算法谈起--Python的random.shuffle函数实现原理

    此文首发于我的个人博客:从洗牌算法谈起–random.shuffle实现原理 - zhang0peter的个人博客 昨天看知乎的时候看到了洗牌算法(Knuth shuffle, 最初版本叫Fisher ...

最新文章

  1. C# Task的简单使用
  2. linux ce mysql安装_Linux 安装 MySQL 8.0
  3. mysql元数据死锁日志,MySQL 实战笔记 第02期:MySQL 元数据锁
  4. 疯子的算法总结(一) 位运算(快速幂、快速乘)
  5. Photoshop脚本 使用ExtendScript编写Ps脚本
  6. “*** IS NOT TRANSLATED IN …….. 解决办法
  7. C#在ASP.NET4.5框架下的首次网页应用
  8. win8无线网络受限怎么办 win8网络受限的解决方法
  9. pip 通过pqi切换源到国内镜像
  10. 图像算法十:轮廓匹配match_contours() 得到精确的旋转角度
  11. Linux 命令(67)—— time 命令
  12. linux安装gcc-c++
  13. SPC X-R控制图的操作步骤
  14. 利用云效度量功能进行质量运营和效率驱动提升
  15. linux给变量加单引号,grep中加单引号与不加引号的区别
  16. 微信公众平台账号迁移公证书如何办理?GDP30强城市收据全新出炉
  17. 不同进制之间相互转换
  18. Hibernate 多对多的增删改查。
  19. 拂去ThreadLocal的轻纱
  20. Pytorch学习之cuda

热门文章

  1. sqlite如何创建数据库
  2. python画图颜色填充_Python使用Turtle图形函数画图 颜色填充!(学习笔记)
  3. 磁盘阵列两块硬盘掉线数据恢复成功案例
  4. size(),length和length()的区别(最详细版)
  5. 一个始乱没有终弃故事——leo看职场小说《做单》
  6. 深入理解 Linux 内核---访问文件
  7. 九度1465:最简真分数
  8. MySQL 基于MyCAT配置数据分片
  9. html做一个简单的网易邮箱注册
  10. Microsoft Visual Studio C++2022 Windows 11 SDK环境