javascript如何将一个无序的数组变成一个有序的数组,咱们有不少的排序算法能够实现,例如冒泡算法,快速排序,插入排序等。然而咱们是否想过,如何将一个有序的数组乱序输出呢?本文将从经典的洗牌算法出发,详细讲解underscore中关于乱序排列的实现。javascript
在介绍洗牌算法的概念前,咱们先引入现实生活的一个经典例子。当咱们和多人一块儿玩扑克牌的时候,咱们须要先将一份全新的扑克牌打乱,让牌组随机化,以确保游戏的公平性。这个将牌组随机化的过程,咱们衍生到代码中能够归纳为: 一个有序的数组[1,2,3,4,5],如何随机打乱,使生成一个随机的数组,如[3,2,5,1,4],且须要保证数组中的数出如今每一个位置的几率相同。如何实现呢?java
利用es5的sort函数,其实咱们很容易找到一种简洁的方法。es6
var shuffle = (arr) => arr.sort(() => Math.random() - 0.5)
复制代码
本质上利用es6原生的sort函数,咱们能够达到数组乱序,然而sort方法自己却没法保证元素出如今每一个位置上出现的几率随机。在关于 JavaScript 的数组随机排序一文中,做者详细讲解了使用sort排序并不能保证随机,而且列举了形成没法随机化的缘由。文章较长,大概能够总结为如下两点。算法
其实,为了实现这一场景,前人已经给出了问题的答案,也就是公认成熟的洗牌算法(Fisher-Yates),简单的思路以下:chrome
因为第二步生成随机位置的随机性,因此整个洗牌算法保证了乱序的随机性。将文字转化为代码,javascript的简单实现以下:数组
function shuffle(arr) {
var length = arr.length,
j = length;
for (var i = 0; i < length; i++) {
var random = Math.floor(Math.random() * (j--)); // 生成起始位置到基准位置之间的随机位置,并将基准从结束位置不停左移。
// es3实现
var newA = arr[i];
arr[i] = arr[random];
arr[random] = newA
// es6 实现
[arr[i], arr[random]] = [arr[random], arr[i]]; // 本质为交换元素位置。
}
return arr
}
复制代码
underscore中在乱序方面一样用到了洗牌算法,有了洗牌算法的概念和实现基础后,接下来咱们将关注点放在underscore中乱序方法的实现中。浏览器
洗牌算法的第二步,生成随机数的过程,无疑须要使用到原生Math.random()
(产生一个[0,1)之间的随机数),而underscore在原生的Math.random()
方法上,从新包装了random函数,实如今给定范围内生成随机整数,若是只传递一个参数,那么将返回0和这个参数之间的整数。bash
// 随机函数
_.random = function (min, max) {
if (max == null) { // max == null即表明只传递一个参数,此时最大值为传递的最小值,最小值为0
max = min;
min = 0;
}
return min + Math.floor(Math.random() * (max - min + 1)); // 生成[min, max]以前的随机数
};
复制代码
在underscore 1.8版本之前,洗牌算法的实现直接放在了 _.shuffle 方法中实现,而1.9的版本直接抛弃了这种写法,咱们将放在下文分析这种写法。在1.9版本中,洗牌算法的实现放在了sample函数中,sample会在 list中产生一个随机样本。n则表明返回多少个随机数。同时list不只能够是数组,也能够是对象或者类数组,当list为对象时,随机返回的是对象的值,如框架
console.log(_.sample({a: 123, b: 234}, 2)) // 234 123
复制代码
实现思路以下dom
_.sample = function(list, n) {
if(n == null) { // 不指定个数时,默认在数组中随机取出一个。
if (!isArrayLike(obj)) list = _.values(list); // list为对象时,取出对象值的集合。
return list[_.random(list.length - 1)] // 生成数组的随机位置,返回该位置的值。
}
var sample = isArrayLike(obj) ? _.clone(obj) : _.values(obj); // 数组类数组获得克隆的数组,对象获得值的集合。
var length = getLength(sample);
n = Math.max(Math.min(n, length), 0); // 对n的大小作限制,不能大于新数组的长度,不能小于0。
var last = length - 1;
for (var index = 0; index < n; index++) { // for循环下的操做为洗牌算法的核心步骤,和前面讲解的实现方式相同。
var rand = _.random(index, last);
var temp = sample[index];
sample[index] = sample[rand];
sample[rand] = temp;
}
return sample.slice(0, n);
}
复制代码
shuffle是洗牌算法的用法函数,返回一个随机乱序的list副本。有了_.sample的基础,shuffle只须要传递一个n值为数组长度的参数给sample函数便可。
_.shuffle = function(list) {
return _.sample(obj, Infinity); // n值传递无穷大,因为sample函数内部对n值的限制,真正执行洗牌算法时,n的值为数组的长度。
}
复制代码
前面提到,1.8版本之前underscore对于洗牌算法的实现放在了shuffle函数中,它的实现相比于1.9版原本说,有不少的巧妙之处,咱们来看看实现代码。
_.shuffle = function(obj) {
var set = obj && obj.length === +obj.length ? obj : _.values(obj);
var length = set.length;
var shuffled = Array(length);
for (var index = 0, rand; index < length; index++) {
rand = _.random(0, index);
if (rand !== index) shuffled[index] = shuffled[rand];
shuffled[rand] = set[index];
}
return shuffled;
};
复制代码
// 交换位置代码
if (rand !== index) shuffled[index] = shuffled[rand];
shuffled[rand] = set[index];
复制代码
中间关系乱序的实现看起来很巧妙,咱们来详细分析每一步是如何进行的。
[1,2,3,4]
shuffle = [undefined, undefined, undefined, undefined]
shuffled[0] = set[0] = 1
shuffle依然为shuffle = [1, undefined, undefined, undefined]
shuffled[1] = shuffled[0] = 1 ; shuffled[0] = set[1] = 2;
即 shuffle改变成shuffle = [2, 1, undefined, undefined]
shuffled[2] = shuffled[1] = 1 ; shuffled[1] = set[2] = 3;
即 shuffle改变成shuffle = [2, 3, 1, undefined]
shuffled[3] = shuffled[0] = 3 ; shuffled[0] = set[3] = 4;
即 shuffle改变成shuffle = [4, 3, 1, 2]
通过以上十步,乱序数组也随之生成