什麼是計數排序

有這樣一道排序題:數組中有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,如圖所示:
在這裏插入圖片描述

    public static int[] countSort(int[] arr){
        //1.得到數列的最值
        int min = Integer.MAX_VALUE;
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < arr.length; i++){
            if (arr[i] > max){
                max = arr[i];
            }

            if (arr[i] < min){
                min = arr[i];
            }
        }

        //2.根據數列的最值確定統計數組的長度
        int []countArray = new int[max - min + 1];
        //3.遍歷數列,填充統計數組
        for (int i = 0; i < arr.length; i++){
            countArray[arr[i] - min]++;
        }


        //4.遍歷統計數組,輸出結果
        int index = 0;
        int[] sortArray = new int[arr.length];
        for (int i = 0; i < countArray.length; i++){
            while (countArray[i] > 0){
                sortArray[index++] = i + min;
                countArray[i]--;
            }
        }

        return sortArray;
    }

    public static void main(String[] args) {
        int[] arr = new int[]{95, 94, 91, 98, 99, 90, 99, 93, 91, 92};

        int[] brr = countSort(arr);

        System.out.println(Arrays.toString(arr));
        System.out.println(Arrays.toString(brr));
    }

進階一:

步驟:
第一步:求出原始數組的最大值和最小值
在這裏插入圖片描述
第二步:創建一個max - min + 1 長度的中間數組count
在這裏插入圖片描述
第三步:遍歷原始數組,並且將其映射到中間數組:以原數組中的元素作爲count數組的索引,以原數組中的元素出現次數作爲count數組的元素值。
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
第四步:對count數組變形,新元素的值是前面元素累加之和的值,即count[i+1] = count[i+1] + count[i];
在這裏插入圖片描述
第五步:創建一個和原始數組長度一樣的結果數組
在這裏插入圖片描述
第六步:從頭開始遍歷原始數組,並通過中間數組填充結果數組:當前元素A[j]減去最小值min,作爲索引,在計數數組中找到對應的元素值count[A[j]-min],再將count[A[j]-min]的值減去1,就是A[j]在結果數組result中的位置,做完上述這些操作,count[A[j]-min]自減1。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

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;
}

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

依舊以上一個數組{101,109,107,103,108,102,103,110,107,103}爲例,其中有兩個107,我們要實現第二個107在排序後依舊排在第一個107的後面,可以在第六步的時候,做下變動就可以實現,用倒序的方式遍歷原始數組,即從後往前遍歷A數組。

在這裏插入圖片描述
在這裏插入圖片描述

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;
}

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

答案時可以的。待研究

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

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

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

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

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

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

https://www.cnblogs.com/xiaochuan94/p/11198610.html
動畫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章