講下 V8 sort 的大概思路,並手寫一個 sort 的實現

以上是常見的幾種排序算法,首先思考一下, Array.prototype.sort() 使用了上面的那種算法喃?前端

Array.prototype.sort()

sort() 方法用原地算法對數組的元素進行排序,並返回數組。默認排序順序是在將元素轉換爲字符串,而後比較它們的UTF-16代碼單元值序列時構建的git

— MDNgithub

const array = [1, 30, 4, 21, 100000];
array.sort();
console.log(array);
// [1, 100000, 21, 30, 4]

const numbers = [4, 2, 5, 1, 3];
numbers.sort((a, b) => a - b);
console.log(numbers)
// [1, 2, 3, 4, 5]
複製代碼

V8 種的 Array.prototype.sort()

關於 Array.prototype.sort() ,ES 規範並無指定具體的算法,在 V8 引擎中, 7.0 版本以前 ,數組長度小於10時, Array.prototype.sort() 使用的是插入排序,不然用快速排序。面試

在 V8 引擎 7.0 版本以後 就捨棄了快速排序,由於它不是穩定的排序算法,在最壞狀況下,時間複雜度會降級到 O(n2)。算法

因而採用了一種混合排序的算法:TimSort數組

這種功能算法最初用於Python語言中,嚴格地說它不屬於以上10種排序算法中的任何一種,屬於一種混合排序算法:markdown

在數據量小的子數組中使用插入排序,而後再使用歸併排序將有序的子數組進行合併排序,時間複雜度爲 O(nlogn)函數

什麼是 TimSort ?

在 解答 v8 sort 源碼前,咱們先看看 TimSort 具體是如何實現的,幫助咱們閱讀源碼oop

Timsort 是 Tim Peter 在 2001 年爲 Python 語言特地創造的,主要是 基於現實數據集中存在者大量的有序元素(不須要從新排序) 。 Timsort 會遍歷全部數據,找出數據中全部有序的分區(run),而後按照必定的規則將這些分區(run)歸併爲一個。性能

具體過程爲:

  • 掃描數組,並尋找所謂的 _runs_ ,一個 run 能夠認爲是已經排序的小數組,也包括以逆向排序的,由於這些數組能夠簡單地翻轉(reverse)就成爲一個run
  • 肯定最小 run 長度,小於的 run 會經過 插入排序 歸併成長度高於最小長度的 run
  • 反覆歸併一些相鄰 run ,過程當中避免歸併長度相差很大的片斷,直至整個排序完成

如何避免歸併長度相差很大 run 呢?在 Timsort 排序過程當中,會存在一個棧用於記錄每一個 run 的起始索引位置與長度, 依次將 run 壓入棧中,若棧頂 A 、B、C 的長度

  • |C| > |B| + |A|
  • |B| > |A|

在上圖的例子中,由於 | A |> | B | ,因此B被合併到了它先後兩個runs(A、C)中較小的一個 | A | ,而後 | A | 再與 | C | 。 依據這個法則,可以儘可能使得大小相同的 run 合併,以提升性能。注意Timsort是穩定排序故只有相鄰的 run 才能歸併。

因此,對於已經排序好的數組,會以 O(n) 的時間內完成排序,由於這樣的數組將只產生單個 run ,不須要合併操做。最壞的狀況是 O(n log n) 。這樣的算法性能參數,以及 Timsort 天生的穩定性是咱們最終選擇 Timsort 而非 Quicksort 的幾個緣由。

手寫一個 Array.prototype.sort() 實現

瞭解的 Timsort 的基本思想與排序過程後,咱們手寫一個簡易版的 Timsort :

// 順序合併兩個小數組left、right 到 result
function merge(left, right) {
  let result = [],
      ileft = 0,
      iright = 0
  while(ileft < left.length && iright < right.length) {
    if(left[ileft] < right[iright]){
      result.push(left[ileft ++])
    } else {
      result.push(right[iright ++])
    }
  }
  while(ileft < left.length) {
    result.push(left[ileft ++])
  }
  while(iright < right.length) {
    result.push(right[iright ++])
  }
  return result
}

// 插入排序
function insertionSort(arr) {
    let n = arr.length;
    let preIndex, current;
    for (let i = 1; i < n; i++) {
        preIndex = i - 1;
        current = arr[i];
        while (preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex + 1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex + 1] = current;
    }
    return arr;
}

// timsort
function timsort(arr) {
    // 空數組或數組長度小於 2,直接返回
    if(!arr || arr.length < 2) return arr
    let runs = [], 
        sortedRuns = [],
        newRun = [arr[0]],
        length = arr.length
    // 劃分 run 區,並存儲到 runs 中,這裏簡單的按照升序劃分,沒有考慮降序的run
    for(let i = 1; i < length; i++) {
        if(arr[i] < arr[i - 1]) {
            runs.push(newRun)
            newRun = [arr[i]]
        } else {
            newRun.push(arr[i])
        }
        if(length - 1 === i) {
            runs.push(newRun)
            break
        }
    }
    // 因爲僅僅是升序的run,沒有涉及到run的擴充和降序的run,所以,其實這裏沒有必要使用 insertionSort 來進行 run 自身的排序
    for(let run of runs) {
        insertionSort(run)
    }
    // 合併 runs
    sortedRuns = []
    for(let run of runs) {
        sortedRuns = merge(sortedRuns, run)
    }
    return sortedRuns
}

