優雅的 JavaScript 排序演算法(ES6)

RayJune發表於2018-03-24

面試官:小夥子排序演算法瞭解嗎?

回答:我能寫出來四種氣泡排序兩種選擇排序兩種插入排序兩種雜湊排序兩種歸併排序兩種堆排序四種快速排序

用我自己的方式。

前言

文中所有程式碼位於位於此程式碼倉庫中,推薦下載程式碼進行練習、推敲。


(已過期)號外:博主為 18 屆應屆生,目前狀態是前端開發補招進行時。如有內推機會,歡迎一波流帶走 :》

更多資訊,歡迎check: rayjune.me/about

另,如果覺得這些用心推敲的程式碼對你有幫助的話,歡迎 star 一下程式碼倉庫眾籌博主找到一份體面的工作,在這裡給大家遞茶了:)


P.S. 原文顯示效果更好喔:) check:rayjune.me/優雅的 JavaScr…

作者:RayJune轉載請署名,請尊重博主含辛茹苦、遍查資料、一行一行含淚碼出來的成果。參考&感謝 部分裡程式碼參考地址都已列出)

另,本文中常使用 swap 函式,在這裡提前列出來,以下就省略了。

function swap(arr, indexA, indexB) {
  [arr[indexA], arr[indexB]] = [arr[indexB], arr[indexA]];
}
複製程式碼

氣泡排序 Bubble Sort

簡明解釋

通過依次比較、交換相鄰的元素大小(按照由小到大的順序,如果符合這個順序就不用交換)。

1 次這樣的迴圈可以得到一個最大值,n - 1 次這樣的迴圈可以排序完畢

屬性

  • 穩定
  • 時間複雜度 O(n²)
  • 交換 O(n²)
  • 對即將排序完成的陣列進行排序 O(n)(但是這種情況下不如插入排序塊,請繼續看下文)

核心概念

  • 利用交換,將最大的數冒泡到最後
  • 使用快取 postion 來優化
  • 使用雙向遍歷來優化

第一版:基本實現

function bubbleSort(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    for (let j = 0; j < i; j++) {
      if (arr[j] > arr[j + 1]) {
        swap(arr, j, j + 1);
      }
    }
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort(arr));
複製程式碼

第二版:快取 pos

設定一標誌性變數 pos,用於記錄每趟排序中最後一次進行交換的位置。 由於 pos 位置之後的記錄均已交換到位,故在進行下一趟排序時只要掃描到 pos 位置即可

