摘要:出于性能優化的目的,當數組排序區間長度在之內時,實際的排序方法是插入排序,其余時候使用快速排序。大體上,這個排序方法的思想是對數組進行區間劃分,當排序區間大于時,使用快排,使局部有序,當區間小于等于時使用插入排序,使數組整體有序。
數組排序方法sort淺析
數組提供了排序方法,使用時傳入一個比較函數,根據比較函數的返回來確定元素最終在數組中的位置。默認排序順序是根據字符串Unicode碼點。
var a = [12,4,6,8,9,54,11,13]; a.sort(); // [11, 12, 13, 4, 54, 6, 8, 9]
如果指明了比較函數,那么數組會按照調用該函數的返回值排序。比較函數接受兩個參數(x,y),表示數組中待排序的元素,根據返回結果來決定如何排序:
返回結果小于0,表示x在前y在后。
返回結果等于0,則x和y位置不改變。(備注: ECMAScript 標準并不要求這一行為,說明sort排序不一定是穩定的)
返回結果大于0,表示x應該在y之后。
在MDN中還特別指出,無法保證排序的時間和空間復雜性。這是因為不同引擎實現排序方法的方式不一定相同。
V8的排序方法函數的整體結構如下,當參數不是可執行的比較函數時,內部定義默認的比較函數。
出于性能優化的目的,當數組排序區間長度在10之內時,實際的排序方法是插入排序,其余時候使用快速排序。所以定義了內部函數InsertionSort和QuickSort,同時還有函數GetThirdIndex,用于輔助快排中支點的選擇。
// TODO(pwong): Remove once TypedArray.prototype.join() is ported to Torque. function InnerArraySort(array, length, comparefn) { // In-place QuickSort algorithm. // For short (length <= 10) arrays, insertion sort is used for efficiency. if (!IS_CALLABLE(comparefn)) { comparefn = function (x, y) { // ... }; } function InsertionSort(a, from, to) { // ... }; function GetThirdIndex(a, from, to) { // ... } function QuickSort(a, from, to) { // ... }; if (length < 2) return array; var num_non_undefined = %PrepareElementsForSort(array, length); QuickSort(array, 0, num_non_undefined); return array; }
下面來逐段分析代碼。
第一個if處理默認排序,內部會將x、y轉化成字符串再進行比較。字符串比較是使用基于標準字典的 Unicode 值來進行比較的,這也是第一個例子中13在4之前的原因。
if (!IS_CALLABLE(comparefn)) { comparefn = function (x, y) { if (x === y) return 0; if (%_IsSmi(x) && %_IsSmi(y)) { return %SmiLexicographicCompare(x, y); } x = TO_STRING(x); // 轉化成字符串 y = TO_STRING(y); if (x == y) return 0; else return x < y ? -1 : 1; }; }
接著實現了插入排序。模擬將新元素插入到數組中間的過程,從第二個元素from + 1開始,根據大小關系確定插入的位置。當確定插入位置在j的時候,原在j上以及后面的元素都要向右移一,索引加一。這就是插入排序。
插入排序每次排序過程會將當前元素與前面的元素進行比較。以升序排序為例,循環將當前元素與前一元素比較,當前元素較小時交換兩個元素的位置,直至當前元素大于前一元素或到達排序區間的第一位時結束循環,完成當前元素的排序。更新新元素位置時的遍歷區間是[from, i],i的取值是[from + 1, to]。
使用element緩存插入排序中要插入的值,每次迭代中,使用tmp緩存a[j]的值,執行comparefn(tmp, element)。
返回結果order大于0的時候,說明element仍需向前,所以要將a[j]向后移動。a[j + 1] = tmp便完成了這樣的工作;直至order大于等于0,說明則找到element應插入的位置,執行a[j + 1] = element插入a[i]。
function InsertionSort(a, from, to) { for (var i = from + 1; i < to; i++) { var element = a[i]; for (var j = i - 1; j >= from; j--) { var tmp = a[j]; var order = comparefn(tmp, element); if (order > 0) { a[j + 1] = tmp; } else { break; } } a[j + 1] = element; } };
如圖示,使用插入排序使數組升序,上箭頭表示當前循環中的i,當前緩存值element是19,下箭頭是j,從i - 1開始想前遍歷,如果element應在a[j]之前,則a[j + 1] = tmp,確定插入位置時,a[j + 1] = element。這樣就完成了一個元素的插入過程。
當數組長度較長時,內部使用快速排序。快排的思想是選取某一個值作為支點值,先從頭遍歷,找出第一個應該在支點值右邊的元素,再從尾向頭遍歷,找出第一個應該在支點值左邊的元素,交換兩個元素,直至左邊與右邊重疊。重疊的位置即是支點應在的位置。以升序排序為例,支點左邊的值均小于等于支點值,右邊的值均大于支點值。
在V8引擎的實現中,支點值的選取是確定第三個值,再取其與a[from]、a[to]的中值作為支點值。當排序區間的長度在1000以內時,第三個值的位置是from + ((to - from) >> 1),接近區間的中值點。當排序區間較大時(大于1000),第三個值的索引是通過GetThirdIndex來獲取。GetThirdIndex的選取思想是將區間分成多段,每段用一個值代表,然后從這些值去選取一個接近中值的值作為支點。
increment是區間分段后每段的長度,取值區間是[200, 215]。分段的范圍是[from + 1, to - 1],每一段用起點值代表。將代表值及其在原數組a中的索引保存在數組中作為內部數組t_array的元素,并根據代表值進行排序。
最后的返回結果是t_array[t_array.length >> 1][0]。t_array.length >> 1是將t_array.length的二進制形式左移一位,取值接近t_array的中值,t_array[t_array.length >> 1][0]則是這個中值在數組a中的索引。
function GetThirdIndex(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; }
快排的實現如下。
內部使用一個while(true)循環,只有當to - from <= 10才會結束無限循環。在函數內末尾有修改from/to的代碼,避免無限循環,同時遞歸調用自身。大體上,這個排序方法的思想是對數組進行區間劃分,當排序區間大于10時,使用快排,使局部有序,當區間小于等于10時使用插入排序,使數組整體有序。
function QuickSort(a, from, to) { var third_index = 0; while (true) { // Insertion sort is faster for short arrays. if (to - from <= 10) { InsertionSort(a, from, to); return; } // ... if (to - high_start < low_end - from) { QuickSort(a, high_start, to); to = low_end; } else { QuickSort(a, from, low_end); from = high_start; } } }
第三個點的位置會根據排序區間的長度來選取。
if (to - from > 1000) { third_index = GetThirdIndex(a, from, to); } else { third_index = from + ((to - from) >> 1); }
將a[from]/a[to-1]和上面選取的第三個點的值記為v0、v1、v2。交換這些值,使其按v0 <= v1 <= v2的順序排列。
var v0 = a[from]; var v1 = a[to - 1]; var v2 = a[third_index]; var c01 = comparefn(v0, v1); if (c01 > 0) { // v1 < v0, so swap them. var tmp = v0; v0 = v1; v1 = tmp; } // v0 <= v1. var c02 = comparefn(v0, v2); if (c02 >= 0) { // v2 <= v0 <= v1. var tmp = v0; v0 = v2; v2 = v1; v1 = tmp; } else { // v0 <= v1 && v0 < v2 var c12 = comparefn(v1, v2); if (c12 > 0) { // v0 <= v2 < v1 var tmp = v1; v1 = v2; v2 = tmp; } } // v0 <= v1 <= v2 a[from] = v0; a[to - 1] = v2; var pivot = v1;
如果忽略from之前與to之后的元素,當前的排序區間可以表示成
隨后,交換low_end與third_index的值。
var low_end = from + 1; // Upper bound of elements lower than pivot. var high_start = to - 1; // Lower bound of elements greater than pivot. a[third_index] = a[low_end]; a[low_end] = pivot;
現在排序區間的結構如下:
接著從low_end + 1開始向右遍歷,令element = a[i],比較當前元素element與pivot。若comparefn(element, pivot) < 0,說明element應該在pivot前,將element與a[low_end](即pivot)交換,low_end++表示pivot位置向右移一位,因為原來的位置已變成element。
如果comparefn(element, pivot) > 0說明element應該在pivot后,從high_start開始向左查找第一個應該在pivot前或與pivot相等的元素top_elem,交換top_elem與element。如果top_elem應該在pivot之前,二者互換。如果comparefn(element, pivot) == 0,則element/pivot取值相同,無需交換,同時也無需移動low_end。
直至i == high_start時退出循環。下面是上述流程完整的代碼。
// From low_end to i are elements equal to pivot. // From i to high_start are elements that haven"t been compared yet. partition: for (var i = low_end + 1; i < high_start; i++) { var element = a[i]; var order = comparefn(element, pivot); if (order < 0) { a[i] = a[low_end]; a[low_end] = element; low_end++; } else if (order > 0) { do { high_start--; if (high_start == i) break partition; var top_elem = a[high_start]; order = comparefn(top_elem, pivot); } while (order > 0); a[i] = a[high_start]; a[high_start] = element; if (order < 0) { element = a[i]; a[i] = a[low_end]; a[low_end] = element; low_end++; } } }
完成整個快排的數組區間以a[i]/pivot為分界點,a[i]左邊的元素全小于或等于pivot,a[i]右邊的元素全大于pivot。然后從[from, low_end]和[high_start, to]中選出區間較小的一組遞歸調用QuickSort;同時將更新一個端點,縮小區間。
if (to - high_start < low_end - from) { QuickSort(a, high_start, to); to = low_end; } else { QuickSort(a, from, low_end); from = high_start; }
這是完整的快排算法。
function QuickSort(a, from, to) { var third_index = 0; while (true) { // Insertion sort is faster for short arrays. if (to - from <= 10) { InsertionSort(a, from, to); return; } if (to - from > 1000) { third_index = GetThirdIndex(a, from, to); } else { third_index = from + ((to - from) >> 1); } // Find a pivot as the median of first, last and middle element. var v0 = a[from]; var v1 = a[to - 1]; var v2 = a[third_index]; var c01 = comparefn(v0, v1); if (c01 > 0) { // v1 < v0, so swap them. var tmp = v0; v0 = v1; v1 = tmp; } // v0 <= v1. var c02 = comparefn(v0, v2); if (c02 >= 0) { // v2 <= v0 <= v1. var tmp = v0; v0 = v2; v2 = v1; v1 = tmp; } else { // v0 <= v1 && v0 < v2 var c12 = comparefn(v1, v2); if (c12 > 0) { // v0 <= v2 < v1 var tmp = v1; v1 = v2; v2 = tmp; } } // v0 <= v1 <= v2 a[from] = v0; a[to - 1] = v2; var pivot = v1; var low_end = from + 1; // Upper bound of elements lower than pivot. var high_start = to - 1; // Lower bound of elements greater than pivot. a[third_index] = a[low_end]; a[low_end] = pivot; // From low_end to i are elements equal to pivot. // From i to high_start are elements that haven"t been compared yet. partition: for (var i = low_end + 1; i < high_start; i++) { var element = a[i]; var order = comparefn(element, pivot); if (order < 0) { a[i] = a[low_end]; a[low_end] = element; low_end++; } else if (order > 0) { do { high_start--; if (high_start == i) break partition; var top_elem = a[high_start]; order = comparefn(top_elem, pivot); } while (order > 0); a[i] = a[high_start]; a[high_start] = element; if (order < 0) { element = a[i]; a[i] = a[low_end]; a[low_end] = element; low_end++; } } } if (to - high_start < low_end - from) { QuickSort(a, high_start, to); to = low_end; } else { QuickSort(a, from, low_end); from = high_start; } } };參考鏈接
Array.prototype.sort() - MDN
V8的排序方法實現
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98764.html
摘要:方法可以接受一個可選的參數,比較回調函數。方法會修改原本數組輸出如上,在調用方法后,自身數組被修改。對于長數組會使用快速排序,而快速排序一般是不穩定的。所以方法返回的數組永遠是該方法認為的升序數組。 前幾天在某公司面試的時候被問到關于這個方法的默認值的問題(然而面試官跟我說的其實是錯的,當場我還不夠底氣去反駁)。突然發現對這個方法的了解還不夠,因此回來查了資料,看了v8引擎的實現和EC...
摘要:快速排序在解決中方法問題時,筆者沒有考慮時間復雜度的問題,使用的排序算法進行重寫,在實際產品環境中引發不小的性能問題。中方法的兼容問題筆者發現或者中方法不生效不同瀏覽器實現機制差異,故判斷后進行該方法的重寫處理,代碼如下 快速排序(update) 在解決 Sarafi 中 sort 方法問題時,筆者沒有考慮時間復雜度的問題,使用 O(n2) 的排序算法進行重寫,在實際產品環境中引發不小...
摘要:前幾天一個朋友在微信里面問我一個關于數組排序的問題。對數組的進行排序,然后把排完序的數組進行處理。翻譯成編程術語就是排序算法是不穩定排序。因此第二個排序算法會把移動到最后,然后對剩余的數據進行排序。 前幾天一個朋友在微信里面問我一個關于 JS 數組排序的問題。 原始數組如下: var data = [ {value: 4}, {value: 2}, {value: un...
摘要:源碼地址為了簡化篇幅,我們對這個數組進行分析,數組長度為,此時采用的是插入排序。插入排序的源碼是其原理在于將第一個元素視為有序序列,遍歷數組,將之后的元素依次插入這個構建的有序序列中。 JavaScript 專題系列第十九篇,講解數組亂序,重點探究 Math.random() 為什么不能真正的亂序? 亂序 亂序的意思就是將數組打亂。 嗯,沒有了,直接看代碼吧。 Math.random ...
摘要:寫在前面專題系列是我寫的第二個系列,第一個系列是深入系列。專題系列自月日發布第一篇文章,到月日發布最后一篇,感謝各位朋友的收藏點贊,鼓勵指正。 寫在前面 JavaScript 專題系列是我寫的第二個系列,第一個系列是 JavaScript 深入系列。 JavaScript 專題系列共計 20 篇,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里...
閱讀 2044·2021-11-15 11:39
閱讀 3226·2021-10-09 09:41
閱讀 1491·2019-08-30 14:20
閱讀 3262·2019-08-30 13:53
閱讀 3325·2019-08-29 16:32
閱讀 3362·2019-08-29 11:20
閱讀 3018·2019-08-26 13:53
閱讀 775·2019-08-26 12:18