V8數組排序方法sort淺析

數組排序方法sort淺析

數組提供了排序方法,使用時傳入一個比較函數,根據比較函數的返回來肯定元素最終在數組中的位置。默認排序順序是根據字符串Unicode碼點。git

var a = [12,4,6,8,9,54,11,13];
a.sort(); // [11, 12, 13, 4, 54, 6, 8, 9]

若是指明瞭比較函數,那麼數組會按照調用該函數的返回值排序。比較函數接受兩個參數(x,y),表示數組中待排序的元素,根據返回結果來決定如何排序:github

  • 返回結果小於0,表示x在前y在後。
  • 返回結果等於0,則x和y位置不改變。(備註: ECMAScript 標準並不要求這一行爲,說明sort排序不必定是穩定的)
  • 返回結果大於0,表示x應該在y以後。

在MDN中還特別指出,沒法保證排序的時間和空間複雜性。這是由於不一樣引擎實現排序方法的方式不必定相同。算法

V8的排序方法

函數的總體結構以下,當參數不是可執行的比較函數時,內部定義默認的比較函數。數組

出於性能優化的目的,當數組排序區間長度在10以內時,實際的排序方法是插入排序,其他時候使用快速排序。因此定義了內部函數InsertionSortQuickSort,同時還有函數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處理默認排序,內部會將xy轉化成字符串再進行比較。字符串比較是使用基於標準字典的 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)ui

返回結果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]和上面選取的第三個點的值記爲v0v1v2。交換這些值,使其按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以後的元素,當前的排序區間能夠表示成

quick-sort-1

隨後,交換low_endthird_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;

如今排序區間的結構以下:

quick-sort-2

接着從low_end + 1開始向右遍歷,令element = a[i],比較當前元素elementpivot。若comparefn(element, pivot) < 0,說明element應該在pivot前,將elementa[low_end](即pivot)交換,low_end++表示pivot位置向右移一位,由於原來的位置已變成element

quick-sort-3

若是comparefn(element, pivot) > 0說明element應該在pivot後,從high_start開始向左查找第一個應該在pivot前或與pivot相等的元素top_elem,交換top_elemelement。若是top_elem應該在pivot以前,兩者互換。若是comparefn(element, pivot) == 0,則element/pivot取值相同,無需交換,同時也無需移動low_end

quick-sort-4

直至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]左邊的元素全小於或等於pivota[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;
      }
    }
  };

參考連接

相關文章
相關標籤/搜索