天天看點

一文弄懂計數排序算法!

這是小川的第385次更新,第413篇原創

01 計數排序算法概念

計數排序不是一個比較排序算法,該算法于1954年由 Harold H. Seward提出,通過計數将時間複雜度降到了

O(N)

02 基礎版算法步驟

第一步:找出原數組中元素值最大的,記為

max

第二步:建立一個新數組

count

,其長度是

max

加1,其元素預設值都為0。

第三步:周遊原數組中的元素,以原數組中的元素作為

count

數組的索引,以原數組中的元素出現次數作為

count

數組的元素值。

第四步:建立結果數組

result

,起始索引

index

第五步:周遊

count

數組,找出其中元素值大于0的元素,将其對應的索引作為元素值填充到

result

數組中去,每處理一次,

count

中的該元素值減1,直到該元素值不大于0,依次處理

count

中剩下的元素。

第六步:傳回結果數組

result

03 基礎版代碼實作

一文弄懂計數排序算法!
一文弄懂計數排序算法!
一文弄懂計數排序算法!
public int[] countSort(int[] A) {
    // 找出數組A中的最大值
    int max = Integer.MIN_VALUE;
    for (int num : A) {
        max = Math.max(max, num);
    }
    // 初始化計數數組count
    int[] count = new int[max+1];
    // 對計數數組各元素指派
    for (int num : A) {
        count[num]++;
    }
    // 建立結果數組
    int[] result = new int[A.length];
    // 建立結果數組的起始索引
    int index = 0;
    // 周遊計數數組,将計數數組的索引填充到結果數組中
    for (int i=0; i<count.length; i++) {
        while (count[i]>0) {
            result[index++] = i;
            count[i]--;
        }
    }
    // 傳回結果數組
    return result;
}
           

04 優化版

基礎版能夠解決一般的情況,但是它有一個缺陷,那就是存在空間浪費的問題。

比如一組資料

{101,109,108,102,110,107,103}

,其中最大值為110,按照基礎版的思路,我們需要建立一個長度為111的計數數組,但是我們可以發現,它前面的

[0,100]

的空間完全浪費了,那怎樣優化呢?

将數組長度定為

max-min+1

,即不僅要找出最大值,還要找出最小值,根據兩者的差來确定計數數組的長度。

public int[] countSort2(int[] A) {
    // 找出數組A中的最大值、最小值
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for (int num : A) {
        max = Math.max(max, num);
        min = Math.min(min, num);
    }
    // 初始化計數數組count
    // 長度為最大值減最小值加1
    int[] count = new int[max-min+1];
    // 對計數數組各元素指派
    for (int num : A) {
        // A中的元素要減去最小值,再作為新索引
        count[num-min]++;
    }
    // 建立結果數組
    int[] result = new int[A.length];
    // 建立結果數組的起始索引
    int index = 0;
    // 周遊計數數組,将計數數組的索引填充到結果數組中
    for (int i=0; i<count.length; i++) {
        while (count[i]>0) {
            // 再将減去的最小值補上
            result[index++] = i+min;
            count[i]--;
        }
    }
    // 傳回結果數組
    return result;
}
           

05 進階版步驟

以數組

A = {101,109,107,103,108,102,103,110,107,103}

為例。

第一步:找出數組中的最大值

max

、最小值

min

count

,其長度是max-min加1,其元素預設值都為0。

count

count

一文弄懂計數排序算法!
一文弄懂計數排序算法!
一文弄懂計數排序算法!

第四步:對

count

數組變形,新元素的值是前面元素累加之和的值,即

count[i+1] = count[i+1] + count[i];

一文弄懂計數排序算法!

第五步:建立結果數組

result

,長度和原始數組一樣。

第六步:周遊原始數組中的元素,目前元素A[j]減去最小值

min

,作為索引,在計數數組中找到對應的元素值

count[A[j]-min]

,再将count[A[j]-min]的值減去1,就是

A[j]

在結果數組

result

中的位置,做完上述這些操作,

count[A[j]-min]

自減1。

是不是對第四步和第六步有疑問?為什麼要這樣操作?

第四步操作,是讓計數數組

count

存儲的元素值,等于原始數組中相應整數的最終排序位置,即計算原始數組中的每個數字在結果數組中處于的位置。

比如索引值為9的

count[9]

,它的元素值為10,而索引9對應的原始數組

A

中的元素為9+101=110(要補上最小值

min

,才能還原),即110在排序後的位置是第10位,即result[9] = 110,排完後

count[9]

的值需要減1,

count[9]

變為9。

再比如索引值為6的

count[6]

,他的元素值為7,而索引6對應的原始數組

A

中的元素為6+101=107,即107在排序後的位置是第7位,即

result[6] = 107

,排完後

count[6]

count[6]

變為6。

如果索引值繼續為6,在經過上一次的排序後,

count[6]

的值變成了6,即107在排序後的位置是第6位,即

result[5] = 107

count[6]

count[6]

變為5。

至于第六步操作,就是為了找到A中的目前元素在結果數組

result

中排第幾位,也就達到了排序的目的。

06 進階版代碼實作

