有這樣一道排序題:數組裏有20個隨機數,取值範圍爲從0到10,要求用最快的速度把這20個整數從小到大進行排序。java
第一時間你可能會想使用快速排序,由於快排的時間複雜度只有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操做。3d
好比第一個整數是9,那麼數組下標爲9的元素加1:code
第二個整數是3,那麼數組下標爲3的元素加1:blog
繼續遍歷數列並修改數組......排序
最終,數列遍歷完畢時,數組的狀態以下:class
數組中的每個值,表明了數列中對應整數的出現次數。
有了這個統計結果,排序就很簡單了,直接遍歷數組,輸出數組元素的下標值,元素的值是幾,就輸出幾回:
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,因爲代碼中第一、二、4步都涉及到遍歷原始數列,運算量都是N,第3步遍歷統計數列,運算量是M,因此整體運算量是3N+M,去掉係數,時間複雜度是O(N+M)。
至於空間複雜度,若是不考慮結果數組,只考慮統計數組的話,空間複雜度是O(M)。
雖然計數排序看上去很強大,可是它存在兩大侷限性:
1.當數列最大最小值差距過大時,並不適用於計數排序
好比給定20個隨機整數,範圍在0到1億之間,此時若是使用計數排序的話,就須要建立長度爲1億的數組,不但嚴重浪費了空間,並且時間複雜度也隨之升高。
2.當數列元素不是整數時,並不適用於計數排序
若是數列中的元素都是小數,好比3.1415,或是0.00000001這樣子,則沒法建立對應的統計數組,這樣顯然沒法進行計數排序。
正是因爲這兩大侷限性,才使得計數排序不像快速排序、歸併排序那樣被人們普遍適用。