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

簡介

本文介紹了常見的 10 種排序算法的原理基本實現常見的優化實現,並有(我的認爲)足夠詳細的代碼註釋
實在是居家工做,面試筆試必備良藥。html

這裏只給出基於其原理的通常實現,不少算法都有邏輯更復雜的或代碼量更少的精簡版,像遍歷的改爲遞歸的,兩個函數實現的改爲一個函數等等,就再也不說起了。前端

夠詳細了!傻子都能看懂!若是不懂,多看幾遍!git

前幾天在微博上看到一個視頻:用音頻演示15種排序算法,能夠看一下面試

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

分類

另外一種分類方式是根據是否爲「比較排序」。shell

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

複雜度和穩定性

平均時間複雜度 最好 最壞 空間複雜度 穩定性
冒泡排序 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

通常實現

已排序元素將放在數組尾部segmentfault

大體流程:api

  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)
    }
  }
  return arr
}
// 後面還會屢次用到,就再也不寫出來了
function swap(arr, n, m) {
  [arr[n], arr[m]] = [arr[m], arr[n]]
}
複製代碼

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

  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[min_index]) 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. 選擇一個基準元素 pivot,好比第一個元素

    固然能夠選其餘元素,可是最後會遞歸至只剩一個元素,因此仍是選第一個元素比較靠譜

  2. 遍歷數組,比 pivot 更小的元素建立一個數組,更大的建立一個數組,相等的也建立一個數組
  3. 遞歸大小兩個數組,繼續執行1,直到數組只剩1個元素;遞歸的同時把這三部分鏈接起來

普通快速排序沒有考慮與 pivot 相等的狀況,只建了更小和更大的兩個數組。
像上面考慮與 pivot 相等的狀況時,又叫作三路快排

演示圖:

快速排序演示圖

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
JS快速排序&三路快排
圖解排序算法(二)之希爾排序
計數排序,桶排序與基數排序 - segmentfault
時間複雜度 - 維基
比較排序 - 維基

打個廣告

個人其餘文章:

《深刻 JavaScript 經常使用的8種繼承方案》
《免費爲網站添加 SSL 證書》
《詳解 new/bind/apply/call 的模擬實現》

相關文章
相關標籤/搜索