**超詳細的**10種排序演算法原理及 JS 實現

迪斯馬斯克發表於2019-04-08

簡介

本文介紹了常見的 10 種排序演算法的原理基本實現常見的優化實現,並有(個人認為)足夠詳細的程式碼註釋
實在是居家工作,面試筆試必備良藥。

這裡只給出基於其原理的一般實現,很多演算法都有邏輯更復雜的或程式碼量更少的精簡版,像遍歷的改成遞迴的,兩個函式實現的改成一個函式等等,就不再提及了。

夠詳細了!傻子都能看懂!如果不懂,多看幾遍!

前幾天在微博上看到一個視訊:用音訊演示15種排序演算法,可以看一下

所有動圖均來自《十大經典排序演算法總結(JavaScript 描述)》

分類

  • 氣泡排序
  • 選擇排序
    • 普通選擇排序
    • 堆排序
  • 插入排序
    • 普通插入排序
    • 希爾排序
  • 快速排序
  • 歸併排序
  • 計數排序
  • 桶排序
  • 基數排序

另一種分類方式是根據是否為“比較排序”。

  • 常見比較排序:
    • 氣泡排序
    • 選擇排序
    • 插入排序
    • 快速排序
    • 歸併排序
  • 常見非比較排序:
    • 計數排序
    • 基數排序
    • 桶排序

複雜度和穩定性

平均時間複雜度 最好 最壞 空間複雜度 穩定性
氣泡排序 O(n^2) O(n) O(n^2) O(1) 穩定
選擇排序 O(n^2) O(n^2) O(n^2) O(1) 不穩定
堆排序 O(n logn) O(n logn) O(n logn) O(1) 不穩定
插入排序 O(n^2) O(n) O(n^2) O(1) 穩定
希爾排序 O(n logn) O(n log^2 n) O(n log^2 n) O(1) 不穩定
快速排序 O(n logn) O(n logn) O(n^2) O(logn) 不穩定
歸併排序 O(n logn) O(n logn) O(n logn) O(n) 穩定
計數排序 O(n+k) O(n+k) O(n+k) O(k) 穩定
桶排序 O(n+k) O(n+k) O(n^2) O(n+k) 穩定
基數排序 O(n*k) O(n*k) O(n*k) O(n+k) 穩定

氣泡排序 Bubble Sort

一般實現

已排序元素將放在陣列尾部

大致流程:

  1. 從第一個元素開始,比較每兩個相鄰元素,如果前者大,就交換位置
  2. 每次遍歷結束,能夠找到該次遍歷過的元素中的最大值
  3. 如果還有沒排序過的元素,繼續1

演示圖:

氣泡排序演示圖

function bubbleSort(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    for (let j = 0; j < arr.length -1 - i; j++) {
      if (arr[j] > arr[j+1]) swap(arr, j ,j+1)
    }
  }
}
// 後面還會多次用到,就不再寫出來了
function swap(arr, n, m) {
  [arr[n], arr[m]] = [arr[m], arr[n]]
}
複製程式碼

有優化空間,主要從兩方面進行優化:

  1. 減少外層遍歷次數
  2. 讓每次遍歷能找到兩個極值

優化1

檢查某次內層遍歷是否發生交換

如果沒有發生交換,說明已經排序完成,就算外層迴圈還沒有執行完 length-1 次也可以直接 break

function bubbleSort1(arr) {
  for (let i = 0; i < arr.length - 1; i++) {
    // 外層迴圈初始值為 false,沒有發生交換
    let has_exchanged = false
    for (let j = 0; j < arr.length - i - 1; j++) {
      if (arr[j] > arr[j + 1]) {
        swap(arr, j ,j+1)
        has_exchanged = true
      }
    }
    // 內層迴圈結束判斷一下是否發生了交換
    if (!has_exchanged) break
  }
  return arr
}
複製程式碼

優化2

記錄內層遍歷最後一次發生交換的位置,下一次外層遍歷只需要到這個位置就可以了。

