有這樣一道排序題:數組中有20個隨機數,取值範圍爲0待10,要求用最快的速度將這20個整數從小到大進行排序。
第一時間你可能會使用快速排序,因爲快速排序的時間複雜度只有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操作。
比如第一個整數是9,那麼數組下標爲9的元素加1:
第二個整數是3,那麼數組下標爲3的元素加1:
繼續遍歷數列並修改數組…
最終,數列遍歷完畢時,數組的狀態如下:
數組中的每一個值,代表了數列中對應整數的出現次數。
有了這個統計結果,排序就很簡單了,直接遍歷數組,輸出數組元素的下標值,元素的值是幾,就輸出幾次:
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,如圖所示:
public static int[] countSort(int[] arr){
//1.得到數列的最值
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++){
if (arr[i] > max){
max = arr[i];
}
if (arr[i] < min){
min = arr[i];
}
}
//2.根據數列的最值確定統計數組的長度
int []countArray = new int[max - min + 1];
//3.遍歷數列,填充統計數組
for (int i = 0; i < arr.length; i++){
countArray[arr[i] - min]++;
}
//4.遍歷統計數組,輸出結果
int index = 0;
int[] sortArray = new int[arr.length];
for (int i = 0; i < countArray.length; i++){
while (countArray[i] > 0){
sortArray[index++] = i + min;
countArray[i]--;
}
}
return sortArray;
}
public static void main(String[] args) {
int[] arr = new int[]{95, 94, 91, 98, 99, 90, 99, 93, 91, 92};
int[] brr = countSort(arr);
System.out.println(Arrays.toString(arr));
System.out.println(Arrays.toString(brr));
}
進階一:
步驟:
第一步:求出原始數組的最大值和最小值
第二步:創建一個max - min + 1 長度的中間數組count
第三步:遍歷原始數組,並且將其映射到中間數組:以原數組中的元素作爲count數組的索引,以原數組中的元素出現次數作爲count數組的元素值。
第四步:對count數組變形,新元素的值是前面元素累加之和的值,即count[i+1] = count[i+1] + count[i];
第五步:創建一個和原始數組長度一樣的結果數組
第六步:從頭開始遍歷原始數組,並通過中間數組填充結果數組:當前元素A[j]減去最小值min,作爲索引,在計數數組中找到對應的元素值count[A[j]-min],再將count[A[j]-min]的值減去1,就是A[j]在結果數組result中的位置,做完上述這些操作,count[A[j]-min]自減1。
public int[] countSort3(int[] A) {
// 找出數組A中的最大值、最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int num : A) {
max = Math.max(max, num);
min = Math.min(min, num);
}
// 初始化計數數組count
// 長度爲最大值減最小值加1
int[] count = new int[max-min+1];
// 對計數數組各元素賦值
for (int num : A) {
// A中的元素要減去最小值,再作爲新索引
count[num-min]++;
}
// 計數數組變形,新元素的值是前面元素累加之和的值
for (int i=1; i<count.length; i++) {
count[i] += count[i-1];
}
// 創建結果數組
int[] result = new int[A.length];
// 遍歷A中的元素,填充到結果數組中去
for (int j=0; j<A.length; j++) {
result[count[A[j]-min]-1] = A[j];
count[A[j]-min]--;
}
return result;
}
如果我們想要原始數組中的相同元素按照本來的順序的排列,那該怎麼處理呢?
依舊以上一個數組{101,109,107,103,108,102,103,110,107,103}爲例,其中有兩個107,我們要實現第二個107在排序後依舊排在第一個107的後面,可以在第六步的時候,做下變動就可以實現,用倒序的方式遍歷原始數組,即從後往前遍歷A數組。
public int[] countSort4(int[] A) {
// 找出數組A中的最大值、最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int num : A) {
max = Math.max(max, num);
min = Math.min(min, num);
}
// 初始化計數數組count
// 長度爲最大值減最小值加1
int[] count = new int[max-min+1];
// 對計數數組各元素賦值
for (int num : A) {
// A中的元素要減去最小值,再作爲新索引
count[num-min]++;
}
// 計數數組變形,新元素的值是前面元素累加之和的值
for (int i=1; i<count.length; i++) {
count[i] += count[i-1];
}
// 創建結果數組
int[] result = new int[A.length];
// 遍歷A中的元素,填充到結果數組中去,從後往前遍歷
for (int j=A.length-1; j>=0; j--) {
result[count[A[j]-min]-1] = A[j];
count[A[j]-min]--;
}
return result;
}
既然從後往前遍歷原始數組的元素可以保證其原始排序,那麼從前往後可不可以達到相同的效果?
答案時可以的。待研究
雖然計數排序看上去很強大,但是它存在兩大侷限性:
1.當數列最大最小值差距過大時,並不適用於計數排序
比如給定20個隨機整數,範圍在0到1億之間,此時如果使用計數排序的話,就需要創建長度爲1億的數組,不但嚴重浪費了空間,而且時間複雜度也隨之升高。
2.當數列元素不是整數時,並不適用於計數排序
如果數列中的元素都是小數,比如3.1415,或是0.00000001這樣子,則無法創建對應的統計數組,這樣顯然無法進行計數排序。
正是由於這兩大侷限性,才使得計數排序不像快速排序、歸併排序那樣被人們廣泛適用。