一文弄懂計數排序算法!
一文弄懂計數排序算法!
一文弄懂計數排序算法!
一文弄懂計數排序算法!
一文弄懂計數排序算法!
一文弄懂計數排序算法!
public int[] countSort3(int[] A) {
    // 找出數組A中的最大值、最小值
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for (int num : A) {
        max = Math.max(max, num);
        min = Math.min(min, num);
    }
    // 初始化計數數組count
    // 長度為最大值減最小值加1
    int[] count = new int[max-min+1];
    // 對計數數組各元素指派
    for (int num : A) {
        // A中的元素要減去最小值,再作為新索引
        count[num-min]++;
    }
    // 計數數組變形,新元素的值是前面元素累加之和的值
    for (int i=1; i<count.length; i++) {
        count[i] += count[i-1];
    }
    // 建立結果數組
    int[] result = new int[A.length];
    // 周遊A中的元素,填充到結果數組中去
    for (int j=0; j<A.length; j++) {
        result[count[A[j]-min]-1] = A[j];
        count[A[j]-min]--;
    }
    return result;
}
           

07 進階版的延伸之一

如果我們想要原始數組中的相同元素按照本來的順序的排列,那該怎麼處理呢?

依舊以上一個數組

{101,109,107,103,108,102,103,110,107,103}

為例,其中有兩個107,我們要實作第二個107在排序後依舊排在第一個107的後面,可以在第六步的時候,做下變動就可以實作,用倒序的方式周遊原始數組,即從後往前周遊

A

數組。

從後往前周遊,第一次遇到107(

A[8]

)時,107-101 = 6,

count[6] = 7

,即第二個107要排在第7位,即

result[6] = 107

,排序後

count[6] = 6

繼續往前,第二次遇到107(

A[2]

count[6] = 6

,即第一個107要排在第6位,即

result[5] = 107

count[6] = 5

public int[] countSort4(int[] A) {
    // 找出數組A中的最大值、最小值
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for (int num : A) {
        max = Math.max(max, num);
        min = Math.min(min, num);
    }
    // 初始化計數數組count
    // 長度為最大值減最小值加1
    int[] count = new int[max-min+1];
    // 對計數數組各元素指派
    for (int num : A) {
        // A中的元素要減去最小值,再作為新索引
        count[num-min]++;
    }
    // 計數數組變形,新元素的值是前面元素累加之和的值
    for (int i=1; i<count.length; i++) {
        count[i] += count[i-1];
    }
    // 建立結果數組
    int[] result = new int[A.length];
    // 周遊A中的元素,填充到結果數組中去,從後往前周遊
    for (int j=A.length-1; j>=0; j--) {
        result[count[A[j]-min]-1] = A[j];
        count[A[j]-min]--;
    }
    return result;
}
           

08 進階版的延伸之二

既然從後往前周遊原始數組的元素可以保證其原始排序,那麼從前往後可不可以達到相同的效果?

答案時可以的。

max

min

count

max-min加1再加1

,其元素預設值都為0。

count

count

count

count[i+1] = count[i+1] + count[i];

result

第六步:從前往後周遊原始數組中的元素,目前元素

A[j]

減去最小值

min

count[A[j]-min]

,就是

A[j]

result

count[A[j]-min]

自增加1。

{101,109,107,103,108,102,103,110,107,103}

為例,其中有兩個107,我們要實作第一個107在排序後依舊排在第二個107的前面。

此時計數數組

count

{0, 1, 2, 5, 5, 5, 5, 7, 8, 9, 10}

,從前往後周遊原始數組

A

中的元素。

第一次遇到107(

A[2]

count[6] = 5

,即第一個107在結果數組中的索引為5,即

result[5] = 107

count[6] = 6

第二次遇到107(

A[8]

count[6] = 6

,即第二個107在結果數組中的索引為6,即

result[6] = 107

count[6] = 7

public int[] countSort5(int[] A) {
    // 找出數組A中的最大值、最小值
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for (int num : A) {
        max = Math.max(max, num);
        min = Math.min(min, num);
    }
    // 初始化計數數組count
    // 長度為最大值減最小值加1,再加1
    int[] count = new int[(max-min+1)+1];
    // 對計數數組各元素指派,count[0]永遠為0
    for (int num : A) {
        // A中的元素要減去最小值再加上1,再作為新索引
        count[num-min+1]++;
    }
    // 計數數組變形,新元素的值是前面元素累加之和的值
    for (int i=1; i<count.length; i++) {
        count[i] += count[i-1];
    }
    // 建立結果數組
    int[] result = new int[A.length];
    // 周遊A中的元素,填充到結果數組中去,從前往後周遊
    for (int j=0; j<A.length; j++) {
        // 如果後面遇到相同的元素,在前面元素的基礎上往後排
        // 如此就保證了原始數組中相同元素的原始排序
        result[count[A[j]-min]] = A[j];
        count[A[j]-min]++;
    }
    return result;
}
           

09 小結

以上就是計數排序算法的全部内容了,雖然它可以将排序算法的時間複雜度降低到

O(N)

,但是有兩個前提需要滿足:一是需要排序的元素必須是整數,二是排序元素的取值要在一定範圍内,并且比較集中。隻有這兩個條件都滿足,才能最大程度發揮計數排序的優勢。

繼續閱讀