那麼外層遍歷就不能用 for 了,因為每次遍歷的結束位置可能會發生改變。

function bubbleSort2(arr) {
  // 遍歷結束位置的初始值為陣列尾,並逐漸向陣列頭部逼近
  let high = arr.length - 1
  while (high > 0) {
    // 本次內層遍歷發生交換的位置的初始值
    let position = 0
    for (let j = 0; j < high; j++) {
      if (arr[j] > arr[j + 1]) {
        swap(arr, j, j + 1)
        // 如果發生了交換,更新 position
        position = j
      }
    }
    // 下次遍歷只需要到 position 的位置即可
    high = position
  }
  return arr
}
複製程式碼

優化3

雙向遍歷,每次迴圈能找到一個最大值和一個最小值。

前後各設定一個索引,向中間的未排序部分逼近

function bubbleSort3(arr) {
  let low = 0, high = arr.length - 1
  while (low < high) {
    // 正向遍歷找最大
    for (let i = low; i <= high; i++) if (arr[i] > arr[i + 1]) swap(arr, i, i + 1)
    high--
    // 反向遍歷找最小
    for (let j = high; j >= low; j--) if (arr[j] < arr[j - 1]) swap(arr, j, j - 1)
    low++
  }
  return arr
}
複製程式碼

選擇排序 Selection Sort

每次遍歷選擇最小。

排序後的元素將放在陣列前部

大致流程:

  1. 取出未排序部分的第一個元素,遍歷該元素之後的部分並比較大小。對於第一次遍歷,就是取出第一個元素
  2. 如果有更小的,與該元素交換位置
  3. 每次遍歷都能找出剩餘元素中的最小值並放在已排序部分的最後

並不是倒著的氣泡排序。氣泡排序是比較相鄰的兩個元素

演示圖:

選擇排序演示圖

function selectionSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    let min_index = i
    // 遍歷後面的部分,尋找更小值
    for (let j = i + 1; j < arr.length; j++) {
      // 如果有,更新min_index
      if (arr[j] < arr[i]) min_index = j
    }
    swap(arr, i, min_index)
  }
  return arr
}
複製程式碼

堆排序 HeapSort

使用堆的概念實現的選擇排序。

首先,關於堆:

  1. 堆是樹的一種。當堆的父節點都大於,或者都小於子節點時,分別稱為最大堆最小堆
  2. 可以用陣列來表示樹(堆)。從0開始,以陣列的第 index 個元素為堆的父節點,其左右子節點分別為陣列的第 2*index+12*index+2 個元素

已排序元素將放在陣列尾部

大致流程:

  1. 建最大堆:把陣列整理為最大堆的順序,那麼堆的根節點,或者說陣列的第一個元素,就是最大的值
  2. 排序:把最大值與未排序部分的最後一個元素交換,剩餘的部分繼續調整為最大堆。每次建堆都能找到剩餘元素中最大的一個

注意:

  1. 第一次建堆時,只需要遍歷陣列左側一半元素就夠了,並且要從中點向左側倒序遍歷,這樣才能保證把最大的元素移動到陣列頭部
  2. 排序時,當然就需要遍歷陣列裡所有元素了

演示圖:

堆排序演示圖

// 排序
function heapSort(arr) {
  var arr_length = arr.length
  if (arr_length <= 1) return arr
  // 1. 建最大堆
  // 遍歷一半元素就夠了
  // 必須從中點開始向左遍歷,這樣才能保證把最大的元素移動到根節點
  for (var middle = Math.floor(arr_length / 2); middle >= 0; middle--) maxHeapify(arr, middle, arr_length)
  // 2. 排序,遍歷所有元素
  for (var j = arr_length; j >= 1; j--) {
    // 2.1. 把最大的根元素與最後一個元素交換
    swap(arr, 0, j - 1)
    // 2.2. 剩餘的元素繼續建最大堆
    maxHeapify(arr, 0, j - 2)
  }
  return arr
}
// 建最大堆
function maxHeapify(arr, middle_index, length) {
  // 1. 假設父節點位置的值最大
  var largest_index = middle_index
  // 2. 計算左右節點位置
  var left_index = 2 * middle_index + 1,
    right_index = 2 * middle_index + 2
  // 3. 判斷父節點是否最大
  // 如果沒有超出陣列長度,並且子節點比父節點大,那麼修改最大節點的索引
  // 左邊更大
  if (left_index <= length && arr[left_index] > arr[largest_index]) largest_index = left_index
  // 右邊更大
  if (right_index <= length && arr[right_index] > arr[largest_index]) largest_index = right_index
  // 4. 如果 largest_index 發生了更新,那麼交換父子位置,遞迴計算
  if (largest_index !== middle_index) {
    swap(arr, middle_index, largest_index)
    // 因為這時一個較大的元素提到了前面,一個較小的元素移到了後面
    // 小元素的新位置之後可能還有比它更大的,需要遞迴
    maxHeapify(arr, largest_index, length)
  }
}
複製程式碼

插入排序 Insertion Sort

一般實現

已排序元素將放在陣列前部

大致流程:

  1. 取未排序部分的第一個元素。第一次遍歷時,將第一個元素作為已排序元素,從第二個元素開始取
  2. 遍歷前面的已排序元素,並與這個未排序元素比較大小,找到合適的位置插入
  3. 繼續執行1

第一種理解方式,也就是一般的實現原理:

在上面的第2步中,遍歷已排序元素時,如果該未排序元素仍然小於當前比較的已排序元素,就把前一個已排序元素的值賦給後一個位置上的元素,也就是產生了兩個相鄰的重複元素。
這樣一來,在比較到最後,找到合適的位置時,用該未排序元素給兩個重複元素中合適的那一個賦值,覆蓋掉一個,排序就完成了。

敘述可能不夠清楚,看後面的程式碼就是了。Talk is hard, show you some codes

和選擇排序好像有一點類似的地方:

  • 選擇排序,先找合適的元素,然後直接放到已排序部分
  • 插入排序,先按順序取元素,再去已排序部分裡找合適的位置

第二種理解方式:

在前面的第2步中,相當於把已排序部分末尾新增一個元素,並且執行一次氣泡排序。 因為前面的陣列是已排序的,所以冒泡只需要遍歷一次就可以給新的元素找到正確的位置。

但是以這種方式實現的程式碼無法使用二分法進行優化。

那麼是不是說明,氣泡排序的優化方法可以用在這裡?
並不是。因為氣泡排序主要從兩方面進行優化:

  1. 減少外層遍歷次數
  2. 增加每次遍歷找到的極值個數

而這裡的冒泡只有一次,並且也不是找極值。

演示圖:

插入排序演示圖

// 按照第一種理解方式的實現,即一般的實現
function insertionSort(arr) {
  for (let index = 1; index < arr.length; index++) {
    // 取出一個未排序元素
    let current_ele = arr[index]
    // 已排序元素的最後一個的位置
    let ordered_index = index - 1
    // 前面的元素更大,並且還沒遍歷完
    while (arr[ordered_index] >= current_ele && ordered_index >= 0) {
      // 使用前面的值覆蓋當前的值
      arr[ordered_index + 1] = arr[ordered_index]
      // 向前移動一個位置
      ordered_index--
    }
    // 遍歷完成,前面的元素都比當前元素小,把未排序元素賦值進去
    arr[ordered_index + 1] = current_ele
  }
  return arr
}
// 按照第二種理解方式的實現
function insertionSort(arr) {
  for (let i = 0; i < arr.length; i++) {
    // 對前面的已排序陣列和新選出來的元素執行一趟氣泡排序
    for (let j = i + 1; j >= 0; j--) if (arr[j] < arr[j - 1]) swap(arr, j, j - 1)
  }
  return arr
}
複製程式碼

一個意外的弱智發現:while(a&&b){}while(a){ if(b){} } 不等價。。。

優化

使用二分查詢。

遍歷已排序部分時,不再是按順序挨個比較,而是比較中位數。

function binaryInsertionSort(array) {
  for (let i = 1; i < array.length; i++) {
    // 未排序部分的第1個
    let current_ele = array[i]
    // 已排序部分的第1個和最後1個
    let left = 0, right = i - 1
    // 先找位置
    while (left <= right) {
      // 不再是從最後一個位置開始向前每個都比較,而是比較中間的元素
      let middle = parseInt((left + right) / 2)
      if (current_ele < array[middle]) right = middle - 1
      else left = middle + 1
    }
    // while結束,已經找到了一個大於或等於當前元素的位置 left
    // 再修改陣列:把 left-i 之間的元素向後移動一個位置
    for (let j = i - 1; j >= left; j--) array[j + 1] = array[j]
    // 插入當前元素
    array[left] = current_ele
  }
  return array
}
複製程式碼

插入排序使用的二分查詢二分查詢函式顯然不同。

因為兩者的目的不相同。
二分查詢函式需要返回“存在”或“不存在”;而插入排序中的二分查詢,關注的不是存在與否,而是“位置應該在哪裡”,不管存在不存在,都要返回一個位置。

希爾排序 Shell Sort

也叫縮小增量排序,是插入排序的增強版。
不直接對整個陣列執行插入排序,而是先分組,對每個組的元素執行插入排序,使陣列大致有序,逐步提高這個“大致”的精確度,也就是減少分組的數量,直到最後只有一組。

指定一個增量 gap,對陣列分組,使得每相距 gap-1 的元素為一組,共分成 gap 組,對每組執行插入排序。逐步縮小 gap 的大小並繼續執行插入排序,直到為1,也就是整個陣列作為一組,對整個陣列執行插入排序。

可以發現,不管增量 gap 初始值設定為多少,最後總會對整個陣列進行一次插入排序,也就是說 gap 對排序結果是沒有影響的,只是影響了演算法效率。
至於 gap 如何取值最好,還沒有研究過。期待大家留言交流。(只是隨便一說,我看這個單純就是為了面試。。)

大致流程:

  1. 共三層迴圈,外層迴圈用來逐步減少 gap 的值
  2. 中層與內層兩層迴圈基本上就是插入排序,細節上的不同直接看程式碼就好,不再贅述

演示圖:

希爾排序演示圖

function shellSort(arr) {
  // 外層迴圈逐步縮小增量 gap 的值
  for (let gap = 5; gap > 0; gap = Math.floor(gap / 2)) {
    // 中層和內層是插入排序
    // 普通插入排序從第1個元素開始,這裡分組了,要看每一組的第1個元素
    // 共分成了 gap 組,第一組的第1個元素索引為 gap
    // 第一組元素索引為 0, 0+gap, 0+2*gap,...,第二組元素索引為 1, 1+gap, 2+2*gap,...
    for (let i = gap; i < arr.length; i++) {
      let current_ele = arr[i]
      // 普通插入排序時,j 每次減少1,即與前面的每個元素比較
      // 這裡 j 每次減少 gap,只會與當前元素相隔 n*(gap-1) 的元素比較,也就是隻會與同組的元素比較
      let ordered_index = i - gap
      while (ordered_index >= 0 && arr[ordered_index] > current_ele) {
        arr[ordered_index + gap] = arr[ordered_index]
        ordered_index -= gap
      }
      arr[ordered_index + gap] = current_ele
    }
  }
  return arr
}
複製程式碼

快速排序 Quick Sort

大致流程:

  1. 選擇一個基準元素,比如第一個元素

    當然可以選其他元素,但是最後會遞迴至只剩一個元素,所以還是選第一個元素比較靠譜

  2. 遍歷陣列,比基準元素更小的元素建立一個陣列,更大的建立一個陣列,相等的也建立一個陣列
  3. 遞迴大小兩個陣列,繼續執行1,直到陣列只剩1個元素;遞迴的同時把這三部分連線起來

演示圖:

快速排序演示圖

