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]
通過以上十步,亂序數組也隨之生成