關于JavaScript的數組隨機排序

昨天了解了一下Fisher–Yates 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'],使用上面的方法將數組亂序,執行多次后,會發現每個元素仍然有很大機率在它原來的位置附近出現。

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

在?瀏覽器控制臺 中執行,輸出[ 2891, 2928, 1927, 1125, 579, 270, 151, 76, 34, 19 ](帶有一定隨機性,每次結果都不同,但大致分布應該一致),即進行 10000 次排序后,字母'a'(數組中的第一個元素)有約 2891 次出現在第一個位置、2928 次出現在第二個位置,與之對應的只有 19 次出現在最后一個位置。如果把這個分布繪制成圖像,會是下面這樣:

類似地,我們可以算出字母'f'(數組中的第六個元素)在各個位置出現的分布為[ 312, 294, 579, 1012, 1781, 2232, 1758, 1129, 586, 317 ],圖像如下:

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

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

function newarr(){
let a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']; // 長度為11
let n = 10000; 
var count = (new Array(a.length)).fill(0); 
for (var i = 0; i < n; i ++) { var arr = [].concat(a); arr.sort(() => Math.random() - 0.5); count[arr.indexOf('a')]++; 
} 
//console.log(count);
return count;
}newarr();

在?瀏覽器控制臺?中多次執行,其中第一個元素'a'的分布位置結果如下:

(11)?[785, 826, 629, 652, 937, 1079, 960, 680, 617, 986, 1849]
newarr()
(11)?[844, 816, 636, 665, 947, 1053, 901, 654, 661, 982, 1841]
newarr()
(11)?[804, 829, 622, 655, 923, 1093, 916, 667, 591, 974, 1926]
newarr()
(11)?[779, 793, 655, 713, 916, 1161, 911, 642, 579, 936, 1915]
newarr()
(11)?[786, 783, 607, 653, 956, 1116, 954, 655, 619, 1028, 1843]
newarr()
(11)?[867, 797, 647, 635, 943, 1056, 929, 652, 572, 977, 1925]

雖然數組長度大于10后比之前的分布更均勻,但是明顯還有問題(最后一個最大)。

分布不同的原因是 v8 引擎中針對短數組和長數組使用了不同的排序方法(下面會講)。可以看到,兩種算法的結果雖然不同,但都明顯不夠均勻。

探索

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

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

一次執行結果為:[ 1023, 991, 1007, 967, 990, 1032, 968, 1061, 990, 971 ]。多次驗證,同時在這兒查看shuffle(arr)函數結果的可視化分布,可以看到,這個方法可以認為足夠隨機了。

方案二(Fisher–Yates shuffle)

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

考察Lodash 庫中的 shuffle 算法,注意到它使用的實際上是Fisher–Yates 洗牌算法,這個算法由 Ronald Fisher 和 Frank Yates 于 1938 年提出,然后在 1964 年由 Richard Durstenfeld 改編為適用于電腦編程的版本。

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; } } //對應的ES6如下
function shuffle(arr) { let i = arr.length; while (i) { let j = Math.floor(Math.random() * i--);  //5555
 [arr[j], arr[i]] = [arr[i], arr[j]]; } } 

小結

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

轉自:http://developer.51cto.com/art/201704/536457.htm

轉載于:https://www.cnblogs.com/7qin/p/9710034.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/451173.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/451173.shtml
英文地址,請注明出處:http://en.pswp.cn/news/451173.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

通用唯一識別碼UUID

UUID是通用唯一識別碼&#xff08;Universally Unique Identifier&#xff09;的縮寫。UUID 的目的&#xff0c;是讓分布式系統中的所有元素&#xff0c;都能有唯一的辨識資訊&#xff0c;而不需要透過中央控制端來做辨識資訊的指定。如此一來&#xff0c;每個人都可以建立不與…

java內省機制 + 內省是什么 + 內省實現方式 + 和反射的區別

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 一、內省是什么、實現方式&#xff1a; 內省&#xff08;Introspector&#xff09;是Java語言對Bean類屬性、事件的一種缺省處理方法。…

百度聯合長虹發布第二款云手機 售價900元以下

摘要&#xff1a;【搜狐IT消息】5月15日消息&#xff0c;百度今天宣布聯合長虹發布第二款智能手機&#xff0c;采用3.5英寸屏幕、300萬像素攝像頭&#xff0c;650MHz主頻處理器&#xff0c;零售價格在700-899元之間&#xff0c;中國聯通將為其提供話費補貼。 【搜狐IT消息】5月…

vmware workstation17環境安裝centos7

打開控制面板&#xff0c;搜索“服務”&#xff0c;啟動vmware authorize service -------解決無法開啟虛擬機問題之無法連接MKS 2.虛擬機硬盤擴展為15G------解決安裝centos7時出現的“檢查存儲配置出錯”問題 3.硬盤分區----/boot 300mb&#xff08;不能小于200mb&#xff0…

博客園中的源代碼格式顯示

昨天寫了一篇文章&#xff0c;但是在寫的時候呢&#xff0c;沒有注意&#xff0c;直接將代碼復制上去了&#xff0c;今天正好有人提醒&#xff0c;看到了格式的混亂&#xff0c;借此記錄整理一下&#xff0c;如何能直接粘貼代碼&#xff0c;而且格式&#xff08;縮進&#xff0…

static的使用

類中的靜態變量在程序運行期間&#xff0c;其內存空間對所有該類的對象實例而言是共享的&#xff0c;為了節省系統內存開銷、共享資源&#xff0c;應該對一些適合使用static的變量聲明為靜態變量。 變量聲明為static的使用場景&#xff1a; &#xff08;1&#xff09;變量所…

