這是小川的第385次更新,第413篇原創
01 計數排序算法概念
計數排序不是一個比較排序算法,該算法於1954年由 Harold H. Seward提出,通過計數將時間復雜度降到了O(N)
。
02 基礎版算法步驟
第一步:找出原數組中元素值最大的,記為max
。
第二步:創建一個新數組count
,其長度是max
加1,其元素默認值都為0。
第三步:遍歷原數組中的元素,以原數組中的元素作為count
數組的索引,以原數組中的元素出現次數作為count
數組的元素值。
第四步:創建結果數組result
,起始索引index
。
第五步:遍歷count
數組,找出其中元素值大於0的元素,將其對應的索引作為元素值填充到result
數組中去,每處理一次,count
中的該元素值減1,直到該元素值不大於0,依次處理count
中剩下的元素。
第六步:返回結果數組result
。
03 基礎版代碼實現
public int[] countSort(int[] A) {
// 找出數組A中的最大值
int max = Integer.MIN_VALUE;
for (int num : A) {
max = Math.max(max, num);
}
// 初始化計數數組count
int[] count = new int[max+1];
// 對計數數組各元素賦值
for (int num : A) {
count[num]++;
}
// 創建結果數組
int[] result = new int[A.length];
// 創建結果數組的起始索引
int index = 0;
// 遍歷計數數組,將計數數組的索引填充到結果數組中
for (int i=0; i<count.length; i++) {
while (count[i]>0) {
result[index++] = i;
count[i]--;
}
}
// 返回結果數組
return result;
}
04 優化版
基礎版能夠解決一般的情況,但是它有一個缺陷,那就是存在空間浪費的問題。
比如一組數據{101,109,108,102,110,107,103}
,其中最大值為110,按照基礎版的思路,我們需要創建一個長度為111的計數數組,但是我們可以發現,它前面的[0,100]
的空間完全浪費了,那怎樣優化呢?
將數組長度定為max-min+1
,即不僅要找出最大值,還要找出最小值,根據兩者的差來確定計數數組的長度。
public int[] countSort2(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]++;
}
// 創建結果數組
int[] result = new int[A.length];
// 創建結果數組的起始索引
int index = 0;
// 遍歷計數數組,將計數數組的索引填充到結果數組中
for (int i=0; i<count.length; i++) {
while (count[i]>0) {
// 再將減去的最小值補上
result[index++] = i+min;
count[i]--;
}
}
// 返回結果數組
return result;
}
05 進階版步驟
以數組A = {101,109,107,103,108,102,103,110,107,103}
為例。
第一步:找出數組中的最大值max
、最小值min
。
第二步:創建一個新數組count
,其長度是max-min加1,其元素默認值都為0。
第三步:遍歷原數組中的元素,以原數組中的元素作為count
數組的索引,以原數組中的元素出現次數作為count
數組的元素值。
第四步:對count
數組變形,新元素的值是前面元素累加之和的值,即count[i+1] = count[i+1] + count[i];
。
第五步:創建結果數組result
,長度和原始數組一樣。
第六步:遍歷原始數組中的元素,當前元素A[j]減去最小值min
,作為索引,在計數數組中找到對應的元素值count[A[j]-min]
,再將count[A[j]-min]的值減去1,就是A[j]
在結果數組result
中的位置,做完上述這些操作,count[A[j]-min]
自減1。
是不是對第四步和第六步有疑問?為什么要這樣操作?
第四步操作,是讓計數數組count
存儲的元素值,等於原始數組中相應整數的最終排序位置,即計算原始數組中的每個數字在結果數組中處於的位置。
比如索引值為9的count[9]
,它的元素值為10,而索引9對應的原始數組A
中的元素為9+101=110(要補上最小值min
,才能還原),即110在排序后的位置是第10位,即result[9] = 110,排完后count[9]
的值需要減1,count[9]
變為9。
再比如索引值為6的count[6]
,他的元素值為7,而索引6對應的原始數組A
中的元素為6+101=107,即107在排序后的位置是第7位,即result[6] = 107
,排完后count[6]
的值需要減1,count[6]
變為6。
如果索引值繼續為6,在經過上一次的排序后,count[6]
的值變成了6,即107在排序后的位置是第6位,即result[5] = 107
,排完后count[6]
的值需要減1,count[6]
變為5。
至於第六步操作,就是為了找到A中的當前元素在結果數組result
中排第幾位,也就達到了排序的目的。
06 進階版代碼實現
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;
}
07 進階版的延伸之一
如果我們想要原始數組中的相同元素按照本來的順序的排列,那該怎么處理呢?
依舊以上一個數組{101,109,107,103,108,102,103,110,107,103}
為例,其中有兩個107,我們要實現第二個107在排序后依舊排在第一個107的后面,可以在第六步的時候,做下變動就可以實現,用倒序的方式遍歷原始數組,即從后往前遍歷A
數組。
從后往前遍歷,第一次遇到107(A[8]
)時,107-101 = 6,count[6] = 7
,即第二個107要排在第7位,即result[6] = 107
,排序后count[6] = 6
。
繼續往前,第二次遇到107(A[2]
)時,107-101 = 6,count[6] = 6
,即第一個107要排在第6位,即result[5] = 107
,排序后count[6] = 5
。
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;
}
08 進階版的延伸之二
既然從后往前遍歷原始數組的元素可以保證其原始排序,那么從前往后可不可以達到相同的效果?
答案時可以的。
第一步:找出數組中的最大值max
、最小值min
。
第二步:創建一個新數組count
,其長度是max-min加1再加1
,其元素默認值都為0。
第三步:遍歷原數組中的元素,以原數組中的元素作為count
數組的索引,以原數組中的元素出現次數作為count
數組的元素值。
第四步:對count
數組變形,新元素的值是前面元素累加之和的值,即count[i+1] = count[i+1] + count[i];
。
第五步:創建結果數組result
,長度和原始數組一樣。
第六步:從前往后遍歷原始數組中的元素,當前元素A[j]
減去最小值min
,作為索引,在計數數組中找到對應的元素值count[A[j]-min]
,就是A[j]
在結果數組result
中的位置,做完上述這些操作,count[A[j]-min]
自增加1。
依舊以上一個數組{101,109,107,103,108,102,103,110,107,103}
為例,其中有兩個107,我們要實現第一個107在排序后依舊排在第二個107的前面。
此時計數數組count
為{0, 1, 2, 5, 5, 5, 5, 7, 8, 9, 10}
,從前往后遍歷原始數組A
中的元素。
第一次遇到107(A[2]
)時,107-101 = 6,count[6] = 5
,即第一個107在結果數組中的索引為5,即result[5] = 107
,排序后count[6] = 6
。
第二次遇到107(A[8]
)時,107-101 = 6,count[6] = 6
,即第二個107在結果數組中的索引為6,即result[6] = 107
,排序后count[6] = 7
。
public int[] countSort5(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,再加1
int[] count = new int[(max-min+1)+1];
// 對計數數組各元素賦值,count[0]永遠為0
for (int num : A) {
// A中的元素要減去最小值再加上1,再作為新索引
count[num-min+1]++;
}
// 計數數組變形,新元素的值是前面元素累加之和的值
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]] = A[j];
count[A[j]-min]++;
}
return result;
}
09 小結
以上就是計數排序算法的全部內容了,雖然它可以將排序算法的時間復雜度降低到O(N)
,但是有兩個前提需要滿足:一是需要排序的元素必須是整數,二是排序元素的取值要在一定范圍內,並且比較集中。只有這兩個條件都滿足,才能最大程度發揮計數排序的優勢。