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

老姚發表於2019-09-16

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

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

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

比如計數排序,所謂“計數”,就是數一數,統計每個元素重複出現的次數。

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

上圖演示了該演算法的總體流程。分為兩步:查和排。

首先查一查每個元素都出現了多少次,比如元素0出現了1一次,元素1出現了一次,元素2出現了3次等。

都統計好了,然後排序的過程就簡單了,從小到大按順序填充陣列即可,出現幾次就填充幾次就好了。“從小到大”這個詞語,就體現了排序的過程。

在我看來,計數排序是所有排序演算法中最簡單的,也是最符合直覺的演算法。查和排,這二者都很容易實現。

查,實現如下:

let array = [3, 2, 1, 2, 3, 2, 0, 4]
let counts = []
for (let v of array) {
  counts[v] = (counts[v] || 0) + 1
}
console.log(counts) // [1, 1, 3, 2, 1]
複製程式碼

其中counts陣列是統計結果,用其下標表示待排陣列的元素。其長度為5(待排陣列的最大值加1)。

排,實現如下:

let result = []
for (let i = 0; i < counts.length; i++) {
  let count = counts[i]
  while(count > 0) {
    result.push(i)
    count--
  }
}
console.log(result) // [0, 1, 2, 2, 2, 3, 3, 4]
複製程式碼

上述程式碼,result是新陣列,當然你也可以使用array來填充,那樣就需要有一個變數來記錄排到第幾個位置了,可以檢視文末的完整實現連結。

從演算法具體過程可以看出,計數排序適合整數排序。這裡有個問題,假如數值中有負數怎麼辦?

解決之道,也容易想到,我們先遍歷一遍求出陣列中的最小值,以此作為偏移量就好。

假如資料是[-3, -2, -1, -2, -3, -2, 0, -4],其中資料中陣列的最小值是-4,最大值是0。我們使用counts的第0個下標表示最小值就行了。

let array = [-3, -2, -1, -2, -3, -2, 0, -4]
let counts = [], result = []
let min = Math.min(...array)
for (let v of array) {
  counts[v-min] = (counts[v-min] || 0) + 1
}
for (let i = 0; i < counts.length; i++) {
  let count = counts[i]
  while(count > 0) {
    result.push(i + min)
    count--
  }
}
console.log(result) // [-4, -3, -3, -2, -2, -2, -1, 0]
複製程式碼

統計時要減去偏移量,排序時要加回來的。另外,可以看出counts的長度是最大值和最小值的差值加1。

這種實現方式,也避免了另外一個問題,資料集中空間浪費情形。比如資料都介於900~1000的,那麼counts的長度只需要101就夠了。

至此,計數排序原理和實現已經說完了。

檢視完整程式碼:codepen

這裡總結一下,計數排序適合整數排序,時間複雜度為O(n+k)。簡單說明一下為啥是O(n+k)。這裡使用了兩層迴圈,外層由counts的length——待排陣列最值之差(記為k)——決定的,而while迴圈次數是count決定的,而所有count之和正好為array的length(記為n)。另外關於空間的使用,開篇實現方式的空間複雜度為O(n+k),完整程式碼裡的實現的空間複雜度為O(k)。可見當k特別大時,將會使用很多空間。

計數排序,要做到能分分鐘手寫出來,是需要掌握其排序原理的。先統計出現個數,然後從小到大輸出即可,一旦理解就容易寫出來,不需要死記硬背的。

希望有所幫助,本文完。



本系列已經發表文章:

相關文章