貌似前端[1]圈一直以來流傳着一種誤解:前端用不到算法知識。[2]javascript
長久以來,我也曾受這種說法的影響。直到前陣子遇到一個產品需求,回過頭來看,發現事實並不是如此。html
前端將排序條件做爲請求參數傳遞給後端,後端將排序結果做爲請求響應返回前端,這是一種常見設計。可是對於有些產品則不是那麼適用。前端
試想一個場景:你在使用美食類APP時,是否會常常切換排序方式,一下子按照價格排序,一下子按照評分排序。java
實際生產中,受限於服務器成本等因素,當單次數據查詢成爲總體性能瓶頸時,也會考慮經過將排序在前端完成的方式來優化性能。git
感受這個沒什麼介紹的必要,做爲計算機科學的一種基礎算法,描述就直接照搬 Wikipedia
。github
這裏存在這一段內容純粹是爲了承(cou)上(man)啓(zi)下(shu)。算法
既然說到前端排序,天然首先會想到JavaScript的原生接口 Array.prototype.sort
。編程
這個接口自 ECMAScript 1st Edition
起就存在。讓咱們看看最新的規範中關於它的描述是什麼樣的。後端
Array.prototype.sort(compareFn)
數組
The elements of this array are sorted. The sort is not necessarily stable (that is, elements that compare equal do not necessarily remain in their original order). If comparefn is not undefined, it should be a function that accepts two arguments x and y and returns a negative value if x < y, zero if x = y, or a positive value if x > y.
顯然,規範並無限定 sort
內部實現的排序算法是什麼。甚至接口的實現都不須要是 穩定排序 的。這一點很關鍵,接下來會屢次涉及。
在這樣的背景下,前端排序這件事其實取決於各家瀏覽器的具體實現。那麼,當今主流的瀏覽器關於排序是怎麼實現的呢?接下來,咱們分別簡單對比一下 Chrome
、Firefox
和 Microsoft Edge
。
Chrome
的JavaScript引擎是 v8
。因爲它是開源的,因此能夠直接看源代碼。
整個 array.JS
都是用 JavaScript
語言實現的。排序方法部分很明顯比曾經看到過的快速排序要複雜得多,但顯然核心算法仍是快速排序
。算法複雜的緣由在於 v8
出於性能考慮進行了不少優化。(接下來會展開說)
暫時沒法肯定Firefox的JavaScript引擎即將使用的數組排序算法會是什麼。[3]
按照現有的信息,SpiderMoney
內部實現了 歸併排序
。
Microsoft Edge
的JavaScript引擎 Chakra
的核心部分代碼已經於2016年初在github開源。
經過看源代碼能夠發現,Chakra
的數組排序算法實現的也是 快速排序
。並且相比較於 v8
,它就只是實現了純粹的快速排序,徹底沒有 v8
中的那些性能優化的蹤跡。
衆所周知,快速排序
是一種不穩定的排序算法,而 歸併排序
是一種穩定的排序算法。因爲不一樣引擎在算法選擇上的差別,致使前端依賴 Array.prototype.sort
接口實現的JavaScript代碼,在瀏覽器中實際執行的排序效果是不一致的。
排序穩定性的差別須要有特定的場景觸發纔會存在問題;在不少狀況下,不穩定的排序也不會形成影響。
假如實際項目開發中,對於數組的排序沒有穩定性的需求,那麼其實看到這裏爲止便可,瀏覽器之間的實現差別不那麼重要。
可是若項目要求排序必須是穩定的,那麼這些差別的存在將沒法知足需求。咱們須要爲此進行一些額外的工做。
舉個例子:
某市的機動車牌照拍賣系統,最終中標的規則爲: 1. 按價格進行倒排序; 2. 相同價格則按照競標順位(即價格提交時間)進行正排序。 排序如果在前端進行,那麼採用快速排序的瀏覽器中顯示的中標者極可能是不符合預期的
尋找解決辦法以前,咱們有必要先探究一下出現問題的緣由。
其實這個狀況從一開始便存在。
Chrome測試版
於2008年9月2日發佈,然而發佈後不久,就有開發者向 Chromium
開發組提交#90 Bug反饋v8的數組排序實現不是穩定排序的。
這個Bug ISSUE討論的時間跨度很大。一直到2015年11月10日,仍然有開發者對v8的數組排序實現問題提出評論。
同時咱們還注意到,該ISSUE曾經已被關閉。可是於2013年6月被開發組成員從新打開,做爲 ECMAScript Next
規範討論的參考。
而es-discuss的最後結論是這樣的
It does not change. Stable is a subset of unstable. And vice versa, every unstable algorithm returns a stable result for some inputs. Mark’s point is that requiring 「always unstable」 has no meaning, no matter what language you chose.
/Andreas
正如本文前段所引用的已定稿 ECMAScript 2015
規範中的描述。
IMHO,Chrome發佈之初即被報告出這個問題多是有其特殊的時代特色。
上文已經說到,Chrome初版
是 2008年9月
發佈的。根據statcounter的統計數據,那個時期市場佔有率最高的兩款瀏覽器分別是IE
(那時候只有IE6
和IE7
) 和 Firefox
,市場佔有率分別達到了67.16%和25.77%。也就是說,兩個瀏覽器加起來的市場佔有率超過了90%。
而根據另外一份瀏覽器排序算法穩定性的統計數據顯示,這兩款超過了90%市場佔有率的瀏覽器都採用了穩定的數組排序。因此Chrome發佈之初被開發者質疑也是合情合理的。
從Bug ISSUE討論的過程當中,能夠大概理解開發組成員對於引擎實現採用快速排序的一些考量。
其中一點,他們認爲引擎必須遵照ECMAScript
規範。因爲規範不要求穩定排序的描述,故他們認爲 v8
的實現是徹底符合規範的。
另外,他們認爲 v8
設計的一個重要考量在於引擎的性能。
快速排序
相比較於 歸併排序
,在總體性能上表現更好:
快速排序
在實際計算機執行環境中比同等時間複雜度的其餘排序算法更快(不命中最差組合的狀況下)既然說 v8
很是看中引擎的性能,那麼在數組排序中它作了哪些事呢?
經過閱讀源代碼,仍是粗淺地學習了一些皮毛。
混合插入排序
快速排序
是分治的思想,將大數組分解,逐層往下遞歸。可是若遞歸深度太大,爲了維持遞歸調用棧的內存資源消耗也會很大。優化很差甚至可能形成棧溢出。
目前v8的實現是設定一個閾值,對最下層的10個及如下長度的小數組使用 插入排序
。
根據代碼註釋以及 Wikipedia
中的描述,雖然插入排序的平均時間複雜度爲 O(n²)
差於快速排序的 O(n㏒n)
。可是在運行環境,小數組使用插入排序的效率反而比快速排序會更高,這裏再也不展開。
v8代碼示例
var QuickSort = function QuickSort(a, from, to) { ...... while (true) { // Insertion sort is faster for short arrays. if (to - from <= 10) { InsertionSort(a, from, to); return; } ...... } ...... };
三數取中
正如已知的,快速排序的阿克琉斯之踵在於,最差數組組合狀況下會算法退化。
快速排序的算法核心在於選擇一個基準 (pivot)
,將通過比較交換的數組按基準分解爲兩個數區進行後續遞歸。試想若是對一個已經有序的數組,每次選擇基準元素時老是選擇第一個或者最後一個元素,那麼每次都會有一個數區是空的,遞歸的層數將達到 n
,最後致使算法的時間複雜度退化爲 O(n²)
。所以 pivot
的選擇很是重要。
v8採用的是 三數取中(median-of-three)
的優化:除了頭尾兩個元素再額外選擇一個元素參與基準元素的競爭。
第三個元素的選取策略大體爲:
最後取三個元素的中位值做爲 pivot
。
v8代碼示例
var GetThirdIndex = function(a, from, to) { var t_array = new InternalArray(); // Use both 'from' and 'to' to determine the pivot candidates. var increment = 200 + ((to - from) & 15); var j = 0; from += 1; to -= 1; for (var i = from; i < to; i += increment) { t_array[j] = [i, a[i]]; j++; } t_array.sort(function(a, b) { return comparefn(a[1], b[1]); }); var third_index = t_array[t_array.length >> 1][0]; return third_index; }; var QuickSort = function QuickSort(a, from, to) { ...... while (true) { ...... if (to - from > 1000) { third_index = GetThirdIndex(a, from, to); } else { third_index = from + ((to - from) >> 1); } } ...... };
原地排序
在溫習快速排序算法時,我在網上看到了不少用JavaScript實現的例子。
可是發現一大部分的代碼實現以下所示
var quickSort = function(arr) { if (arr.length <= 1) { return arr; } var pivotIndex = Math.floor(arr.length / 2); var pivot = arr.splice(pivotIndex, 1)[0]; var left = []; var right = []; for (var i = 0; i < arr.length; i++){ if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return quickSort(left).concat([pivot], quickSort(right)); };
以上代碼的主要問題在於:利用 left
和 right
兩個數區存儲遞歸的子數組,所以它須要 O(n)
的額外存儲空間。這與理論上的平均空間複雜度 O(㏒n)
相比差距較大。
額外的空間開銷,一樣會影響實際運行時的總體速度。這也是快速排序在實際運行時的表現能夠超過同等時間複雜度級別的其餘排序算法的其中一個緣由。因此通常來講,性能較好的快速排序會採用原地 (in-place)
排序的方式。
v8
源代碼中的實現是對原數組進行元素交換。
它的背後也是有故事的。
Firefox其實在一開始發佈的時候對於數組排序的實現並非採用穩定的排序算法,這塊有據可考。
Firefox(Firebird)最第一版本
實現的數組排序算法是 堆排序
,這也是一種不穩定的排序算法。所以,後來有人對此提交了一個Bug。
Mozilla開發組內部針對這個問題進行了一系列討論。
從討論的過程咱們可以得出幾點
Mozilla
的競爭對手是 IE6
,從上文的統計數據可知IE6是穩定排序的Brendan Eich
以爲 Stability is good
堆排序
以前採用的是 快速排序
基於開發組成員傾向於實現穩定的排序算法爲主要前提,Firefox3
將 歸併排序
做爲了數組排序的新實現。
以上說了這麼多,主要是爲了講述各個瀏覽器對於排序實現的差別,以及解釋爲何存在這些差別的一些比較表層的緣由。
可是讀到這裏,讀者可能仍是會有疑問:若是個人項目就是須要依賴穩定排序,那該怎麼辦呢?
其實解決這個問題的思路比較簡單。
瀏覽器出於不一樣考慮選擇不一樣排序算法。可能某些偏向於追求極致的性能,某些偏向於提供良好的開發體驗,可是有規律可循。
從目前已知的狀況來看,全部主流瀏覽器(包括IE6,7,8)對於數組排序算法的實現基本能夠枚舉:
歸併排序
/ Timsort快速排序
因此,咱們將快速排序通過定製改造,變成穩定排序的是否是就能夠了?
通常來講,針對對象數組使用不穩定排序會影響結果。而其餘類型數組自己使用穩定排序或不穩定排序的結果是相等的。所以方案大體以下:
將待排序數組進行預處理,爲每一個待排序的對象增長天然序屬性,不與對象的其餘屬性衝突便可。 自定義排序比較方法compareFn,老是將天然序做爲前置判斷相等時的第二判斷維度。
面對歸併排序這類實現時因爲算法自己就是穩定的,額外增長的天然序比較並不會改變排序結果,因此方案兼容性比較好。
可是涉及修改待排序數組,並且須要開闢額外空間用於存儲天然序屬性,可想而知 v8
這類引擎應該不會採用相似手段。不過做爲開發者自行定製的排序方案是可行的。
'use strict'; const INDEX = Symbol('index'); function getComparer(compare) { return function (left, right) { let result = compare(left, right); return result === 0 ? left[INDEX] - right[INDEX] : result; }; } function sort(array, compare) { array = array.map( (item, index) => { if (typeof item === 'object') { item[INDEX] = index; } return item; } ); return array.sort(getComparer(compare)); }
以上只是一個簡單的知足穩定排序的算法改造示例。
之因此說簡單,是由於實際生產環境中做爲數組輸入的數據結構冗雜,須要根據實際狀況判斷是否須要進行更多樣的排序前類型檢測。
必須看到,這幾年愈來愈多的項目正在往富客戶端應用方向轉變,前端在項目中的佔比變大。隨着將來瀏覽器計算能力的進一步提高,它容許進行一些更復雜的計算。伴隨職責的變動,前端的形態也可能會發生一些重大變化。
行走江湖,總要有一技傍身。
JavaScript
做爲編程語言的環境Firefox
數組排序實現的算法時,搜到了 SpiderMoney
的一篇排序相關的Bug。大體意思是討論過程當中有人建議用極端狀況下性能更好的 Timsort
算法替換 歸併排序
,可是 Mozilla
的工程師表示因爲 Timsort
算法存在License
受權問題,沒辦法在 Mozilla
的軟件中直接使用算法,等待對方的後續回覆原文來自:http://efe.baidu.com/blog/talk-about-sort-in-front-end/