1. 程式人生 > 實用技巧 >資料結構與演算法——計數排序

資料結構與演算法——計數排序

原文連結:https://jiang-hao.com/articles/2020/algorithms-algorithms-count-sort.html

目錄

演算法介紹

計數排序(Counting sort)是一種穩定的線性時間排序演算法。該演算法於1954年由 Harold H. Seward 提出。計數排序使用一個額外的陣列C ,其中第i個元素是待排序陣列A中值等於i的元素的個數。然後根據陣列C來將A中的元素排到正確的位置。

當輸入的元素是 n 個 0 到 k 之間的整數時,它的執行時間是 Θ(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序演算法。

由於用來計數的陣列C的長度取決於待排序陣列中資料的範圍(等於待排序陣列的最大值與最小值的差加上1),這使得計數排序對於資料範圍很大的陣列,需要大量時間和記憶體。例如:計數排序是用來排序0到100之間的數字的最好的演算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的演算法來排序資料範圍很大的陣列。

我們使用下面的例子進行說明。

假設有20個隨機整數,其取值範圍在0~10之間,對其進行排序,由於這20個隨機整數的取值範圍是固定的,那麼我們可以定義一個長度為11的陣列,陣列下標為從0到10,並且元素初始值全為0,然後遍歷要排序的20個隨機整數,將遍歷到的數值對應的陣列下標進行+1,直到遍歷結束。如下圖所示:

假設我們要遍歷的20個隨機數為:9,3,5,4,8,9,1,2,7,8,5,3,6,7,9,0,4,7,2,4。建立一個長度為11的陣列如下所示:

然後我們開始遍歷,遍歷的第一個數字是9,則給新建立的陣列下標為9的值+1,即:

接著我們依此類推,直到遍歷完要排序的數,遍歷完之後的結果是:

根據上面的陣列的結果,我們就可以得到一個有序的數列,即:0,1,2,2,3,3,4,4,4,5,5,6,7,7,7,8,8,9,9,9。

演算法實現

public static int[] sort(int[] arr) {
    // 空陣列直接返回
    if (arr.length==0) return arr;
    // 新建陣列儲存排序後的結果
    int[] res = new int[arr.length];
    // 得到數列的最大值
    int max = arr[0];
    for (int i: arr) {
        if (max < i) max = i;
    }
    // 根據數列最大值確定統計陣列的長度
    int[] cntArr = new int[max+1];
    // 遍歷陣列,填充統計陣列
    for (int i: arr) {
        cntArr[i]++;
    }
    // 遍歷統計陣列,得到排好序後的陣列
    int index = 0;
    for (int i=0; i<cntArr.length; i++) {
        for (int j=0; j<cntArr[i]; j++) {
            res[index++] = i;
        }
    }
    return res;
}

演算法優化

問題1:

對於上面的計數排序,我們不難發現存在這樣一個問題:如果要排序的20個隨機整數的範圍在81~93之間,那麼我們就需要建立一個長度為94的陣列,這樣就會造成新建的陣列的前面81個空間位置就白白浪費了。

解決方法:

我們可以通過使用使用數列的最大值-最小值+1作為統計陣列的長度即可,同時使用最小值作為一個偏移量,用於計算整數在統計陣列中的下標。

問題2:

以上的計數排序只是按照輸入數列從小到大的一個順序對其進行輸出而已,並沒有對原始數列進行排序。如果遇到類似於對學生分數進行排序這樣的現實問題,比如對於相同分數的學生就會造成分不清分數對應的是誰這樣的問題。

解決方法:

假如現在有5個學生的分數為:(學生A:90,學生B:99,學生C:95,學生D:94,學生E:95),使用上面的方法得到的統計陣列如下所示:

我們首先對統計陣列進行變形,即從統計陣列的第2個元素開始,每個元素都加上前面的所有元素之和,得到的新的統計陣列如下所示:

然後,我們從後向前依次遍歷5位學生的分數,第1個為學生E95分,找到統計陣列下標為5的元素,值是4,代表學生E成績排名在第4位,然後對陣列下標為5的元素的值-1,表示下次再遇到成績為95分的,其位置就會在第3位,按照此方法就會得出最終的成績排名順序為:(學生A:90,學生B:94,學生C:95,學生D:95,學生E:99)。

優化後的計數排序的實現:

public static int[] sort(int[] arr) {
    // 空陣列直接返回
    if (arr.length==0) return arr;
    // 新建陣列儲存排序後的結果
    int[] res = new int[arr.length];
    // 得到數列的最大值,最小值
    int max = arr[0];
    int min = max;
    for (int i: arr) {
        if (max < i) max = i;
        if (min > i) min = i;
    }
    // 根據數列最大最小值確定統計陣列的長度
    int[] cntArr = new int[max-min+1];
    // 遍歷陣列,填充統計陣列
    for (int i: arr) cntArr[i-min]++;
    // 遍歷統計陣列累加計數
    for (int i=1; i<cntArr.length; i++) cntArr[i]+=cntArr[i-1];
    // 倒序遍歷陣列,得到排好序後的陣列
    for (int i=arr.length-1; i>=0; i--) res[--cntArr[arr[i]-min]] = arr[i];
    return res;
}