前言
我們知道,通過比較兩個數大小來進行排序的算法(比如插入排序,合並排序,以及上文提到的快速排序等)的時間復雜度至少是Θ(nlgn),這是因為比較排序對應的決策樹的高度至少是Θ(nlgn),所以排序最壞情況肯定是Θ(nlgn)。那有沒有哪種排序算法的時間復雜度是線性的(Θ(n))呢?(因為我們如果要對一個數組排序,肯定至少要考察每個元素,因此可以推斷Θ(n)是所有排序算法的下界)。答案是:在一定條件下,是有的。
思考過程
為了更好的理解計數排序,我們先來想象一下如果一個數組里所有元素都是整數,而且都在0-k以內。那對於數組里每個元素來說,如果我能知道數組里有多少項小於或等於該元素。我就能准確地給出該元素在排序后的數組的位置。
拿上圖這個數組來說,元素5之前有8個元素小於等於5(含5本身),因此我排序后5所在的位置肯定是8.所以我只要構造一個(k+1)大小的數組,里面存下所有對應A中每個元素之前的元素個數,理論上就能在線性時間內完成排序。
算法過程
根據以上說明,我們能得出計數算法的過程:
- 初始化一個大小為(k+1)的數組C(所有元素初始值為0),遍歷整個待排序數組A,將A中每個元素對應C中的元素大小+1。操作結果見下圖:
我們可以得到原數組中有2個0,0個1,2個2,3個3,0個4,1個5.
2.我們將C中每個i位置的元素大小改成C數組前i項和(基於之前的算法思考,我們不難理解這么做的道理):
3.OK,現在我們已經快看到成功的曙光了。現在要做的就是初始化一個和A同樣大小的數組B用於存儲排序后數組,然后倒序遍歷A中元素(后面會提到為何要倒序遍歷),通過查找C數組,將該元素放置到B中相應的位置,同時將C中對應的元素大小-1(表明已經放置了一個這樣大小的元素,下次再放同樣大小的元素,就要往前擠一個位置)。遍歷完A數組后,就完成了所有的排序工作(只畫出了前3步):
最后排序結果B:
我們現在回過頭來思考一下為什么要限定A中是整數而且要限定元素大小?以及這個計數算法的時間復雜度是多少?
首先第一個問題,要知道我們要在C數組中存儲所有A中對應元素之前的元素個數,因此如果不是整數或者大小范圍無限大的話,我們就沒法構造C數組,加之我們要對C數組遍歷操作,如果K太大的話,這個算法的線性復雜度也就沒有任何意義了。所以限制是整數純粹只是為了限制C數組的大小,如果你想提出另外一種有限范圍的限制,比如都是整數或者0.5結尾的小數(1.5,3.5等)也是可以的,只要將C的數組大小變成2k+2就可以了,只不過這種假設幾乎沒有任何實際意義而已。
對於第二個問題,我們來看看算法過程:第一步我們遍歷了A數組,因此操作時間是Θ(n),第二步遍歷C數組操作時間是Θ(k),第三步遍歷A數組插入B,因此操作時間是也是Θ(n)。加起來時間復雜度就是Θ(n+k)。據此我們也能得到該算法的適用場景僅限於k較小的情況,如果k很大的話,就不如使用比較排序效率高了。
細心的讀者應該還記得我在前文提過要解釋為何要倒序遍歷A數組,我們觀察一下A數組中的3,我們可以看到有3個元素都等於3,對應位置:3,6,8。這3個3最后在5,6,7位置
我們是把8位置的3放在了7位置上,6位置的3放在了6位置上,3位置的3放在了5位置上。也就是說所有元素仍保持了之前的相對位置,我們稱這個性質為排序的穩定性。有可能有人會覺得這個穩定性看起來沒什么用,單純從計數排序結果看,確實沒什么用處,但是當在其他地方用到計數排序時,穩定性就非常有用了,比如我們在下一篇博客將要談到的基數排序。
最后附上計數排序的java代碼:
package sort; public class CountSort { private static int[] countSort(int[] array,int k) { int[] C=new int[k+1];//構造C數組 int length=array.length,sum=0;//獲取A數組大小用於構造B數組 int[] B=new int[length];//構造B數組 for(int i=0;i<length;i++) { C[array[i]]+=1;// 統計A中各元素個數,存入C數組 } for(int i=0;i<k+1;i++)//修改C數組 { sum+=C[i]; C[i]=sum; } for(int i=length-1;i>=0;i--)//遍歷A數組,構造B數組 { B[C[array[i]]-1]=array[i];//將A中該元素放到排序后數組B中指定的位置 C[array[i]]--;//將C中該元素-1,方便存放下一個同樣大小的元素 } return B;//將排序好的數組返回,完成排序 } public static void main(String[] args) { int[] A=new int[]{2,5,3,0,2,3,0,3}; int[] B=countSort(A, 5); for(int i=0;i<A.length;i++) { System.out.println((i+1)+"th:"+B[i]); } } }
排序結果: