一文弄懂計數排序算法!


這是小川的第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]的值需要減1,count[6]變為6。

如果索引值繼續為6,在經過上一次的排序后,count[6]的值變成了6,即107在排序后的位置是第6位,即result[5] = 107,排完后count[6]的值需要減1,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])時,107-101 = 6,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])時,107-101 = 6,count[6] = 5,即第一個107在結果數組中的索引為5,即result[5] = 107,排序后count[6] = 6

第二次遇到107(A[8])時,107-101 = 6,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),但是有兩個前提需要滿足:一是需要排序的元素必須是整數,二是排序元素的取值要在一定范圍內,並且比較集中。只有這兩個條件都滿足,才能最大程度發揮計數排序的優勢。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM