深刻解析V8中sort的工做原理

深刻解析V8中sort的工做原理

背景

由一道算法題引發的思考。
以前在leetcode刷題的時候遇到這道題(題目來源尋找兩個有序數組的中位數javascript

給定兩個大小爲 m 和 n 的有序數組 nums1 和 nums2。
請你找出這兩個有序數組的中位數,而且要求算法的時間複雜度爲 O(log(m + n))。
你能夠假設 nums1 和 nums2 不會同時爲空。html

示例1
nums1 = [1, 3]
nums2 = [2]

則中位數是 2.0

示例2
nums1 = [1, 2]
nums2 = [3, 4]

則中位數是 (2 + 3)/2 = 2.5

當初作的時候沒有認真審題,直接兩個數組合並而後排序再取中位數,提交的時候也直接AC了。後來看解析的時候發現有人吐槽js直接用sort,耍賴皮,我才發現,原來題目要求時間複雜度爲O(log(m + n))。java

那麼問題來了,排序最快也要O(nlogn),那爲什麼不會超時呢?git

先看下我不審題的解答github

var findMedianSortedArrays = function(nums1, nums2) {
    let num = nums1.concat(nums2);
    num = num.sort((a, b) => a - b);
    let mid = Math.floor(num.length / 2);
    if (num.length % 2 === 0) {
        return (num[mid-1] + num[mid])/2
    } else {
        return num[mid]
    }
};

我這裏用的是V8優化事後的sort而不是普通的快排,那麼是否是證實V8的sort要比快排還要快呢?算法

V8中的sort與快速排序比較

這裏我寫了一個腳本,用來比較兩個算法的運行時長segmentfault

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

let arr = [], brr = [], idx = 0, length = Math.floor(Math.random() * 10000000);
while (idx < length) {
    arr[idx] = brr[idx] = Math.floor(Math.random() * length);
    idx++;
}
console.log('length===', length)

console.time('quicksort')
quickSort(arr)
console.timeEnd('quicksort')

console.time('V8_sort')
brr.sort((a, b) => {
    return a - b
})
console.timeEnd('V8_sort')

clipboard.png

咱們能夠看到結果,不管隨機數組的長度如何,顯然V8提供的sort是要比快排快的(可能有人吐糟個人快排有問題,快排寫法取自阮一峯老師的博客,可能又有槓精要說阮一峯老師的快排是非原地快排,好的,請出門左拐,不送)數組

快排原理

這裏先給你們補習一下快排的原理,熟悉的同窗能夠直接到下一標題。dom

原理

①選擇一個元素做爲"基準"
②小於"基準"的元素,都移到"基準"的左邊;大於"基準"的元素,都移到"基準"的右邊。
③對"基準"左邊和右邊的兩個子集,不斷重複第一步和第二步,直到全部子集只剩下一個元素爲止。函數

示例

如下示例取自阮一峯老師的博客快速排序(Quicksort)的Javascript實現
舉例來講,如今有一個數據集{85, 24, 63, 45, 17, 31, 96, 50},怎麼對其排序呢?

第一步,選擇中間的元素45做爲"基準"。(基準值能夠任意選擇,可是選擇中間的值比較容易理解。)
clipboard.png

第二步,按照順序,將每一個元素與"基準"進行比較,造成兩個子集,一個"小於45",另外一個"大於等於45"。

clipboard.png

第三步,對兩個子集不斷重複第一步和第二步,直到全部子集只剩下一個元素爲止。

clipboard.png

clipboard.png

clipboard.png

clipboard.png

V8中sort原理

其中V8中的sort並非單一的一種排序方法,而是根據數組長度來選擇具體的方法,當數組長度小於等於22,選擇用插入排序,大於22則選擇快速排序,源碼中是這樣寫到:

// In-place QuickSort algorithm.
  // For short (length <= 22) arrays, insertion sort is used for efficiency.

插入排序其實沒什麼好說的,本文就此略過。
那麼咱們重點來看V8中sort的快速排序是怎麼實現的。

基準的選擇

先看源碼

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

①當數組長度小於等於10,剩下的數組直接用插入排序
②當數組長度大於10小於等於1000時,third_index = from + ((to - from) >> 1);
③當數組長度大於1000時,每隔 200 ~ 215 個元素取一個值,而後將這些值進行排序,取中間值的下標,經過如下函數實現

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

這裏補充一下from + ((to - from) >> 1)200 + ((to - from) & 15)中的&和>>:

①&:按位與運算符「&」是雙目運算符。其功能是參與運算的兩數各對應的二進位相與。只有對應的兩個二進位都爲1時,結果位才爲1。參與運算的兩個數均以補碼出現。

規則:

1&1=1
1&0=0
0&1=0
0&0=0

例如:

3:0000 0011 
5:0000 0101
獲得的結果是:
1:0000 0001
因此3 & 5 = 1

②>>:按照二進制把數字右移指定數位,符號位爲正補零,符號位負補一,低位直接移除。
例如:

let a = 60;
(60: 0011 1100)
a >> 2 以後等於 15
(15: 0000 1111)

源碼解析

源碼太長,咱們在這就不一行一行地過,直接貼上比較關鍵的代碼,有興趣的同窗能夠去看github上面的源碼V8 sort源碼,建議從第710行開始看

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;
    };
  }
  var InsertionSort = f