// 測試
var numbers = [4, 2, 5, 1, 3]
timsort(numbers)
// [1, 2, 3, 4, 5]
複製代碼

簡易版的,完整的實現能夠查看 v8 array-sort 實現,下面咱們就來看一下

v8 中的 Array.prototype.sort() 源碼解讀

即 TimSort 在 v8 中的實現,具體實現步驟以下:

  1. 判斷數組長度,小於2直接返回,不排序
  2. 開始循環
  3. 找出一個有序子數組,咱們稱之爲 「run」 ,長度 currentRunLength
  4. 計算最小合併序列長度 minRunLength (這個值會根據數組長度動態變化,在32~64之間)
  5. 比較 currentRunLength 和 minRunLength ,若是 currentRunLength >= minRunLength ,不然採用插入排序補足數組長度至 minRunLength ,將 run 壓入棧 pendingRuns 中
  6. 每次有新的 run 被壓入 pendingRuns 時保證棧內任意 3 個連續的 run(run0, run1, run2)從下至上知足run0 > run1 + run2 && run1 > run2 ,不知足的話進行調整直至知足
  7. 若是剩餘子數組爲 0 ,結束循環
  8. 合併棧中全部 run,排序結束

核心源碼解讀

下面重點解讀 3 個核心函數:

  • ComputeMinRunLength :用來計算 minRunLength
  • CountAndMakeRun :計算第一個 run 的長度
  • MergeCollapse :調整 pendingRuns ,使棧長度大於 3 時,全部 run 都知足 run[n]>run[n+1]+run[n+2]run[n+1]>run2[n+2]
// 計算最小合併序列長度 minRunLength
macro ComputeMinRunLength(nArg: Smi): Smi {
  let n: Smi = nArg;
  let r: Smi = 0;  // Becomes 1 if any 1 bits are shifted off.

  assert(n >= 0);
  // 若是小於 64 ,則返回n(該值過小,沒法打擾那些奇特的東西)
  // 不然不斷除以2,獲得結果在 32~64 之間
  while (n >= 64) {
    r = r | (n & 1);
    n = n >> 1;
  }

  const minRunLength: Smi = n + r;
  assert(nArg < 64 || (32 <= minRunLength && minRunLength <= 64));
  return minRunLength;
}
複製代碼
// 計算第一個 run 的長度
macro CountAndMakeRun(implicit context: Context, sortState: SortState)(
    lowArg: Smi, high: Smi): Smi {
  assert(lowArg < high);
  // 這裏保存的纔是咱們傳入的數組數據
  const workArray = sortState.workArray;

  const low: Smi = lowArg + 1;
  if (low == high) return 1;

  let runLength: Smi = 2;

  const elementLow = UnsafeCast<JSAny>(workArray.objects[low]);
  const elementLowPred = UnsafeCast<JSAny>(workArray.objects[low - 1]);
  // 調用比對函數來比對數據
  let order = sortState.Compare(elementLow, elementLowPred);

  // TODO(szuend): Replace with "order < 0" once Torque supports it.
  // Currently the operator<(Number, Number) has return type
  // 'never' and uses two labels to branch.
  const isDescending: bool = order < 0 ? true : false;

  let previousElement: JSAny = elementLow;
  // 遍歷子數組並計算 run 的長度
  for (let idx: Smi = low + 1; idx < high; ++idx) {
    const currentElement = UnsafeCast<JSAny>(workArray.objects[idx]);
    order = sortState.Compare(currentElement, previousElement);

    if (isDescending) {
      if (order >= 0) break;
    } else {
      if (order < 0) break;
    }

    previousElement = currentElement;
    ++runLength;
  }

  if (isDescending) {
    ReverseRange(workArray, lowArg, lowArg + runLength);
  }

  return runLength;
}
複製代碼
// 調整 pendingRuns ,使棧長度大於3時,全部 run 都知足 run[n]>run[n+1]+run[n+2] 且 run[n+1]>run2[n+2]
transitioning macro MergeCollapse(context: Context, sortState: SortState) {
  const pendingRuns: FixedArray = sortState.pendingRuns;

  // Reload the stack size because MergeAt might change it.
  while (GetPendingRunsSize(sortState) > 1) {
    let n: Smi = GetPendingRunsSize(sortState) - 2;

    if (!RunInvariantEstablished(pendingRuns, n + 1) ||
        !RunInvariantEstablished(pendingRuns, n)) {
      if (GetPendingRunLength(pendingRuns, n - 1) <
          GetPendingRunLength(pendingRuns, n + 1)) {
        --n;
      }

      MergeAt(n); // 將第 n 個 run 和第 n+1 個 run 進行合併
    } else if (
        GetPendingRunLength(pendingRuns, n) <=
        GetPendingRunLength(pendingRuns, n + 1)) {
      MergeAt(n); // 將第 n 個 run 和第 n+1 個 run 進行合併
    } else {
      break;
    }
  }
}
複製代碼

天天三分鐘,進階一個前端小 tip 面試題庫 算法題庫

相關文章
相關標籤/搜索