手寫演算法並記住它:桶排序

老姚發表於2019-09-19

對於經典演算法,你是否也遇到這樣的情形:學時覺得很清楚,可過陣子就忘了?

本系列文章就嘗試解決這個問題。

研讀那些排序演算法,細品它們的名字,其實都很貼切。

比如桶排序,一提起“桶”,我就想到了垃圾分類。

桶排序就是先分類,即把資料放進相應的桶裡,然後對每個桶進行區域性排序,最後再把桶合併一下就行了。

手寫演算法並記住它:桶排序

上圖演示了該演算法的總體流程。分為三步,分類,排序和合並。

該演算法的核心是如何分類,也就是,如何劃分桶並把元素放進相應的桶裡。

它的分類是按區間分類。圖中元素最小值是1,最大值是9,因此所有元素位於屬於區間【1,9】。假設桶(區間)的範圍大小是3,那麼就有3個桶,具體是:【1,3】、【4,6】、【7,9】。

用程式碼體現一下:

let array = [3, 8, 6, 1, 5, 7, 9, 2, 4]
let min = Math.min(...array)
let max = Math.max(...array)
let size = 3
let count = Math.floor((max - min) / size) + 1
let buckets = []
for (let i = 0; i < count; i++) {
  buckets.push([])
}
console.log(buckets) // [ [], [], [] ]
複製程式碼

總體來說程式碼比較簡單,需要注意的是count的計算。

桶有了,接下來就是把每個元素歸類。

for (let v of array) {
  let num = Math.floor((v - min) / size)
  buckets[num].push(v)
}
console.log(buckets) // [ [ 3, 1, 2 ], [ 6, 5, 4 ], [ 8, 7, 9 ] ]
複製程式碼

其中,num表示桶的序號,與count一樣,也是通過偏移量來計算的。因為下標是從0開始的,因此這裡沒有再加1。

關鍵問題解決了,排序和合並就相對簡單了。

排序用其他任一排序演算法都可以,比如資料量不太大的時候,插入排序就比較適合。

因為區間本來就是有序的,合併時只需直接連線這些陣列即可。

let result = []
for (bucket of buckets) {
  result.push(...insertionSort(bucket)) 
}
console.log(result) // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
複製程式碼

至此,桶排序原理和實現已經說完了。檢視完整程式碼:codepen

其實還有優化的空間,比如min和max可以通過一次迴圈確定出來。另外一點是,我們知道插入排序的思想是把待排序元素插入到已排序序列裡,因此可以在第一步分類時就直接插入就行。

這裡總結一下,桶排序是分散式排序,適合處理大批量資料。需要額外空間,是外部排序。桶排序是否穩定,取決於第二步排序演算法的選擇。時間複雜度是線性級O(n),可以簡單理解:桶的範圍大小是人為指定的,它不隨資料規模變化,如果資料相對均勻分佈,那麼桶的個數就是核心影響因子了。

桶排序,要做到能分分鐘手寫出來,是需要掌握其排序原理的。核心是如何劃分割槽間和元素歸類,一旦理解就容易寫出來,不需要死記硬背的。

希望有所幫助,本文完。



本系列已經發表文章:

相關文章