Linux內核的裁剪和移植

linux內核的裁剪和移植具體都在這個網址里面。https://blog.csdn.net/xie0812/article/details/10816059https://blog.csdn.net/xie0812/article/details/10821779轉載于:https://blog.51cto.com/13401435/2145947

李開復唱衰互聯網手機:大部分公司會失敗

摘要&#xff1a;互聯網企業和手機制造企業之間巨大的鴻溝也被李開復鮮明地指出來&#xff1a;“兩個產業差別巨大&#xff0c;企業基因不同。”百度此前也坦誠表示&#xff0c;與長虹合作的千元機&#xff0c;主要是針對2000元以下的用戶體驗&#xff0c;不能與四五千元的蘋果…

【POJ】3268 Silver Cow Party

題目鏈接&#xff1a;http://poj.org/problem?id3268 題意 &#xff1a;有N頭奶牛&#xff0c;M條單向路。X奶牛開party&#xff0c;其他奶牛要去它那里。每頭奶牛去完X那里還要返回。去回都是走的最短路。現在問這里面哪頭奶牛走的路最長。 題解&#xff1a;對每個奶牛i與X做…

java.util.ConcurrentModificationException異常分析

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 Java在操作ArrayList、HashMap、TreeMap等容器類時&#xff0c;遇到了java.util.ConcurrentModificationException異常。以ArrayList為例…

redis基本數據類型之String

redis基本數據類型之String redis一共分為5中基本數據類型&#xff1a;String,Hash,List,Set,ZSet String String類型是包含很多種類型的特殊類型&#xff0c;并且是二進制安全的。比如序列化的對象進行儲存&#xff0c;比如一張圖片進行二進制儲存&#xff0c;比如一個簡單…

Laravel5.5之事件監聽、任務調度、隊列

一、事件監聽 流程&#xff1a; 1.1 創建event php artisan make:event UserLogin LoginController.php /*** The user has been authenticated.** param \Illuminate\Http\Request $request* param mixed $user* return mixed*/protected function authenticated(Request …

朱江洪功成身退 朱董配解體誰主格力(圖)

摘要&#xff1a;中國家電營銷委員會副理事長洪仕斌向時代周報記者表示&#xff1a;“朱江洪和董明珠已經完成了他們在格力發展前二十年的使命。“朱董配”解體之后&#xff0c;有人質疑格力“技術營銷”的格局必將被打破&#xff0c;難以延續&#xff0c;“董氏班底”與朱江洪…

一些dos下簡單命令

(1)切換盤符 d: 回車 (2)顯示某目錄下的所有文件或者文件夾(掌握) dir 回車 (3)創建文件夾 md 文件夾名稱 回車 (4)刪除文件夾 rd 文件夾名稱 回車 (5)進入目錄(掌握) 單級進入 cd 目錄名稱 多級進入 cd 目錄名稱1\目錄名稱2\... (6)回退目錄(掌握) 單級回退 cd.. …

ssh服務器拒絕了密碼 請再試一次 Xftp5連接失敗

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 我的情況都很簡單&#xff1a; 第一回主機 ip 不對&#xff0c; 第二次 是賬號、密碼都不對。 最后 IP、賬號、密碼都對了 就連上了。

后端DTO(數據傳輸對象)與DAO(數據庫數據源對象)解耦的好處

我們在后端的開發中經常會將DO對象傳到Service層直接作為DTO傳給前端&#xff0c;這樣做其實會有很多弊端。 &#xff08;一&#xff09;DO對象一般其成員域和數據庫字段是對應的&#xff0c;所以不能添加額外的字段&#xff0c;但是有時候端就是需要這個字段。反之前端要向后…

【刷算法】字符串的全排列

題目描述 輸入一個字符串,按字典序打印出該字符串中字符的所有排列。例如輸入字符串abc,則打印出由字符a,b,c所能排列出來的所有字符串abc,acb,bac,bca,cab和cba。 分析 沒啥好分析的了&#xff0c;這個題不會&#xff0c;上網查的思路&#xff0c;大概就是&#xff1a; abc分化…

BZOJ.2741.[FOTILE模擬賽]L(分塊 可持久化Trie)

題目鏈接 首先記\(sum\)為前綴異或和&#xff0c;那么區間\(s[l,r]sum[l-1]^{\wedge}sum[r]\)。即一個區間異或和可以轉為求兩個數的異或和。 那么對\([l,r]\)的詢問即求\([l-1,r]\)中某兩個數異或的最大值。 區間中某一個數和已知的一個數異或的最大值可以用可持久化Trie \(O(…

傳騰訊人事大地震 馬化騰將重整公司架構

摘要&#xff1a;5月17日消息&#xff0c;傳騰訊董事長馬化騰將重新組織公司架構&#xff0c;為騰訊大換血。據悉&#xff0c;騰訊之所以選擇互動娛樂部門負責人接任這一重要崗位&#xff0c;也是因為互娛部門業績持續快速發展&#xff0c;成為了“騰訊帝國”發展的核心驅動力之…

阿里云對象存儲OSS與文件存儲NAS的區別

一、簡介 應用場景&#xff1a;選擇一款存儲產品&#xff0c;面向文檔數據的存取&#xff0c;不會涉及到數據處理。 產品選型主要從OSS和NAS中選擇一款&#xff0c;滿足文檔存儲的需求。 二、NAS優缺點 NAS 是一種采用直接與網絡介質相連的特殊設備實現數據存儲的機制。由于這些…