計數排序--時間複雜度為線性的排序演算法
我們知道基於比較的排序演算法的最好的情況的時間複雜度是O(nlgn),然而存在一種神奇的排序演算法,不是基於比較的,而是空間換時間,使得時間複雜度能夠達到線性O(n+k),這種演算法就是本文將要介紹的計數排序。
一、適用情況
這個演算法在n個輸入元素中每一個都是0到k的範圍的整數,其中k也是整數。當k = O(n)時,排序的時間複雜度為Θ(n)。它的性質也就決定了它的運用範圍比較窄,但是對於一個待排序的有負數的陣列,我們可以將整個陣列的整體加上一個整數,使得整個陣列的最小值為0,然後就可以使用這個排序演算法了。而且這個演算法是穩定的。
二、基本思想
對一個一個待排序的元素x,我們可以確定小於等於x的個數i,根據這個個數i,我們就可以把x放到索引i處。那麼如何確定小於等於x的個數i呢?我們可以專門開闢一個數組c[],然後遍歷陣列,確定陣列a中每個元素中出現的頻率,然後就可以確定對於a中每個元素x,小於等於這個元素的個數。然後就可以把元素x放到對應位置了。當然元素x的大小是可能重複的,這樣就需要我們對陣列c的值訪問之後減1,保證和x一樣大的元素能放在其前面。
三、執行過程
1、對於陣列A,我們首先統計每個值的個數,將A的值作為C元素索引,值的個數作為C陣列的值。比如對於陣列A中的元素2,在陣列A中出現了2次,所以c[2] = 2,而元素5出現了以此,所以c[5] = 1。
2、至此為止,陣列C中已經統計了各個元素的出現次數,那麼我們就可以根據各個元素的出現次數,累加出比該元素小的元素個數,更新到陣列C中。比如a圖中,C[0]=2表示出現0的次數為2,C[1]=0表示出現1的次數為0,那麼小於等於1的元素個數為C[0]+C[1]=2,我們把C[1]更新為2,同理C[2]=2表示出現2的次數為2,那麼小於等於2的元素個數為C[1]+C[2]=4,繼續把C[2]更新為4,以此類推...
3、到這裡,我們得到了儲存小於等於元素的個數的陣列C。現在我們開始從尾部到頭部遍歷陣列A,比如首先我們看A[7] = 3,然後查詢C[3],發現C[3] = 7,說明有7個元素小於等於3。我們首先需要做一步C[3] = C[3] - 1,因為這裡雖然有7個元素小於等於3,但是B的索引是從0開始的,而且這樣減一可以保證下次再找到一個3,可以放在這個3的前面。然後B[C[3]] = 3,就把第一個3放到了對的位置。後面以此類推,直到遍歷完陣列B。
4、截至到這,我們就獲得了一個有序的陣列B。
四、程式碼實現
我們使用Java來實現這個演算法:
public static void countingSort(int[] a, int[] b, int k){ int[] c = new int[k+1];//存放0~k for(int i = 0; i<a.length; i++) c[a[i]] += 1; for(int i = 1; i<=k; i++) c[i] += c[i-1]; for(int i = a.length-1; i >= 0; i--){ c[a[i]] --; b[c[a[i]]] = a[i]; } }
簡簡單單幾行程式碼就實現了計數排序,其中引數a陣列表示待排序的陣列,b陣列表示排序之後的儲存陣列,k表示a陣列中最大的值。
五、時間複雜度分析和時間比較
開頭我們就說過了,這個演算法不是基於比較的排序演算法,因此它的下界可以優於Ω(nlgn),甚至這個演算法都沒有出現比較元素的操作。這個演算法很明顯是穩定的,也就是說具有相同值得元素在輸出陣列中的相對次序和他們在輸入陣列中的相對次序相同。演算法中的迴圈時間代價都是線性的,還有一個常數k,因此時間複雜度是Θ(n+k)。當k=O(n)時,我們採用計數排序就很好,總的時間複雜度為Θ(n)。
下面我們來和快速排序這個時間複雜度為Θ(2nlnn)的演算法在時間上進行比較,關於快速排序的演算法之前寫過,請點選此處,測試程式碼如下:
package acm;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Random;
public class CountingRank {
public static void main(String[] args) throws NumberFormatException, IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int length ;
int maxElement;
System.out.println("請輸入待測試陣列長度和陣列的最大元素:");
String input[] = br.readLine().split(" ");
length = Integer.parseInt(input[0]);
maxElement = Integer.parseInt(input[1]);
br.close();
int[] a = new int[length];
int[] b = new int[length];
Random random = new Random(System.currentTimeMillis());
for(int i = 0;i<a.length; i++){
a[i] = random.nextInt(maxElement+1);//產生[0,maxElement]的隨機數
}
double start1 = System.currentTimeMillis();
countingSort(a, b, maxElement);
double end1 = System.currentTimeMillis();
System.out.println(end1-start1);
double start2 = System.currentTimeMillis();
quickRank(a, 0, a.length-1);
double end2 = System.currentTimeMillis();
System.out.println(end2-start2);
}
private static int quickRankDivide(int[] arr, int left , int right){
int shaft = arr[left];//定義一個軸
int start = left++;//記錄軸元素位置 left++ 減少一次軸元素與自己的無用比較
while(left<right){//只要left在right的右邊 就持續進行
while(left<right&&arr[left]<=shaft){//查詢到不滿足軸擺放要求的元素就暫停
left++;//移動left指標
}//迴圈結束時找到了不滿足軸擺放要求的元素(大於軸元素的元素)
while(left<right&&arr[right]>shaft){
right--;//移動right指標
}//迴圈結束時找到了不滿足軸擺放要求的元素(小於軸元素的元素)
swap(arr, left , right);//交換left right指標的元素
left++;
right--;//交換完之後再次移動指標 減少無用比較次數
}
swap(arr,start,right);//擺放軸元素到準確的位置
return right;//返回軸元素的位置
}
private static void swap(int[] arr , int location1 , int location2){
int temp = arr[location1];
arr[location1] = arr[location2];
arr[location2] = temp;
}
public static void quickRank(int[] arr, int left, int right){//分治 遞迴
if(left<right){
int location = quickRankDivide(arr, left, right);//找到軸元素的位置
quickRank(arr,left,location-1);
quickRank(arr,location+1,right);//遞迴呼叫
}
}
public static void countingSort(int[] a, int[] b, int k){
int[] c = new int[k+1];//存放0~k
for(int i = 0; i<a.length; i++)
c[a[i]] += 1;
for(int i = 1; i<=k; i++)
c[i] += c[i-1];
for(int i = a.length-1; i >= 0; i--){
c[a[i]] --;
b[c[a[i]]] = a[i];
}
}
}
我們首先測試元素大小範圍為[0,10]的10000容量的陣列排序:
可以看到計數排序只花了1ms,而快排花了11ms,當陣列最大元素較小時,這個優勢是很明顯的。
接下來測試元素大小範圍為[0,10000]的10000容量的陣列排序:
當最大元素的大小達到10000的時候,這兩者的差距就很微弱了:一個是2ms,一個是3ms。
如果我們繼續增大陣列元素的最大值,達到1000000:
當元素最大值比較大的時候,計數排序就比不過快速排序了。
六、總結
計數排序是複雜度為O(n+k)的穩定的排序演算法,k是待排序列最大值,適用在對最大值不是很大的整型元素序列進行排序的情況下(整型元素可以有負數,我們可以把待排序列整體加上一個整數,使得待排序列的最小元素為0,然後執行計數排序,完成之後再變回來。這個操作是線性的,所以計數這樣做計數排序的複雜度仍然是O(n+k))。本質上是一種空間換時間的演算法,如果k比較小,計數排序的效率優勢是很明顯的,當k變得很大的時候,這個演算法可能就不如其他優秀的排序演算法(比如我們上面說的快速排序)。