用過sort的同窗應該知道,該函數接收一個函數comparefn做爲參數,若不傳,則默認將元素以字符串的方式升序排序,如:
clipboard.png

var InsertionSort = 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;
    }
  };

  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) {
    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;
      }
    }
  };
  1. 用上面所闡述的方法獲取基準
  2. 將基準、第一個元素以及最後一個元素進行排序處理
  3. 分別從第二個元素往右遍歷和倒數第二個元素往左遍歷,獲取基準左側比基準大的數與基準右側比基準大的數,而後交換位置,接着基準與交換後較小的數字互換位置。
  4. 繼續遍歷,繼續交換,直至左遊標與右遊標相會。
  5. 這時,基準左側均是比基準小的數,基準右側均是比基準大的數,分拆爲兩個數組,再遞歸遍歷重複上面全部步驟,直到遞歸的數組長度小於等於10,便直接使用插入排序。

舉個例子🌰:
現有一個數組let arr= [1, 3, 9, 7, 0, 5, 2, 10, 6, 8, 4];

  1. 首先執行QuickSort函數,from是0,to是11,數組長度爲11,基準爲0 + ((11 - 0) >> 1)等於5
  2. 因此a[from]也就是a[0]a[to]也就是a[10]和基準a[5]三者之間比較大小,獲得新的數組是[1, 3, 9, 7, 0, 4, 2, 10, 6, 8, 5],其中a[from] == a[0] == 1,a[from] == a[10] == 5,基準值a[5] == 4;
  3. 基準與a[to+1]互換,獲得[1, 4, 9, 7, 0, 3, 2, 10, 6, 8, 5]
  4. 而後便進入partition循環,其中a[low_end] = 9; a[high_start] = 8;開始從low_end往右找比基準大的值9,以及從high_start開始往左找比基準小的值2,互換獲得[1, 4, 2, 7, 0, 3, 9, 10, 6, 8, 5]
  5. 而後基準值與剛纔的較小值互換,獲得[1, 2, 4, 7, 0, 3, 9, 10, 6, 8, 5],接着重複步驟4
  6. 而後遍歷7,7與3互換獲得[1, 2, 4, 3, 0, 7, 9, 10, 6, 8, 5]
  7. 緊接着基準與較小值互換[1, 2, 3, 4, 0, 7, 9, 10, 6, 8, 5]
  8. 最後獲得[1, 2, 3, 0, 4, 7, 9, 10, 6, 8, 5]
  9. 能夠看出,基準左側爲比基準小的數組,基準右側爲比基準大的數組,分別用QuicSort遞歸左側數組和右側數組,最後便獲得結果。

總結

V8中的sort並非一種單純的排序方式,而是結合了插入排序以及快速排序的函數,而且針對快排作了優化。
V8 7.0 數組開始使用 TimSort 排序算法,在個人下一篇文章中會有講述Timsort的原理本人才疏學淺,如有錯誤之處,請指正,一定儘快更改。

相關文章
相關標籤/搜索