【排序算法】計數排序

初始計數排序

摘自漫畫算法:

計數排序是一種不基于元素比較,利用數組索引來確定元素的正確位置的。

假設數組中有20個隨機整數,取值范圍0~10,要求用最快的速度把這20個整數從小到大進行排序。

如何給這些無序的隨機整數進行排序呢?

考慮到這些整數只能夠在0、1、2、3、4、5、6、7、8、9、10這11個數中取值,取值范圍有限。所以,可以根據這有限的范圍,建立一個長度為11的數組。數組索引從0到10,元素初始值全為0。

計數排序1.png

假設20個隨機整數的值如下所示:

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

下面就開始遍歷這個無序的隨機數列,每一個整數按照其值對號入座,同時,對應數組索引的元素進行加1操作。

例如第1個整數是9,那么數組索引為9的元素加1。

計數排序2.png

第2個整數是3,那么數組索引為3的元素加1。

計數排序3.png

以此類推。最終,當數列遍歷完畢時,數組的狀態如下:

計數排序4.png

該數組中每一個索引位置的值代表數列中對應整數出現的次數。

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

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

顯然,現在輸出的數列已經是有序的了。

注意:計數排序它適用于一定范圍內的整數排序。在取值范圍不是很大的情況下,它的性能甚至快過那些時間復雜度為O(nlogn)的排序。

計數排序的實現

整體代碼

import java.util.Arrays;

/**
 * 描述:計數排序
 * <p>
 * Create By ZhangBiao
 * 2020/5/31
 */
public class CountSort {

    /**
     * 計數排序
     *
     * @param arr
     * @return
     */
    public static int[] countSort(int[] arr) {
        // 1、得到數列的最大值
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        // 2、根據數列最大值確定統計數組的長度
        int[] countArray = new int[max + 1];
        // 3、遍歷數列,填充統計數組
        for (int i = 0; i < arr.length; i++) {
            countArray[arr[i]]++;
        }
        // 4、遍歷統計數組,輸出結果
        int index = 0;
        int[] sortedArray = new int[arr.length];
        for (int i = 0; i < countArray.length; i++) {
            for (int j = 0; j < countArray[i]; j++) {
                sortedArray[index++] = i;
            }
        }
        return sortedArray;
    }

    public static void main(String[] args) {
        int[] arr = new int[]{4, 4, 6, 5, 3, 2, 8, 1, 7, 5, 6, 0, 10};
        int[] sortedArray = countSort(arr);
        System.out.println(Arrays.toString(sortedArray));
    }

}

這段代碼在開頭有一個步驟,就是求數列的最大整數值max。后面創建的統計數組countArray,長度是max+1,以此來保證數組的最后一個下標是max。

計數排序的優化

從實現功能的角度來看,這段代碼可以實現整數的排序。但是這段代碼也存在一些問題。

當以數列的最大值來決定統計數組的長度,其實并不嚴謹。例如下面的數列:

95、94、91、98、99、90、99、93、91、92

在這個數列中最大值是99,但最小的整數是90。如果創建長度為100的數組,那么前面從0到89的空間位置就都浪費了!

怎么解決這個問題呢?

很簡單,只要不再以輸入數列的最大值+1作為統計數組的長度,而是以數列最大值 - 最小值 + 1作為統計數組的長度即可。

同時,數列的最小值作為一個偏移量,用于計算整數在統計數組中的索引。

以剛才的數列為例,統計出數組的長度為99 - 90 + 1 = 10,偏移量等于數列的最小值90。對于第1個整數95,對應的統計數組索引時95 - 90 = 5。

如圖所示:

計數排序 — 優化1.png

注意:以上確實對計數排序進行了優化。此外,樸素版的計數排序只是簡單地按照統計數組的索引輸出元素值,并沒有真正給原始數列進行排序。

如果只是單純地給整數排序,這樣做并沒有問題。但如果在現實業務里,例如給學生的考試分數進行排序,遇到相同的分數就會分不清是誰。

什么意思呢?讓我們看看下面的例子。

姓名 成績
小灰 90
大黃 99
小紅 95
小白 94
小綠 95

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

那么,當我們填充統計數組以后,只知道有兩個成績并列為95分的同學,卻不知道哪一個是小紅,哪一個是小綠。

計數排序 — 優化2.png

那么如何解決呢?在這種情況下,需要稍微改變之前的邏輯,在填充完統計數組以后,對統計數組做一下變形。

仍然以剛才的學生成績為例,將之前的統計數組變形成下面的樣子。

計數排序 — 優化3.png

這是如何變形的呢?其實就是從統計數組的第2個元素開始,每一個元素都加上前面所有元素之和。

為什么要相加呢?初次接觸的讀者可能會覺得莫名其妙。

這樣相加的目的,是讓統計數組存儲的元素值,等于相應整數的最終排序位置的序號。例如索引是9的元素值為5,代表原始數列的整數9,最終的排序在第5位。

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

第1步,遍歷成績表最后一行的小綠同學的成績。

小綠的成績是95分,找到countArray索引是5的元素,值是4,代表小綠的成績排名位置在第4位。

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

姓名 成績
小灰 90
大黃 99
小紅 95
小白 94
小綠 95
計數排序 — 優化4.png

第2步,遍歷成績倒數第2行的小白同學的成績。

小白的成績是94分,找到countArray索引是4的元素,值是2,代表小白的成績排名位置在第2位。

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

計數排序 — 優化5.png

第3步,遍歷成績表倒數第2行的小紅同學的成績。

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

計數排序 — 優化6.png

這樣一來,同樣是95分的小紅和小綠就能夠清除地排出順序了,也正因為此,優化版本的計數排序屬于穩定排序。

后面的遍歷過程以此類推,這里就不再詳細描述了。

整體實現代碼:

import java.util.Arrays;

/**
 * 描述:計數排序
 * <p>
 * Create By ZhangBiao
 * 2020/5/31
 */
public class CountSort {

    /**
     * 計數排序
     *
     * @param arr
     * @return
     */
    public static int[] countSort(int[] arr) {
        // 1、得到數列的最大值
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            if (arr[i] > max) {
                max = arr[i];
            }
        }
        // 2、根據數列最大值確定統計數組的長度
        int[] countArray = new int[max + 1];
        // 3、遍歷數列,填充統計數組
        for (int i = 0; i < arr.length; i++) {
            countArray[arr[i]]++;
        }
        // 4、遍歷統計數組,輸出結果
        int index = 0;
        int[] sortedArray = new int[arr.length];
        for (int i = 0; i < countArray.length; i++) {
            for (int j = 0; j < countArray[i]; j++) {
                sortedArray[index++] = i;
            }
        }
        return sortedArray;
    }

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

    public static void main(String[] args) {
        int[] arr = new int[]{95, 94, 91, 98, 99, 90, 99, 93, 91, 92};
        int[] sortedArray = countSort2(arr);
        System.out.println(Arrays.toString(sortedArray));
    }

}

計數排序的局限性

1、當數列最大和最小值差距過大時,并不適合用計數排序

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

2、當數列元素不是整數時,也不適合用計數排序

如果數列中的元素都是小數,如25.213,或0.00 000 001這樣的數字,則無法創建對應的統計數組。這樣顯然無法進行計數排序。

對于這些局限性,另一種線性時間排序算法做出了彌補,這種排序算法叫做桶排序。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370