function quickSort(arr) {
  // 只剩1個元素,不能再分割了
  if (arr.length <= 1) return arr
  // 取第1個元素為基準值
  let base = arr[0]
  // 分割為左小右大兩個陣列,以及包含元素本身的中間陣列
  let left = [], middle = [base], right = []
  for (let index = 1; index < arr.length; index++) {
    // 如果有與本身一樣大的元素,放入 middle 陣列,解決重複元素的問題
    if (arr[index] === base) middle.push(arr[index])
    else if (arr[index] < base) left.push(arr[index])
    else right.push(arr[index])
  }
  // 遞迴併連線
  return quickSort(left).concat(middle, quickSort(right))
}
複製程式碼

歸併排序 Merge Sort

是採用分治法(Divide and Conquer)的一個非常典型的應用。

簡單說就是縮小問題規模,快速排序也是分治法

大致流程:

  1. 遞迴地把陣列分割成前後兩個子陣列,直到陣列中只有1個元素

    直接分兩半,不用排序

  2. 同時,遞迴地從兩個陣列中挨個取元素,比較大小併合並

演示圖:

歸併排序演示圖

// 分割
function mergeSort2(arr) {
  // 如果只剩一個元素,分割結束
  if (arr.length < 2) return arr
  // 否則繼續分成兩部分
  let middle_index = Math.floor(arr.length / 2),
    left = arr.slice(0, middle_index),
    right = arr.slice(middle_index)
  return merge2(mergeSort2(left), mergeSort2(right))
}
// 合併
function merge2(left, right) {
  let result = []
  // 當左右兩個陣列都還沒有取完的時候,比較大小然後合併
  while (left.length && right.length) {
    if (left[0] < right[0]) result.push(left.shift())
    else result.push(right.shift())
  }
  // 其中一個陣列空了,另一個還剩下一些元素
  // 因為是已經排序過的,所以直接concat就好了
  // 注意 concat 不改變原陣列
  if (left.length) result = result.concat(left)
  if (right.length) result = result.concat(right)
  return result
}
複製程式碼

計數排序 Counting Sort

只能用於由確定範圍的整數所構成的陣列。

統計每個元素出現的次數,新建一個陣列 arr,新陣列的索引為原陣列元素的值,每個位置上的值為原陣列元素出現的次數。

大致流程:

  1. 遍歷陣列,找出每個元素出現的次數,放入統計陣列
  2. 遍歷統計陣列,放入結果陣列

演示圖:

計數排序演示圖

function countingSort(array) {
  let count_arr = [], result_arr = []
  // 統計出現次數
  for (let i = 0; i < array.length; i++) {
    count_arr[array[i]] = count_arr[array[i]] ? count_arr[array[i]] + 1 : 1
  }
  // 遍歷統計陣列,放入結果陣列
  for (let i = 0; i < count_arr.length; i++) {
    while (count_arr[i] > 0) {
      result_arr.push(i)
      count_arr[i]--
    }
  }
  return result_arr
}
複製程式碼

桶排序 Bucket Sort

根據原陣列的最小和最大值的範圍,劃分出幾個區間,每個區間用陣列來表示,也就是這裡所說的
根據元素大小分別放入對應的桶當中,每個桶中使用任意演算法進行排序,最後再把幾個桶合併起來。

區間的數量一般是手動指定的。

基本流程:

  1. 初始化指定個數的桶
  2. 找到陣列的最大值和最小值,作差併除以桶數,就得到了每個桶中值的範圍 range
  3. 遍歷陣列,每個元素的值除以 range,商的整數部分即對應的桶的索引,放入該桶
  4. 入桶時,可以立即執行排序,而不只是單單的 push(),比如使用插入排序
  5. 遍歷結束時,每個桶中的元素都是排序好的。並且因為桶也是按順序擺放的,直接把所有的桶按順序 concat起來即可

其他排序方法當然也可以。不過插入排序實現時更接近“給已排序陣列新增一個元素並使之有序”這種目的。

演示圖:

function bucketSort(array, num) {
  let buckets = [],
    min = Math.min(...array),
    max = Math.max(...array)
  // 初始化 num 個桶
  for (let i = 0; i < num; i++) buckets[i] = []
  // (最大值-最小值)/桶數,得到每個桶最小最大值的差,即區間
  // 比如 range 為10, 0號桶區間為0-10,1號桶10-20,...
  let range = (max - min + 1) / num
  for (let i = 0; i < array.length; i++) {
    // (元素-最小值)/區間,取整數部分,就是應該放入的桶的索引
    let bucket_index = Math.floor((array[i] - min) / range),
      bucket = buckets[bucket_index]
    // 空桶直接放入
    if (bucket.length) {
      bucket.push(array[i])
    }
    // 非空,插入排序
    else {
      let i = bucket.length - 1
      while (i >= 0 && bucket[i] > array[i]) {
        bucket[i + 1] = bucket[i]
        i--
      }
      bucket[i + 1] = array[i]
    }
  }
  // 合併所有桶
  let result = []
  buckets.forEach((bucket) => {
    result = result.concat(bucket)
  })
  return result
}
複製程式碼

一個題外話,關於 Arrayfill() 方法。

在初始化陣列的時候,想著是不是可以用 let arr = new Array(4).fill([]),一行程式碼就可以給陣列新增初始元素,這樣就不用先建立陣列,然後再 for 迴圈新增元素了。

但是問題是,fill() 新增的引用型別元素——這裡就是空陣列 []——它們指向的是同一個引用。如果修改了其中一個陣列,其他的陣列也都跟著變了。

還是老老實實 for 迴圈吧。

基數排序 Radix Sort

要求元素必須是0或正整數。

通過比較每個元素對應位置上數字的大小進行排序:個位與個位,十位與十位 ...

根據比較順序不同,分為兩類:

  • Least Significant Digit,從個位開始比較
  • Most Significant Digit,從最高位開始比較

兩種方法的共同點是:

  • 先要找到最大的元素。因為每個元素的每一位都要對應比較,所以要看最大的元素有幾位
  • 當其中一個元素某一位上沒有值時,以0代替

LSD

插播一曲 LSD: Lucy in the Sky with Diamonds

基本流程:

先看一下演示圖比較好

  1. 找出最大元素,並獲取其位數(長度) max_len
  2. 外層迴圈以 max_len 作為遍歷次數,從個位開始;內層迴圈遍歷陣列
  3. 每次外層迴圈,都比較元素該位上的數字
  4. 每次外層迴圈的最開始,先初始化 10 個陣列,或者叫做桶,表示該位上的數字是 0-9 其中的一個
  5. 內層遍歷根據每個元素當前位上的值放到對應的桶裡
  6. 每次外層迴圈結束,把 10 個桶裡的元素按順序取出,並覆蓋原陣列,得到一個排序過後的陣列

演示圖:

基數排序演示圖

function radixSortLSD(arr) {
  // 找出最大元素
  let max_num = Math.max(...arr),
    // 獲取其位數
    max_len = getLengthOfNum(max_num)
  console.log(`最大元素是 ${max_num},長度 ${max_len}`)
  // 外層遍歷位數,內層遍歷陣列
  // 外層迴圈以最大元素的位數作為遍歷次數
  for (let digit = 1; digit <= max_len; digit++) {
    // 初始化0-9 10個陣列,這裡暫且叫做桶
    let buckets = []
    for (let i = 0; i < 10; i++) buckets[i] = []
    // 遍歷陣列
    for (let i = 0; i < arr.length; i++) {
      // 取出一個元素
      let ele = arr[i]
      // 獲取當前元素該位上的值
      let value_of_this_digit = getSpecifiedValue(ele, digit)
      // 根據該值,決定當前元素要放到哪個桶裡
      buckets[value_of_this_digit].push(ele)
      console.log(buckets)
    }
    // 每次內層遍歷結束,把所有桶裡的元素依次取出來,覆蓋原陣列
    let result = []
    buckets.toString().split(',').forEach((val) => {
      if (val) result.push(parseInt(val))
    })
    // 得到了一個排過序的新陣列,繼續下一輪外層迴圈,比較下一位
    arr = result
    console.log(arr)
  }
}

function getLengthOfNum(num) { return (num += '').length }

// 獲取一個數字指定位數上的值,超長時返回0
// 個位的位數是1,十位的位數是2 ...
function getSpecifiedValue(num, position) { return (num += '').split('').reverse().join('')[position - 1] || 0 }
複製程式碼

MSD

這個沒圖,不過更簡單,也不需要圖。

現實生活中比較數字大小的時候一般也是這麼做的,先比較最高位,然後再看更小位。

基本流程:

  1. 找出最大元素,獲取位數
  2. 從最高位開始,比較每個元素相同位置上的數字,分桶
  3. 如果還沒比較到個位,那麼遞迴每個不為空的桶,繼續比較他們的下一位

舉兩個栗子。

沒有重複元素的情況:

// 原始陣列
[110, 24, 27, 56, 9]
// 原陣列相當於
[110, 024, 027, 056, 009]
// 第一次入桶,比較最高位百位
[[024, 027, 056, 009], [110]]
// 當桶中有多個元素時,遞迴。這裡就是遞迴第一個桶
// 第二次入桶,比較十位
[[[009], [024, 027], [056]], [110]]
// 第二個桶中還有元素,繼續遞迴
// 第三次入桶,比較個位
[[[009], [[024], [027]], [056]], [110]]
// 結果就是
[009, 024, 027, 056, 110]
複製程式碼

也就是說,對於沒有重複元素的情況,遞迴的最終結果是每個桶中只有一個元素。

有重複元素的情況:

[110, 024, 024, 056, 009]
// 第一次入桶,比較百位
[[009, 024, 024, 056], [110]]
// 第二次入桶,比較十位
[[[009], [024, 024], [056]], [110]]
// 第三次入桶,比較個位
[[[009], [[024, 024]], [056]], [110]]
複製程式碼

可以發現,對於有重複元素的情況,最終重複的元素都會在同一個桶中,不會產生每個桶中只有一個元素的結果。
這時只要判斷是否已經比較完個位了即可。也就是說,不管有沒有重複元素,最大元素有幾位,就最多需要比較多少次。

總之,可以想象成一個樹結構,從原陣列開始一直向下分出子陣列,最後子陣列中只有一個元素,或只有重複的元素。

function radixSortMSD(arr) {
  // 最大元素
  let max_num = Math.max(...arr),
    // 獲取其位數作為初始值,最小值為1,也就是個位
    digit = getLengthOfNum(max_num)
  return msd(arr, digit)
}
function msd(arr, digit) {
  // 建10個桶
  let buckets = []
  for (let i = 0; i < 10; i++) buckets[i] = []
  // 遍歷陣列,入桶。這裡跟 LSD 一樣
  for (let i = 0; i < arr.length; i++) {
    let ele = arr[i]
    let value_of_this_digit = getSpecifiedValue(ele, digit)
    buckets[value_of_this_digit].push(ele)
  }
  // 結果陣列
  let result = []
  // 遍歷每個桶
  for (let i = 0; i < buckets.length; i++) {
    // 只剩一個元素,直接加入結果陣列
    if (buckets[i].length === 1) result = result.concat(buckets[i])
    // 還有多個元素,但是已經比較到個位了
    // 說明是重複元素的情況,也直接加入結果陣列
    else if (buckets[i].length && digit === 1) result = result.concat(buckets[i])
    // 還有多個元素,並且還沒有比較結束,遞迴比較下一位
    else if (buckets[i].length && digit !== 1) result = result.concat(msd(buckets[i], digit - 1))
    // 空桶就不作處理了
  }
  return result
}
複製程式碼

參考連結

十大經典排序演算法總結(JavaScript描述) - 掘金
前端 排序演算法總結 - segmentfault
圖解排序演算法(二)之希爾排序
計數排序,桶排序與基數排序 - segmentfault
時間複雜度 - 維基
比較排序 - 維基

相關文章