function bubbleSort2(arr) {
  let i = arr.length - 1;

  while (i > 0) {
    let pos = 0;

    for (let j = 0; j < i; j++) {
      if (arr[j] > arr[j + 1]) {
        pos = j;
        swap(arr, j, j + 1);
      }
    }
    i = pos;
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort2(arr));
複製程式碼

第三版:雙向遍歷

傳統氣泡排序中每一趟排序操作只能找到一個最大值或最小值, 我們可以 在每趟排序中進行正向和反向兩遍冒泡 , 一次可以得到兩個最終值(最大和最小) , 從而使外排序趟數幾乎減少了一半

function bubbleSort3(arr) {
  let start = 0;
  let end = arr.length - 1;

  while (start < end) {
    for (let i = start; i < end; i++) {
      if (arr[i] > arr[i + 1]) {
        swap(arr, i, i + 1);
      }
    }
    end -= 1;
    for (let i = end; i > start; i--) {
      if (arr[i - 1] > arr[i]) {
        swap(arr, i - 1, i);
      }
    }
    start += 1;
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort3(arr));
複製程式碼

第四版:結合 2&3

前兩種優化方式(快取 pos、雙向遍歷)的結合:

function bubbleSort4(arr) {
  let start = 0;
  let end = arr.length - 1;

  while (start < end) {
    let endPos = 0;
    let startPos = 0;

    for (let i = start; i < end; i++) {
      if (arr[i] > arr[i + 1]) {
        endPos = i;
        swap(arr, i, i + 1);
      }
    }
    end = endPos;
    for (let i = end; i > start; i--) {
      if (arr[i - 1] > arr[i]) {
        startPos = i;
        swap(arr, i - 1, i);
      }
    }
    start = startPos;
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort4(arr));
複製程式碼

螞蟻金服面試

來自於螞蟻金服的一道面試題:

對於氣泡排序來說,能不能傳入第二個引數(引數為函式),來控制升序和降序?(聯想一下 array.sort()

function bubbleSort(arr, compareFunc) {
  for (let i = arr.length - 1; i > 0; i--) {
    for (let j = 0; j < i; j++) {
      if (compareFunc(arr[j], arr[j + 1]) > 0) {
        swap(arr, j, j + 1);
      }
    }
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(bubbleSort(arr, (a, b) => a - b));
console.log(bubbleSort(arr, (a, b) => b - a));
複製程式碼

選擇排序 Selection Sort

簡明解釋

每一次內迴圈遍歷尋找最小的數,記錄下 minIndex,並在這次內迴圈結束後交換 minIndexi 的位置

重複這樣的迴圈 n - 1 次即得到結果。

屬性

  • 不穩定
  • Θ(n²) 無論什麼輸入,均為 Θ(n²)
  • Θ(n) 交換: 注意,這裡只有 n 次的交換,選擇排序的唯一優點*

關於 Θ(n) swaps:

Selection sort has the property of minimizing the number of swaps. In applications where the cost of swapping items is high, selection sort very well may be the algorithm of choice.

可見即使是我們覺得最慢的選擇排序,也有它的用武之地

核心概念

  • “可預測”的時間複雜度,什麼進來都是 O(n²),但不穩定,唯一的優點是減少了 swap 次數

第一版:基本實現

function selectionSort(arr) {
  for (let i = 0, len = arr.length; i < len - 1; i++) {
    let minIndex = i;

    for (let j = i + 1; j < len; j++) {
      if (arr[j] < arr[minIndex]) {
        minIndex = j;
      }
    }
    if (i !== minIndex) {
      swap(arr, i, minIndex);
    }
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(selectionSort(arr));
複製程式碼

第二版:找到最大值

如果你想在每次內迴圈中找到最大值並把其交換到陣列的末尾(相比較 minIndex 有點麻煩),以下是實現的程式碼:

function selectionSort2(arr) {
  for (let i = arr.length - 1; i > 0; i--) {
    let maxIndex = i;

    for (let j = i - 1; j >= 0; j--) {
      if (arr[j] > arr[maxIndex]) {
        maxIndex = j;
      }
    }
    if (i !== maxIndex) {
      swap(arr, i, maxIndex);
    }
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(selectionSort2(arr));
複製程式碼

插入排序 Insertion Sort

簡明解釋

預設 a[0] 為已排序陣列中的元素arr[1] 開始逐漸往已排序陣列中插入元素從後往前一個個比較,如果待插入元素小於已排序元素,則已排序元素往後移動一位,直到待插入元素找到合適的位置並插入已排序陣列。

經過 n - 1 次這樣的迴圈插入後排序完畢。

屬性

  • 穩定
  • 適合場景:對快要排序完成的陣列時間複雜度為 O(n)
  • 非常低的開銷
  • 時間複雜度 O(n²)

由於它的優點(自適應,低開銷,穩定,幾乎排序時的O(n)時間),插入排序通常用作遞迴基本情況(當問題規模較小時)針對較高開銷分而治之排序演算法, 如希爾排序快速排序

核心概念

  • 高效能(特別是接近排序完畢時的陣列),低開銷,且穩定
  • 利用二分查詢來優化

第一版:基本實現

function insertionSort(arr) {
  for (let i = 1, len = arr.length; i < len; i++) {
    const temp = arr[i];
    let preIndex = i - 1;

    while (arr[preIndex] > temp) {
      arr[preIndex + 1] = arr[preIndex];
      preIndex -= 1;
    }
    arr[preIndex + 1] = temp;
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(insertionSort(arr));
複製程式碼

二分查詢演算法

因為對於插入排序的優化方法是二分查詢優化,這裡補充一下二分查詢的演算法的實現。

核心概念是:折半

function binarySearch(arr, value) {
  let min = 0;
  let max = arr.length - 1;
  
  while (min <= max) {
    const mid = Math.floor((min + max) / 2);

    if (arr[mid] === value) {
      return mid;
    } else if (arr[mid] > value) {
      max = mid - 1;
    } else {
      min = mid + 1;
    }
  }

  return 'Not Found';
}

// test
const arr = [1, 2, 3];
console.log(binarySearch(arr, 2));  // 1
console.log(binarySearch(arr, 4));  // Not Found
複製程式碼

第二版:使用二分查詢

首先把二分查詢演算法做一點小修改,以適應我們的插入排序:

function binarySearch(arr, maxIndex, value) {
  let min = 0;
  let max = maxIndex;
  
  while (min <= max) {
    const mid = Math.floor((min + max) / 2);

    if (arr[mid] <= value) {
      min = mid + 1;
    } else {
      max = mid - 1;
    }
  }

  return min;
}
複製程式碼

然後在查詢插入位置時使用二分查詢的方式來優化效能:

function insertionSort2(arr) {
  for (let i = 1, len = arr.length; i < len; i++) {
    const temp = arr[i];
    const insertIndex = binarySearch(arr, i - 1, arr[i]);

    for (let preIndex = i - 1; preIndex >= insertIndex; preIndex--) {
      arr[preIndex + 1] = arr[preIndex];
    }
    arr[insertIndex] = temp;
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(insertionSort2(arr));
複製程式碼

希爾排序 Shell Sort

簡明解釋

希爾排序是插入排序的改進版,它克服了插入排序只能移動一個相鄰位置的缺陷(希爾排序可以一次移動 gap 個距離),利用了插入排序在排序幾乎已經排序好的陣列的非常快的優點

使用可以動態定義的 gap 來漸進式排序,先排序距離較遠的元素,再逐漸遞進,而實際上排序中元素最終位置距離初始位置遠的概率是很大的,所以希爾排序大大提升了效能(尤其是 reverse 的時候非常快,想象一下這時候氣泡排序和插入排序的速度)。

而且希爾排序不僅效率較高(比冒泡和插入高),它的程式碼相對要簡短,低開銷(繼承插入排序的優點),追求這些特點(效率要求過得去就好,程式碼簡短,開銷低,且資料量較小)的時候希爾排序是好的 O(n·log(n)) 演算法的替代品

總而言之:希爾排序的效能優化來自增量佇列的輸入gap 的設定

屬性

  • 不穩定
  • 在快要排序完成的陣列有 O(n·log(n)) 的時間複雜度(並且它對於反轉陣列的速度非常快)
  • O(n^3/2) time as shown (想要了解更多細節,請查閱 wikipedia Shellsort

關於不穩定:

我們知道, 單次直接插入排序是穩定的,它不會改變相同元素之間的相對順序,但在多次不同的插入排序過程中, 相同的元素可能在各自的插入排序中移動,可能導致相同元素相對順序發生變化。因此, 希爾排序並不穩定

關於 worse-case time 有一點複雜:

The worse-case time complexity of shell sort depends on the increment sequence. For the increments 1 4 13 40 121…, which is what is used here, the time complexity is O(n3/2). For other increments, time complexity is known to be O(n4/3) and even O(n·log2(n)).

核心概念

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  1. 插入排序在對幾乎已經排好序的資料操作時,效率高,即可以達到 O(n) 的效率
  2. 但插入排序一般來說是低效的,因為插入排序每次只能將資料移動一位

其中 gap(增量)的選擇是希爾排序的重要部分。只要最終 gap 為 1 任何 gap 序列都可以工作。演算法最開始以一定的 gap 進行排序。然後會繼續以一定 gap 進行排序,直到 gap = 1 時,演算法變為插入排序

Donald Shell 最初建議 gap 選擇為 n / 2 並且對 gap 取半直到 gap 達到 1 。雖然這樣取可以比 O(n²) 類的演算法(插入排序、氣泡排序)更好,但這樣仍然有減少平均時間和最差時間的餘地。 (關於優化 gap 的細節涉及到複雜的數學知識,我們這裡不做深究,詳細可以參考 wikipedia 上的頁面

第一版:基本實現

Donald Shell 的最初建議(gap = n / 2)版程式碼(方便理解):

function shellSort(arr) {
  const len = arr.length;
  let gap = Math.floor(len / 2);

  while (gap > 0) {
    // 注意下面這段 for 迴圈和插入排序極為相似
    for (let i = gap; i < len; i++) {
      const temp = arr[i];
      let preIndex = i - gap;

      while (arr[preIndex] > temp) {
        arr[preIndex + gap] = arr[preIndex];
        preIndex -= gap;
      }
      arr[preIndex + gap] = temp;
    }
    gap = Math.floor(gap / 2);
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(shellSort(arr));
複製程式碼

第二版:Knuth's increment sequence

常見的、易生成的、優化 gap 的序列方法(來自 Algorithms (4th Edition) ,有些更快的方法但序列不容易生成,因為用到了比較深奧的數學公式):

function shellSort(arr) {
  const len = arr.length;
  let gap = 1;

  while (gap < len / 3) {
    gap = gap * 3 + 1;
  }
  while (gap > 0) {
    for (let i = gap; i < len; i++) {
      const temp = arr[i];
      let preIndex = i - gap;

      while (arr[preIndex] > temp) {
        arr[preIndex + gap] = arr[preIndex];
        preIndex -= gap;
      }
      arr[preIndex + gap] = temp;
    }
    gap = Math.floor(gap / 2);
  }

  return arr;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(shellSort(arr));
複製程式碼

歸併排序 Merge Sort

簡明解釋

歸併排序使用分而治之的思想,以折半的方式來遞迴/迭代排序元素,利用空間來換時間,做到了時間複雜度 O(n·log(n)) 的同時保持了穩定。

這讓它在一些更考慮排序效率和穩定性,次考慮儲存空間的場合非常適用(如資料庫內排序,和堆排序相比,歸併排序的穩定是優點)。並且歸併排序非常適合於連結串列排序

屬性

  • 穩定 (O(n·log(n)) 時間複雜度的排序演算法中,歸併排序是唯一穩定的)
  • 時間複雜度 O(n·log(n))
  • 對於陣列需要 Θ(n) 的額外空間 注意:歸併排序需要額外的空間,這是它的不完美之處
  • 對於連結串列需要 O(log(n)) 的額外空間,所以歸併排序非常適合列表的排序
  • Does not require random access to data 因為這個特點,歸併排序很適合用來排序列表

核心概念

  • 分而治之的思想
  • 空間換時間,並且穩定保持穩定性這一點是它的亮點
  • 二分思想

第一版:基本實現

以迭代的方式來實現(但要注意防止函式呼叫過深導致 JavaScript 的執行棧溢位):

function mergeSort(arr) {
  const len = arr.length;

  if (len < 2) { return arr; }

  const mid = Math.floor(len / 2);
  const left = arr.slice(0, mid);
  const right = arr.slice(mid);

  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  const result = [];

  while (left.length > 0 && right.length > 0) {
    result.push(left[0] <= right[0] ? left.shift() : right.shift());
  }

  return result.concat(left, right);
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(mergeSort(arr));
複製程式碼

第二版:空間優化

array.splice 取代 array.slice,減少一半的空間消耗。

function mergeSort2(arr) {
  const len = arr.length;

  if (len < 2) { return arr; }

  const mid = Math.floor(len / 2);
  const left = arr.splice(0, mid);
  const right = arr;

  return merge(mergeSort(left), mergeSort(right));
}

function merge(left, right) {
  const result = [];

  while (left.length > 0 && right.length > 0) {
    result.push(left[0] <= right[0] ? left.shift() : right.shift());
  }

  return result.concat(left, right);
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(mergeSort2(arr));
複製程式碼

堆排序 Heap Sort

簡明解釋

堆排序可以認為是選擇排序的改進版,像選擇排序一樣將輸入劃分為已排序和待排序

不一樣的是堆排序利用堆這種近似完全二叉樹的良好的資料結構來實現排序,本質上使用了二分的思想

  1. 先將所有的資料堆化
  2. 然後移動 arr[0] 到陣列末尾(已排序區域)
  3. 再重新堆化,依次這樣迴圈來排序。

利用堆這種良好的資料結構,它在擁有良好的可預測性的同時(不管輸入什麼都是 O(n·log(n)) 時間複雜度),但它的缺點也有:即不穩定,而且 O(n·log(n)) 的平均效率決定了它的效率不如快速排序。適用於資料庫內引擎排序(需要這樣的可預測性效能)。

屬性

  • 不穩定
  • O(n·log(n)) time

核心概念

  • 利用良好的資料結構——堆,來排序
  • 二分的思想
  • 選擇排序的改進版,繼承了"可預測性"(什麼資料輸入都為 O(n·log(n) time)

第一版:基本實現

function heapSort(arr) {
  let size = arr.length;

  // 初始化堆,i 從最後一個父節點開始調整,直到節點均調整完畢 
  for (let i = Math.floor(size / 2) - 1; i >= 0; i--) {
    heapify(arr, i, size);
  }
  // 堆排序:先將第一個元素和已拍好元素前一位作交換,再重新調整,直到排序完畢
  for (let i = size - 1; i > 0; i--) {
    swap(arr, 0, i);
    size -= 1;
    heapify(arr, 0, size);
  }

  return arr;
}

function heapify(arr, index, size) {
  let largest = index;
  let left = 2 * index + 1;
  let right = 2 * index + 2;

  if (left < size && arr[left] > arr[largest]) {
    largest = left;
  }
  if (right < size && arr[right] > arr[largest]) {
    largest = right;
  }
  if (largest !== index) {
    swap(arr, index, largest);
    heapify(arr, largest, size);
  }
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(heapSort(arr));
複製程式碼

維基上給出的另一個方法

wikipedia 上給出的方法於第一版的區別在於維護堆性質時採用的方式不同,本質是一樣的:

function heapSort(arr) {
  const size = arr.length;

  // 初始化 heap,i 從最後一個父節點開始調整,直到節點均調整完畢 
  for (let i = Math.floor(size / 2) - 1; i >= 0; i--) {
    heapify(i, size);
  }
  // 堆排序:先將第一個元素和已拍好元素前一位作交換,再重新調整,直到排序完畢
  for (let i = size - 1; i > 0; i--) {
    swap(arr, 0, i);
    heapify(0, i);
  }

  return arr;
}

function heapify(start, end) {
  // 建立父節點下標和子節點下標
  const dad = start;
  let son = dad * 2 + 1;

  if (son >= end) { return 0; }

  if (son + 1 < end && arr[son] < arr[son + 1]){
    son += 1;
  }
  if (arr[dad] <= arr[son]) {
    swap(arr, dad, son);
    heapify(son, end);
  }

  return 0;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(heapSort(arr));
複製程式碼

快速排序 Quick Sort

簡明解釋

  1. 從數列中挑出一個元素,稱為"基準"(pivot),
  2. 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(相同的數可以到任何一邊)。在這個分割槽結束之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作
  3. 遞迴地(recursively)把小於基準值元素的子數列和大於基準值元素的子數列排序。

屬性

  • 不穩定
  • O(n²) time, 但是通常都是 O(n·log(n)) time (或者更快)
  • O(log(n)) extra space

When implemented well, it can be about two or three times faster than its main competitors, merge sort and heap sort

核心概念

  • 使用了分而治之的思想

第一版:基本實現

function quickSort(arr) {
  const pivot = arr[0];
  const left = [];
  const right = [];
  
  if (arr.length < 2) { return arr; }

  for (let i = 1, len = arr.length; i < len; i++) {
    arr[i] < pivot ? left.push(arr[i]) : right.push(arr[i]);
  }

  return quickSort(left).concat([pivot], quickSort(right));
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(quickSort(arr));
複製程式碼

第二版:函數語言程式設計

函數語言程式設計:結構清晰,一目瞭然。

function quickSort2(arr) {
  const pivot = arr.shift();
  const left = [];
  const right = [];

  if (arr.length < 2) { return arr; }

  arr.forEach((element) => {
    element < pivot ? left.push(element) : right.push(element);
  });

  return quickSort2(left).concat([pivot], quickSort2(right));
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(quickSort2(arr));
複製程式碼

第三版:in-place

等等,有沒有覺得第一、二版中的程式碼雖然看起來簡潔,但是卻對空間消耗很大呢?

由此有了 in-place 版本:

function quickSort3(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    const pivot = partition(arr, left, right);

    quickSort3(arr, left, pivot - 1);
    quickSort3(arr, pivot + 1, right);
  }
  return arr;
}

function partition (arr, left ,right) {
  let pivot = left; // 以第一個元素為 pivot

  for (let i = left + 1; i <= right; i++) {
    if (arr[i] < arr[left]) { 
      swap(arr, i, pivot);
      pivot += 1;
    }
  }
  swap(arr, left, pivot); //將 pivot 值移至中間
  
  return pivot;
}

// test
const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(quickSort3(arr));
複製程式碼

第四版:關於 pivot 的選取

這一版的亮點是 pivot 的選取,不再是簡單的取 arr[0],而是:

const pivot = left + Math.ceil((right - left) * 0.5)
複製程式碼

非常感謝評論區的大神 @Chris_dong 的解釋:

const pivot = left + Math.ceil((right - left) * 0.5) => (去掉MAth.ceil是不是很好理解) left + (right - left) * 0.5 => (right + left) * 0.5

看到真相的我眼淚掉下來,原來是取中間值。。。

由此有了以下版本:

function quickSort4(arr, left = 0, right = arr.length - 1) {
  if (left < right) {
    // const pivot = left + Math.ceil((right - left) * 0.5);
    const pivot = Math.floor((right + left) / 2);
    const newPivot = partition(arr, pivot, left, right);

    quickSort4(arr, left, newPivot - 1);
    quickSort4(arr, newPivot + 1, right);
  }

  return arr;
}

function partition(arr, pivot, left, right) {
  const pivotValue = arr[pivot];
  let newPivot = left;

  swap(arr, pivot, right);
  for (let i = left; i < right; i++) {
    if (arr[i] < pivotValue) {
      swap(arr, i, newPivot);
      newPivot += 1;
    }
  }
  swap(arr, right, newPivot);

  return newPivot;
}

const arr = [91, 60, 96, 7, 35, 65, 10, 65, 9, 30, 20, 31, 77, 81, 24];
console.log(quickSort4(arr));
複製程式碼

總結 & 答疑

提出幾個問題,可以當做自我檢測:

  • 資料幾乎快排序完成時?

插入排序不解釋

  • 資料量小,對效率要求不高,程式碼簡單時?

效能大小:希爾排序 > 插入排序 > 氣泡排序 > 選擇排序

  • 資料量大,要求穩定的效率(不會像快速排序一樣有 O(n²) 的情況)(如資料庫中)?

堆排序

  • 資料量大,要求效率高,而且要穩定?

歸併排序

  • 資料量大,要求最好的平均效率?

效能大小:快速排序 > 堆排序 > 歸併排序

因為雖然堆排序做到了 O(n·log(n),而快速排序的最差情況是 O(n²),但是快速排序的絕大部分時間的效率比 O(n·log(n) 還要快,所以快速排序真的無愧於它的名字。(十分快速)

  • 選擇排序絕對沒用嗎?

選擇排序只需要 O(n) 次交換,這一點它完爆氣泡排序。


答疑:

  • 博主你的程式碼從哪裡抄的?

都是博主含辛茹苦、遍查資料、一行一行含淚認真碼出來的。參考&感謝 部分裡列出了所有來源地址:)

  • 為什麼不用 ES5 寫呢?

實際上這篇文章繼承於優雅的 JavaScript 排序演算法 。這一版是上一般的姐妹版(解釋精簡,使用 ES6 使程式碼更精簡),若想參考英文引用、ES5 程式碼、過程詳細解釋可以參考第一版

ES6 是為了更強大的表現力,從而讓我們更加關注於演算法的內在,不被一些邊邊角角所束縛。

附錄:程式碼風格

博主一向認為是有【程式碼品味】這種東西存在的,可以從之前的這篇文章從 shuffle 看程式碼品味一窺端倪。

再次表達一下自己的觀點:

  • 軟體開發不是教條
  • 程式碼品味沒有高低

但是追求的最終目的是一致的:好讀又簡潔,穩定易維護

為了這個目標我做了這些努力:

  • 注重可讀性變數名*:如 preIndex, temp, size
  • 一目瞭然的函式結構
    function () {
      const/let ...;

      function body

      return...;
    };
  • 在計算 len / 2 的取整時為了可讀性選擇了 Math.floor(len / 2),沒有選擇 len >> 1parseInt(len / 2, 10)
  • 注意區分 forwhile 的使用場景,具體可以看這個問題的答案:
    https://stackoverflow.com/questions/39969145/while-loops-vs-for-loops-in-JavaScript;
  • 為了簡單直觀,未使用 Array.isArray()Object.prototype.toString.call()typeOf, instanceOf 來檢查 arr 是不是陣列型別,預設 arr 就是陣列型別;
  • 使用三元運算子 ( ? : ) 來減少 if 的巢狀,提高程式碼可讀性
  • 自增(++)和自減(--)運算子使用 +=-= 代替 (for 中的最後一個條件除外);
  • 使用 ES6 中的預設引數方式(快速排序中)簡化程式碼,將關鍵邏輯突出;
  • Eslint + Airbnb 全域性控制程式碼風格;
  • 在風格之外加上自己的喜好,比如用 function 宣告函式,具體原因見:從 shuffle 看程式碼品味

這是我的品味,你的呢:)

引用 & 感謝

  • Wikipedia about Sorting Algorithms (English & Chinese),參考了其中的概念解釋和程式碼實現(進行修改),:
    https://en.wikipedia.org/wiki/Sorting_algorithm
  • Sorting Algorithms visualization (English),強烈推薦看對理解排序演算法執行的過程很有幫助:
    https://www.toptal.com/developers/sorting-algorithms
  • stackOverFlow Sorting Algorithms 參考其中排序演算法相關的答案來輔助理解:
    https://www.quora.com/Why-do-we-need-so-many-sorting-algorithms
    https://www.quora.com/Why-is-shell-sort-faster-than-insertion-sort-and-bubble-sort
    https://www.quora.com/Why-is-heap-sort-used
    還有一些就不一一列舉了...
  • damonare's demonstrate (Chinese),參考了其中氣泡排序 1,2,3;參考其中的動圖來輔助理解; 選擇排序;歸併排序的實現;並進行修改:
    https://github.com/damonare/Sorts
  • 參考其中的
    quick sort in-place edition https://gist.github.com/paullewis/1981455,並進行修改
  • hustcc's gitbook (Chinese),參考了其中的快速排序,希爾排序的實現,並進行修改
    https://sort.hust.cc/6.quickSort.html

相關文章