簡介
本文介紹了常見的 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
演示圖:
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]]
}
複製程式碼
有優化空間,主要從兩方面進行優化:
- 減少外層遍歷次數
- 讓每次遍歷能找到兩個極值
優化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
每次遍歷選擇最小。
排序後的元素將放在陣列前部
大致流程:
- 取出未排序部分的第一個元素,遍歷該元素之後的部分並比較大小。對於第一次遍歷,就是取出第一個元素
- 如果有更小的,與該元素交換位置
- 每次遍歷都能找出剩餘元素中的最小值並放在已排序部分的最後
並不是倒著的氣泡排序。氣泡排序是比較相鄰的兩個元素
演示圖:
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
使用堆的概念實現的選擇排序。
首先,關於堆:
- 堆是樹的一種。當堆的父節點都大於,或者都小於子節點時,分別稱為最大堆或最小堆
- 可以用陣列來表示樹(堆)。從0開始,以陣列的第
index
個元素為堆的父節點,其左右子節點分別為陣列的第2*index+1
和2*index+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步中,遍歷已排序元素時,如果該未排序元素仍然小於當前比較的已排序元素,就把前一個已排序元素的值賦給後一個位置上的元素,也就是產生了兩個相鄰的重複元素。
這樣一來,在比較到最後,找到合適的位置時,用該未排序元素給兩個重複元素中合適的那一個賦值,覆蓋掉一個,排序就完成了。
敘述可能不夠清楚,看後面的程式碼就是了。
Talk is hard, show you some codes。
和選擇排序好像有一點類似的地方:
- 選擇排序,先找合適的元素,然後直接放到已排序部分
- 插入排序,先按順序取元素,再去已排序部分裡找合適的位置
第二種理解方式:
在前面的第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
如何取值最好,還沒有研究過。期待大家留言交流。(只是隨便一說,我看這個單純就是為了面試。。)
大致流程:
- 共三層迴圈,外層迴圈用來逐步減少
gap
的值 - 中層與內層兩層迴圈基本上就是插入排序,細節上的不同直接看程式碼就好,不再贅述
演示圖:
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
大致流程:
- 選擇一個基準元素
pivot
,比如第一個元素當然可以選其他元素,但是最後會遞迴至只剩一個元素,所以還是選第一個元素比較靠譜
- 遍歷陣列,比
pivot
更小的元素建立一個陣列,更大的建立一個陣列,相等的也建立一個陣列 - 遞迴大小兩個陣列,繼續執行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個元素
直接分兩半,不用排序
-
同時,遞迴地從兩個陣列中挨個取元素,比較大小併合並
演示圖:
// 分割
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
,新陣列的索引為原陣列元素的值,每個位置上的值為原陣列元素出現的次數。
大致流程:
- 遍歷陣列,找出每個元素出現的次數,放入統計陣列
- 遍歷統計陣列,放入結果陣列
演示圖:
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
根據原陣列的最小和最大值的範圍,劃分出幾個區間,每個區間用陣列來表示,也就是這裡所說的桶。
根據元素大小分別放入對應的桶當中,每個桶中使用任意演算法進行排序,最後再把幾個桶合併起來。
區間的數量一般是手動指定的。
基本流程:
- 初始化指定個數的桶
- 找到陣列的最大值和最小值,作差併除以桶數,就得到了每個桶中值的範圍
range
- 遍歷陣列,每個元素的值除以
range
,商的整數部分即對應的桶的索引,放入該桶 - 入桶時,可以立即執行排序,而不只是單單的
push()
,比如使用插入排序 - 遍歷結束時,每個桶中的元素都是排序好的。並且因為桶也是按順序擺放的,直接把所有的桶按順序
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
}
複製程式碼
一個題外話,關於 Array
的 fill()
方法。
在初始化陣列的時候,想著是不是可以用 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
基本流程:
先看一下演示圖比較好
- 找出最大元素,並獲取其位數(長度)
max_len
- 外層迴圈以
max_len
作為遍歷次數,從個位開始;內層迴圈遍歷陣列 - 每次外層迴圈,都比較元素該位上的數字
- 每次外層迴圈的最開始,先初始化 10 個陣列,或者叫做桶,表示該位上的數字是 0-9 其中的一個
- 內層遍歷根據每個元素當前位上的值放到對應的桶裡
- 每次外層迴圈結束,把 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
這個沒圖,不過更簡單,也不需要圖。
現實生活中比較數字大小的時候一般也是這麼做的,先比較最高位,然後再看更小位。
基本流程:
- 找出最大元素,獲取位數
- 從最高位開始,比較每個元素相同位置上的數字,分桶
- 如果還沒比較到個位,那麼遞迴每個不為空的桶,繼續比較他們的下一位
舉兩個栗子。
沒有重複元素的情況:
// 原始陣列
[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 的模擬實現》