javaScript如何實現真正的數組亂序否纬?

作為一個前端吕晌,在開發(fā)過程中有時會遇到要將一個數組隨機排序(shuffle)的需求,一個常見的寫法是這樣:

function shuffle(arr) {

    arr.sort(function () {

        return Math.random() - 0.5;

    });

}

或者使用更簡潔的 ES6 的寫法:

function shuffle(arr) {

    arr.sort(() => Math.random() - 0.5);

}

我也曾經經常使用這種寫法临燃,不久前才意識到睛驳,這種寫法是有問題的,它并不能真正地隨機打亂數組谬俄。

具體是什么問題,看下面:
看下面的代碼弃理,我們生成一個長度為 10 的數組['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']溃论,使用上面的方法將數組亂序,執(zhí)行多次后痘昌,會發(fā)現每個元素仍然有很大機率在它原來的位置附近出現钥勋。

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

如果排序真的是隨機的,那么每個元素在每個位置出現的概率都應該一樣辆苔,實驗結果各個位置的數字應該很接近算灸,而不應像現在這樣明顯地集中在原來位置附近。因此驻啤,我們可以認為菲驴,使用形如arr.sort(() => Math.random() - 0.5)這樣的方法得到的并不是真正的隨機排序。

另外骑冗,需要注意的是上面的分布僅適用于數組長度不超過 10 的情況赊瞬,如果數組更長,比如長度為 11,則會是另一種分布。比如:

let a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']; // 長度為11

let n = 10000;

let count = (new Array(a.length)).fill(0);



for (let i = 0; i < n; i ++) {

    let arr = [].concat(a);

    arr.sort(() => Math.random() - 0.5);

    count[arr.indexOf('a')]++;

}

console.log(count);

探索

看了一下ECMAScript中關于Array.prototype.sort(comparefn)的標準基跑,其中并沒有規(guī)定具體的實現算法峦阁,但是提到一點:

Calling comparefn(a,b) always returns the same value v when given a specific pair of values a and b as its two arguments.

也就是說,對同一組a退渗、b的值,comparefn(a, b)需要總是返回相同的值。而上面的() => Math.random() - 0.5(即(a, b) => Math.random() - 0.5)顯然不滿足這個條件缩筛。

翻看v8引擎數組部分的源碼,注意到它出于對性能的考慮堡称,對短數組使用的是插入排序歪脏,對長數組則使用了快速排序,至此粮呢,也就能理解為什么() => Math.random() - 0.5并不能真正隨機打亂數組排序了婿失。(有一個沒明白的地方:源碼中說的是對長度小于等于 22 的使用插入排序钞艇,大于 22 的使用快排,但實際測試結果顯示分界長度是 10豪硅。)

解決方案

知道問題所在哩照,解決方案也就比較簡單了懒浮。

方案一

既然(a, b) => Math.random() - 0.5的問題是不能保證針對同一組a飘弧、b每次返回的值相同砚著,那么我們不妨將數組元素改造一下,比如將每個元素i改造為:

let new_i = {

    v: i,

    r: Math.random()

};

即將它改造為一個對象稽穆,原來的值存儲在鍵v中,同時給它增加一個鍵r舌镶,值為一個隨機數,然后排序時比較這個隨機數:

arr.sort((a, b) => a.r - b.r);

完整代碼如下:

function shuffle(arr) {

    let new_arr = arr.map(i => ({v: i, r: Math.random()}));

    new_arr.sort((a, b) => a.r - b.r);

    arr.splice(0, arr.length, ...new_arr.map(i => i.v));

}



let a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];

let n = 10000;

let count = (new Array(a.length)).fill(0);



for (let i = 0; i < n; i ++) {

    shuffle(a);

    count[a.indexOf('a')]++;

}



console.log(count);

方案二(Fisher–Yates shuffle)

需要注意的是餐胀,上面的方法雖然滿足隨機性要求了,但在性能上并不是很好否灾,需要遍歷幾次數組,還要對數組進行splice等操作墨技。

考察Lodash 庫中的 shuffle 算法磨镶,注意到它使用的實際上是Fisher–Yates 洗牌算法琳猫,這個算法由 Ronald Fisher 和 Frank Yates 于 1938 年提出,然后在 1964 年由 Richard Durstenfeld 改編為適用于電腦編程的版本私痹。用偽代碼描述如下:

-- To shuffle an array a of n elements (indices 0..n-1):

for i from n?1 downto 1 do

 j ← random integer such that 0 ≤ j ≤ i

 exchange a[j] and a[i]

一個實現如下(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;

    }

}

因為有的同學直接看代碼可能會有些困難脐嫂,所以在這里重新給大家講解一下

首先我們有一個已經排好序的數組:


排序好的數組.png

Step1:
第一步需要做的就是,從數組末尾開始紊遵,選取最后一個元素账千。


找到數組的最后一位數.png

在數組一共9個位置中,隨機產生一個位置暗膜,該位置元素與最后一個元素進行交換匀奏。


在數組中隨機產生一個位置.png
找到這個位置的數.png
與最后一位進行交換.png

Step2:
上一步中,我們已經把數組末尾元素進行隨機置換学搜。
接下來娃善,對數組倒數第二個元素動手论衍。在除去已經排好的最后一個元素位置以外的8個位置中,隨機產生一個位置聚磺,該位置元素與倒數第二個元素進行交換坯台。


找到倒數第二個數.png
在出去最后一個元素的位置中獲取隨機位置.png
找到這個位置的數并交換.png

Step3:
理解了前兩部,接下來就是依次進行瘫寝,如此簡單蜒蕾。


按照之前步驟繼續(xù)進行.png

小結

如果要將數組隨機排序,千萬不要再用(a, b) => Math.random() - 0.5這樣的方法焕阿。目前而言咪啡,Fisher–Yates shuffle 算法應該是最好的選擇。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末暮屡,一起剝皮案震驚了整個濱河市撤摸,隨后出現的幾起案子,更是在濱河造成了極大的恐慌栽惶,老刑警劉巖愁溜,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疾嗅,死亡現場離奇詭異外厂,居然都是意外死亡,警方通過查閱死者的電腦和手機代承,發(fā)現死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門汁蝶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人论悴,你說我怎么就攤上這事掖棉。” “怎么了膀估?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵幔亥,是天一觀的道長。 經常有香客問我察纯,道長,這世上最難降的妖魔是什么香伴? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任具则,我火速辦了婚禮,結果婚禮上低斋,老公的妹妹穿的比我還像新娘。我一直安慰自己拔稳,他們只是感情好,可當我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布术奖。 她就那樣靜靜地躺著采记,像睡著了一般政勃。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上奸远,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天懒叛,我揣著相機與錄音,去河邊找鬼薛窥。 笑死,一個胖子當著我的面吹牛佩番,可吹牛的內容都是我干的罢杉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼赋秀,長吁一口氣:“原來是場噩夢啊……” “哼沃琅!你這毒婦竟也來了蜘欲?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤郭脂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后屿衅,有當地人在樹林里發(fā)現了一具尸體莹弊,經...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡忍弛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了蔗彤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疯兼。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖待侵,靈堂內的尸體忽然破棺而出来氧,到底是詐尸還是另有隱情香拉,我是刑警寧澤,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布扑毡,位于F島的核電站盛险,受9級特大地震影響,放射性物質發(fā)生泄漏苦掘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一惯驼、第九天 我趴在偏房一處隱蔽的房頂上張望祟牲。 院中可真熱鬧,春花似錦说贝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至县习,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間躁愿,已是汗流浹背沪蓬。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逸雹,地道東北人云挟。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像帖世,于是被迫代替她去往敵國和親沸枯。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,107評論 2 356

推薦閱讀更多精彩內容