什么是計數排序?


有這樣一道排序題:數組里有20個隨機數,取值范圍為從0到10,要求用最快的速度把這20個整數從小到大進行排序。

第一時間你可能會想使用快速排序,因為快排的時間復雜度只有O(nlogn)。但是這種方法還是不夠快,有沒有比O(nlogn)更快的排序方法呢?你可能會有疑問:O(nlogn)已經是最快的排序算法了,怎么可能還有更快的排序方法?

讓我們先來回顧一下經典的排序算法,無論是歸並排序,冒泡排序還是快速排序等等,都是基於元素之間的比較來進行排序的。但是有一種特殊的排序算法叫計數排序,這種排序算法不是基於元素比較,而是利用數組下標來確定元素的正確位置。

在剛才的題目里,隨即整數的取值范圍是從0到10,那么這些整數的值肯定是在0到10這11個數里面。於是我們可以建立一個長度為11的數組,數組下標從0到10,元素初始值全為0,如下所示:

先假設20個隨機整數的值是:9, 3, 5, 4, 9, 1, 2, 7, 8,1,3, 6, 5, 3, 4, 0, 10, 9, 7, 9

讓我們先遍歷這個無序的隨機數組,每一個整數按照其值對號入座,對應數組下標的元素進行加1操作。

比如第一個整數是9,那么數組下標為9的元素加1:

第二個整數是3,那么數組下標為3的元素加1:

繼續遍歷數列並修改數組......

最終,數列遍歷完畢時,數組的狀態如下:

數組中的每一個值,代表了數列中對應整數的出現次數。

有了這個統計結果,排序就很簡單了,直接遍歷數組,輸出數組元素的下標值,元素的值是幾,就輸出幾次:

0, 1, 1, 2, 3, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 9, 9, 9, 10

顯然,這個輸出的數列已經是有序的了。

這就是計數排序的基本過程,它適用於一定范圍的整數排序在取值范圍不是很大的情況下,它的性能在某些情況甚至快過那些O(nlogn)的排序,例如快速排序、歸並排序。

代碼實現如下:

public static int[] countSort(int[] array) {
    //1.得到數列的最大值
    int max = array[0];
    for (int i = 1; i < array.length; i++) {
        if (array[i] > max)
            max = array[i];
    }
    //2.根據數列的最大值確定統計數組的長度
    int[] coutArray = new int[max + 1];
    //3.遍歷數列,填充統計數組
    for(int i = 0; i < array.length; i++)
        coutArray[array[i]]++;

    //4.遍歷統計數組,輸出結果
    int index = 0;
    int[] sortedArray = new int[array.length];
    for (int i = 0; i < coutArray.length; i++) {
        for (int j = 0; j < coutArray[i]; j++) {
            sortedArray[index++] = i;
        }
    }

    return sortedArray;
}

這段代碼在一開始補充了一個步驟,就是求得數列的最大整數值max,后面創建的數組countArray,長度就是max+1,以此保證數組最后一個下標是max。

從功能角度來看,這段代碼可以實現整數的排序。但是這段代碼其實並不嚴謹。

比如這個數列:95, 94, 91, 98, 99, 90, 99, 93, 91, 92。該數列最大值是99,但最小值是90,如果我們只以數列的最大值來決定統計數組的長度的話,就要創建長度為100的數組,那么就會浪費前面90個空間。

為了解決這個問題,我們不再以(輸入數列的最大值+1)作為統計數組的長度,而是以(數列最大值和最小值的差+1)作為統計數組的長度。同時,數列的最小值作為一個偏移量,用於統計數組的對號入座。

以剛才的數列為例,統計數組的長度為 99-90+1=10,偏移量等於數列最小值90。

對於第一個整數95,對應的統計數組下標為95-90=5,如圖所示:

這是一方面,另外,上述代碼知識簡單地按照統計數組的下標輸出了元素值,並沒有真正給數列排序。如果僅僅只是給整數排序,這樣並沒有問題。但如果是在現實業務里,比如給學生的考試分數排序,如果遇到相同的分數就會分不清誰是誰。看看下面這個例子:

給出一個學生的成績表,要求按成績從底到高排序,如果成績相同,則遵循原表固有順序。

當我們填充統計數組之后,我們只知道有兩個成績並列95分的學生,卻不知道誰是小紅,誰是小綠:

對此,我們只需在填充完統計數組之后,對統計數組做一下變形。我們仍然以學生的成績表為例,把之前的統計數組進行變形,統計數組從第二個元素開始,每一個元素都加上前面所有元素之和:

相加的目的就是為了讓統計數組存儲的元素值等於相應整數的最終排序位置。比如下標是9的元素值是5,代表原始數列的整數9最終的排序是在第5位。

接下來,我們創建輸出數組sortedArray,長度和輸入數列一致,然后從后向前遍歷輸入數列:

第一步,遍歷成績表最后一行的小綠:小綠是95分,找到countArray下標為5的元素,值是4,代表小綠的成績排名是在第4位。

同時給countArray下標是5的元素值減1,從4變成3,代表着下次再遇到95分時,最終排名是第3位。

第二步,遍歷成績表倒數第二行的小白:小白是94分,找到countArray下標是4的元素,值是2,代表小白的成績排名在第2位。

同時,給countArray下標是4的元素值減1,從2變成1,代表下次再遇到94分的成績時(實際上已經遇不到了),最終排名是第1位。

第三步,遍歷成績表倒數第三行的小紅:小紅是95分,找到countArray下標是5的元素,值是3(最初是4,減1變成了3),代表小白的成績排名在第3位。

同時,給countArray下標是5的元素值減1,從3變成2,代表下次再遇到95分的成績時(實際上已經遇不到了),最終排名是第2位。

因此,同樣是95分的小紅和小綠就能清楚地排出順序,所以優化版的計數排序屬於穩定排序

后面的遍歷過程依此類推。

改進版本的計數排序代碼如下:

public static int[] countSort(int[] array) {
        //1.得到數列的最大值與最小值,並算出差值d
        int max = array[0];
        int min = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
            if(array[i] < min) {
                min = array[i];
            }
        }
        int d = max - min;
        //2.創建統計數組並計算統計對應元素個數
        int[] countArray = new int[d + 1];
        for (int i = 0; i < array.length; i++) {
            countArray[array[i] - min]++;
        }
        //3.統計數組變形,后面的元素等於前面的元素之和
        int sum = 0;
        for (int i = 0; i < countArray.length; i++) {
            sum += countArray[i];
            countArray[i] = sum;
        }
        //4.倒序遍歷原始數組,從統計數組找到正確位置,輸出到結果數組
        int[] sortedArray = new int[array.length];
        for (int i = array.length - 1; i > 0; i--) {
            sortedArray[countArray[array[i] - min] - 1] = array[i];
            countArray[array[i] - min]--;
        }
        return sortedArray;
    }

如果原始數列的規模是N,最大最小整數的差值是M,由於代碼中第1、2、4步都涉及到遍歷原始數列,運算量都是N,第3步遍歷統計數列,運算量是M,所以總體運算量是3N+M,去掉系數,時間復雜度是O(N+M)

至於空間復雜度,如果不考慮結果數組,只考慮統計數組的話,空間復雜度是O(M)

雖然計數排序看上去很強大,但是它存在兩大局限性

1.當數列最大最小值差距過大時,並不適用於計數排序

比如給定20個隨機整數,范圍在0到1億之間,此時如果使用計數排序的話,就需要創建長度為1億的數組,不但嚴重浪費了空間,而且時間復雜度也隨之升高。

2.當數列元素不是整數時,並不適用於計數排序

如果數列中的元素都是小數,比如3.1415,或是0.00000001這樣子,則無法創建對應的統計數組,這樣顯然無法進行計數排序。

正是由於這兩大局限性,才使得計數排序不像快速排序、歸並排序那樣被人們廣泛適用。


免責聲明!

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



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