1. 程式人生 > 其它 >詳解排序演算法(四)之計數排序

詳解排序演算法(四)之計數排序

計數排序

計數排序不是一個比較排序演算法,該演算法於1954年由 Harold H. Seward提出。

01 演算法步驟

  1. 找到數列的最大值,計為 max

  2. 新建一個長度為 max + 1 的陣列,計為 bucket

  3. 遍歷數列,在 bucket 中找到值對應的下標,若對應下標裡已有值,值加 1,若無值,將值設定為 1,例如

    值為5,則找到 bucket 中下標為 5 的位置,即 bucket [5],若 bucket [5] 不存在,則設定 bucket [5] 為1,即 bucket [5] = 1 ,若 bucket [5] 存在,則值加 1,即 bucket [5]++

  4. 遍歷 bucket

    ,對於存在值的元素,設其值為 count,下標為 index,往結果陣列中插入 count 個元素,元素的值為 index例如

    bucket [5] = 3,則 count = 3index = 5,往結果陣列中插入 3 個 5。

02 示例

我們取 2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2 來示範

  1. 找到最大值為 9

  2. 新建一個長度為 10 的陣列,[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

  3. 找到下標 2,將其值置為 1,得 [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

    找到下標 3,將其值置為 1,得 [0, 0, 1, 1

    , 0, 0, 0, 0, 0, 0]

    找到下標 8,將其值置為 1,得 [0, 0, 1, 1, 0, 0, 0, 0, 1, 0]

    找到下標 7,將其值置為 1,得 [0, 0, 1, 1, 0, 0, 0, 1, 1, 0]

    找到下標 1,將其值置為 1,得 [0, 1, 1, 1, 0, 0, 0, 1, 1, 0]

    找到下標 2,將其內值加 1,得 [0, 1, 2, 1, 0, 0, 0, 1, 1, 0]

    找到下標 2,將其內值加 1,得 [0, 1, 3, 1, 0, 0, 0, 1, 1, 0]

    找到下標 2,將其內值加 1,得 [0, 1, 4, 1, 0, 0, 0, 1, 1, 0]

    找到下標 7,將其內值加 1,得 [0, 1, 4, 1, 0, 0, 0, 2

    , 1, 0]

    找到下標 3,將其內值為 1,得 [0, 1, 4, 2, 0, 0, 0, 2, 1, 0]

    找到下標 9,將其值置為 1,得 [0, 1, 4, 2, 0, 0, 0, 2, 1, 1]

    找到下標 8,將其內值加 1,得 [0, 1, 4, 2, 0, 0, 0, 2, 2, 1]

    找到下標 2,將其內值加 1,得 [0, 1, 5, 2, 0, 0, 0, 2, 2, 1]

    找到下標 1,將其內值加 1,得 [0, 2, 5, 2, 0, 0, 0, 2, 2, 1]

    找到下標 4,將其值置為 1,得 [0, 2, 5, 2, 1, 0, 0, 2, 2, 1]

    找到下標 2,將其內值加 1,得 [0, 2, 6, 2, 1, 0, 0, 2, 2, 1]

    找到下標 4,將其內值加 1,得 [0, 2, 6, 2, 2, 0, 0, 2, 2, 1]

    找到下標 6,將其值置為 1,得 [0, 2, 6, 2, 2, 0, 1, 2, 2, 1]

    找到下標 9,將其內值加 1,得 [0, 2, 6, 2, 2, 0, 1, 2, 2, 2]

    找到下標 2,將其內值加 1,得 [0, 2, 7, 2, 2, 0, 1, 2, 2, 2]

  4. 遍歷 [0, 2, 7, 2, 2, 0, 1, 2, 2, 2]

    插入 21,得 [1, 1]

    插入 72,得 [1, 1, 2, 2, 2, 2, 2, 2, 2]

    插入 23,得 [1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3]

    插入 24,得 [1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4]

    插入 16,得 [1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6]

    插入 27,得 [1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7]

    插入 28,得 [1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8]

    插入 29,得 [1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 6, 7, 7, 8, 8, 9, 9]

    排序完成。

03 動態圖

04 javascript程式碼

function countSort(arr) {
    const max = getMax(arr), // 陣列中最大值
          bucketLen = max + 1
          bucket = new Array(bucketLen)
    let sortedIndex = 0

    for (let i = 0; i < arr.length; i++) {
        if (!bucket[arr[i]]) {
            bucket[arr[i]] = 0
        }
        bucket[arr[i]]++
    }

    for (let j = 0; j < bucketLen; j++) {
        while(bucket[j] > 0) {
            arr[sortedIndex++] = j
            bucket[j]--
        }
    }

    return arr
}

// 獲取陣列最大值
function getMax (arr) {
    let max = arr[0]

    arr.forEach(e => {
        max = e > max ? e : max
    })

    return max
}

優化版 —— 基於最小值的計數排序

經過上面的學習,我們已經掌握了計數排序的過程。但上面的演算法有問題嗎?當然有!!!

我們不妨舉以下兩個例子

  • 取數列 101, 99, 99, 100 ,按照上面的演算法,所需陣列長度為 102,但實際上,我們只用到了 3 個位置,空間大大浪費。
  • 假設數列中存在負數,如 -1, 10, 11,按照以上演算法,陣列長度為 12,可適配的值的範圍為 0 - 11,但 -1 顯然不在這個範圍內

基於以上問題,優化版 —— 基於最小值的計數排序 便應運而生。

01 演算法步驟

  1. 找到數列的最小值、最大值,分別計為 minmax

  2. 新建一個長度為 max + 1 的陣列,計為 bucket

  3. 遍歷數列,在 bucket 中找到 值 - min 對應的下標,若對應下標裡已有值,值加 1,若無值,將值設定為 1,例如

    值為5,min = 2,則找到 bucket 中下標為 3 的位置,即 bucket [3],若 bucket [3] 不存在,則設定 bucket [3] 為 1,即 bucket [3] = 1 ,若 bucket [3] 存在,則值加 1,即 bucket [3]++

  4. 遍歷 bucket,對於存在值的元素,設其值為 count,下標為 index,往結果陣列中插入 count 個元素,元素的值為 min + index例如

    bucket [3] = 3min = 2,則 count = 3index = 3min + index = 5,往結果陣列中插入 3 個 5。

02 示例

我們取 101, 99, 99, 100 來示範

  1. 找到最小值為 99,最大值 101,101 - 99 = 2

  2. 新建一個長度為 3 的陣列,[0, 0, 0]

  3. 找到下標 101 - 99 = 2,將其值置為 1,得 [0, 0, 1]

    找到下標 99 - 99 = 0,將其值置為 1,得 [1, 0, 1]

    找到下標 99 - 99 = 0,將其內值加 1,得 [2, 0, 1]

    找到下標 100 - 99 = 1,將其值置為 1,得 [2, 1, 1]

    可以看到,優化後的演算法用到的陣列長度為 3,而優化前的陣列長度為 102

  4. 遍歷 [2, 1, 1]

    插入 299 + 0 = 99,得 [99, 99]

    插入 199 + 1 = 100,得 [99, 99, 100]

    插入 199 + 2 = 101,得 [99, 99, 100, 101]

javascript程式碼

function countSort(arr) {
    const max = getMax(arr), // 陣列中最大值
          min = getMin(arr), // 陣列中最小值,以其作為基數
          bucketLen = max - min + 1
          bucket = new Array(bucketLen)
    let sortedIndex = 0

    for (let i = 0; i < arr.length; i++) {
        const index = arr[i] - min // 插入位置的下標
        if (!bucket[index]) {
            bucket[index] = 0
        }
        bucket[index]++
    }

    for (let j = 0; j < bucketLen; j++) {
        while(bucket[j] > 0) {
            arr[sortedIndex++] = min + j
            bucket[j]--
        }
    }

    return arr
}

// 獲取陣列最大值
function getMax (arr) {
    let max = arr[0]

    arr.forEach(e => {
        max = e > max ? e : max
    })

    return max
}

// 獲取陣列最小值
function getMin (arr) {
    let min = arr[0]

    arr.forEach(e => {
        min = e < min ? e : min
    })

    return min
}

總結

01 複雜度及穩定性

排序演算法 時間複雜度(平均) 時間複雜度(最壞) 時間複雜度(最好) 空間複雜度 穩定性
計數排序 O(n + k) O(n + k) O(n + k) O(k) 穩定

其中 k 為數列不重複元素的個數

02 使用場景

  • 適用場景:元素為整數且排列密集
  • 不適用場景:元素存在小數或排列不密集