也談前端面試常見問題之「數組亂序」

前言html

終於能夠開始 Collection Functions 部分了。前端

可能有的童鞋是第一次看樓主的系列文章,這裏再作下簡單的介紹。樓主在閱讀 underscore.js 源碼的時候,學到了不少,同時以爲有些知識點能夠獨立出來,寫成文章與你們分享,而本文正是其中之一(完整的系列請猛戳 https://github.com/hanzichi/underscore-analysis)。以前樓主已經和你們分享了 Object 和 Array 的擴展方法中一些有意思的知識點,今天開始解讀 Collection 部分。html5

看完 Collection Functions 部分的源碼,首先火燒眉毛想跟你們分享的正是本文主題 —— 數組亂序。這是一道經典的前端面試題,給你一個數組,將其打亂,返回新的數組,即爲數組亂序,也稱爲洗牌問題。git

一個好的方案須要具有兩個條件,一是正確性,毋庸置疑,這是必須的,二是高效性,在確保正確的前提下,如何將複雜度降到最小,是咱們須要思考的。github

splice面試

幾年前樓主還真碰到過洗牌問題,還真的是 「洗牌」。當時是用 cocos2d-js(那時還叫 cocos2d-html5)作牌類遊戲,發牌前毫無疑問須要洗牌。算法

當時我是這樣作的。每次 random 一個下標,看看這個元素有沒有被選過,若是被選過了,繼續 random,若是沒有,將其標記,而後存入返回數組,直到全部元素都被標記了。後來經同事指導,每次選中後,能夠直接從數組中刪除,無需標記了,因而獲得下面的代碼。數組

function shuffle(a) {瀏覽器

  var b = [];dom

 

  while (a.length) {

    var index = ~~(Math.random() * a.length);

    b.push(a[index]);

    a.splice(index, 1);

  }

 

  return b;

}

這個解法的正確性應該是沒有問題的(有興趣的能夠本身去證實下)。咱們假設數組的元素爲 0 – 10,對其亂序 N 次,那麼每一個位置上的結果加起來的平均值理論上應該接近 (0 + 10) / 2 = 5,且 N 越大,越接近 5。爲了能有個直觀的視覺感覺,咱們假設亂序 1w 次,而且將結果作成了圖表,猛戳 http://hanzichi.github.io/test-case/shuffle/splice/ 查看,結果仍是很樂觀的。

驗證了正確性,還要關心一下它的複雜度。因爲程序中用了 splice,若是把 splice 的複雜度當作是 O(n),那麼整個程序的複雜度是 O(n^2)。

Math.random()  (此方法有問題)

另外一個爲人津津樂道的方法是 「巧妙應用」 JavaScript 中的 Math.random() 函數。

function shuffle(a) {

  return a.concat().sort(function(a, b) {

    return Math.random() - 0.5;

  });

}

一樣是 [0, 1, 2 … 10] 做爲初始值,一樣跑了 1w 組 case,結果請猛戳 http://hanzichi.github.io/test-case/shuffle/Math.random/。

看平均值的圖表,很明顯能夠看到曲線浮動,並且屢次刷新,折現的大體走向一致,平均值更是在 5 上下 0.4 的區間浮動。若是咱們將 [0, 1, 2 .. 9] 做爲初始數組,能夠看到更加明顯不符預期的結果(有興趣的能夠本身去試下)。究其緣由,要追究 JavaScript 引擎對於 Math.random() 的實現原理,這裏就不展開了(實際上是我也不知道)。由於 ECMAScript 並無規定 JavaScript 引擎對於 Math.random() 應該實現的方式,因此我猜測不一樣瀏覽器通過這樣的亂序後,結果也不同。

何時能夠用這種方法亂序呢?」非正式」 場合,一些手寫 DEMO 須要亂序的場合,這不失爲一種 clever solution。

可是這種解法不但不正確,並且 sort 的複雜度,平均下來應該是 O(nlogn),跟咱們接下來要說的正解仍是有很多差距的。

Fisher–Yates Shuffle

關於數組亂序,正確的解法應該是 Fisher–Yates Shuffle,複雜度 O(n)。

其實它的思想很是的簡單,遍歷數組元素,將其與以前的任意元素交換。由於遍歷有從前向後和從後往前兩種方式,因此該算法大體也有兩個版本的實現。

從後往前的版本:

function shuffle(array) {

  var _array = array.concat();

 

  for (var i = _array.length; i--; ) {

    var j = Math.floor(Math.random() * (i + 1));

    var temp = _array[i];

    _array[i] = _array[j];

    _array[j] = temp;

  }

  

  return _array;

}

 

underscore 中採用從前日後遍歷元素的方式,實現以下:

 

// Shuffle a collection, using the modern version of the

// [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).

_.shuffle = function(obj) {

  var set = isArrayLike(obj) ? 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;

};

 

將其解耦分離出來,以下:

 

function shuffle(a) {

  var length = a.length;

  var shuffled = Array(length);

 

  for (var index = 0, rand; index < length; index++) {

    rand = ~~(Math.random() * (index + 1));

    if (rand !== index)

      shuffled[index] = shuffled[rand];

    shuffled[rand] = a[index];

  }

 

  return shuffled;

}

 

跟前面同樣,作了下數據圖表,猛戳 http://hanzichi.github.io/test-case/shuffle/Fisher-Yates/。

 

關於證實,引用自月影老師的文章(https://www.h5jun.com/post/array-shuffle.html):

 

隨機性的數學概括法證實

 

對 n 個數進行隨機:

 

  1. 首先咱們考慮 n = 2 的狀況,根據算法,顯然有 1/2 的機率兩個數交換,有 1/2 的機率兩個數不交換,所以對 n = 2 的狀況,元素出如今每一個位置的機率都是 1/2,知足隨機性要求。

  2. 假設有 i 個數, i >= 2 時,算法隨機性符合要求,即每一個數出如今 i 個位置上每一個位置的機率都是 1/i。

  3. 對於 i + 1 個數,按照咱們的算法,在第一次循環時,每一個數都有 1/(i+1) 的機率被交換到最末尾,因此每一個元素出如今最末一位的機率都是 1/(i+1) 。而每一個數也都有 i/(i+1) 的機率不被交換到最末尾,若是不被交換,從第二次循環開始還原成 i 個數隨機,根據 2. 的假設,它們出如今 i 個位置的機率是 1/i。所以每一個數出如今前 i 位任意一位的機率是 (i/(i+1)) * (1/i) = 1/(i+1),也是 1/(i+1)。

  4. 綜合 1. 2. 3. 得出,對於任意 n >= 2,通過這個算法,每一個元素出如今 n 個位置任意一個位置的機率都是 1/n。

 

小結

 

關於數組亂序,若是面試中被問到,能說出 「Fisher–Yates Shuffle」,而且能基本說出原理(你也看到了,其實代碼很是的簡單),那麼基本應該沒有問題了;若是能更進一步,將其證實呈上(甚至一些面試官均可能一時證實不了),那麼就牛逼了。千萬不能只會用 Math.random() 投機取巧!

相關文章
相關標籤/搜索