洗牌算法具体指的是什么

(1)背景介绍

为了让游戏更加公平,我们希望洗牌时得到一个随机的排序,一个简单有效的办法就是把牌随
机选一叠放另外一边做一个新的牌堆,并且重复这一步。只要从剩余牌堆中选出来的牌的概率是相等的,就会得到公平的牌堆。
相似的,对于数组【0,n】,如何对它随机排列元素呢

(2)知识剖析:

JavaScript 开发中有时会遇到要将一个数组随机排序(shuffle)的需求,一个常见的写法是这样:

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

或者使用更简洁的 ES6 的写法(箭头函数创建更简短的函数和不引入this):

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

(3)常见问题:

但是这种写法有问题,并不能实现真正的随机这样排序的问题
看下面的代码,我们生成一个长度为 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);

如果排序真的是随机的,那么每个元素在每个位置出现的概率都应该一样,但是这种排序各个位置的字母会在原位置的左右徘徊。

浏览器打印台:(10) [2892, 2939, 1968, 1040, 582, 288, 161, 66, 44, 20]

因此,我们可以认为,使用形如arr.sort(() => Math.random() - 0.5)这样的方法得到的并不是真正的随机排序。

国外有人写了一个shuffle算法可视化页面 , 在上面可以更直观地看到使用arr.sort(() => Math.random() - 0.5)的确是很不随机的。

(4)解决方案:

Fisher–Yates shuffle算法

这个算法由 Ronald Fisher 和 Frank Yates 于 1938 年提出,然后在 1964 年由 Richard Durstenfeld 改编为适用于电脑编程的版本。

(5)编码实战:

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

ES5实现:

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;
}
}

比较清晰的一种写法

Array.prototype.shuffle = function () {
var input = this;
for (var i = input.length - 1; i >= 0; i--) {
var randomIndex = Math.floor(Math.random() * (i + 1));
var itemAtIndex = input[randomIndex];
input[randomIndex] = input[i];
input[i] = itemAtIndex;
}
return input;
}
var tempArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ;
tempArray.shuffle(); // and the result is... alert(tempArray);

prototype 属性是在函数作为构造器使用的时候,作为其构造对象的原型

this 引用的就是调用该 shuffle 的数组:

for循环遍历数组,顺序是从后往前,从input.length-1位置开始,直到第一个元素

var randomIndex = Math.floor(Math.random() * (i + 1));存储一个随机数,用作数组的索引,来提取一个随机元素,最大值是i的值

var itemAtIndex = input[randomIndex];用新变量保存该随机元素值,比如随机生产的索引2,那么该随机值就是3

最后两行是把选中的元素和随机的元素交换一下

(6)拓展思考:

除了洗牌算法,类似的,还有哪些对数组的排序方式?
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,
也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
比较相邻的元素。如果第一个比第二个大,就交换它们两个;
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;针对所有的元素重复以上的步骤,除了最后一个;重复步骤1~3,直到排序完成。

(7)参考文献:

https://blog.oldj.net/2017/01/23/shuffle-an-array-in-javascript

https://bost.ocks.org/mike/shuffle/compare.html

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions

(8)更多讨论:

提问1

除了Fisher–Yates shuffle算法还有哪些随机算法?

回答:比如上面提到的arr.sort(() => Math.random() - 0.5)算法就是一个常用的方法。尽管得到的并不是完全的随机

提问2:洗牌算法的实际运用?

回答:比如听歌软件的随便听听功能

提问3:fisher算法比sort算法究竟好在哪里

回答:

再拿抽牌举例,因为fisher利用了抽卡本身的顺序,保证到了每一张原本序列中的卡,而sort方法抽取存在出现重复位置的可能性,就等于浪费了一次排序的机会,换句话说,其等效抽卡次数因为出现了过去相同的洗法,有效洗牌次数下降,样本空间缩小,无法充满整个n!空间,所以有效性会下降。而Fisher–Yates算法在原理上保证了不会出现浪费次数,重复选择的情况,导致样本空间一直保持n!,没有坍缩,这就是其在数学意义上优秀的原因。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值