初始計數排序
摘自漫畫算法:
計數排序是一種不基于元素比較,利用數組索引來確定元素的正確位置的。
假設數組中有20個隨機整數,取值范圍0~10,要求用最快的速度把這20個整數從小到大進行排序。
如何給這些無序的隨機整數進行排序呢?
考慮到這些整數只能夠在0、1、2、3、4、5、6、7、8、9、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操作。
例如第1個整數是9,那么數組索引為9的元素加1。
第2個整數是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)的排序。
計數排序的實現
整體代碼
import java.util.Arrays;
/**
* 描述:計數排序
* <p>
* Create By ZhangBiao
* 2020/5/31
*/
public class CountSort {
/**
* 計數排序
*
* @param arr
* @return
*/
public static int[] countSort(int[] arr) {
// 1、得到數列的最大值
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 2、根據數列最大值確定統計數組的長度
int[] countArray = new int[max + 1];
// 3、遍歷數列,填充統計數組
for (int i = 0; i < arr.length; i++) {
countArray[arr[i]]++;
}
// 4、遍歷統計數組,輸出結果
int index = 0;
int[] sortedArray = new int[arr.length];
for (int i = 0; i < countArray.length; i++) {
for (int j = 0; j < countArray[i]; j++) {
sortedArray[index++] = i;
}
}
return sortedArray;
}
public static void main(String[] args) {
int[] arr = new int[]{4, 4, 6, 5, 3, 2, 8, 1, 7, 5, 6, 0, 10};
int[] sortedArray = countSort(arr);
System.out.println(Arrays.toString(sortedArray));
}
}
這段代碼在開頭有一個步驟,就是求數列的最大整數值max。后面創建的統計數組countArray,長度是max+1,以此來保證數組的最后一個下標是max。
計數排序的優化
從實現功能的角度來看,這段代碼可以實現整數的排序。但是這段代碼也存在一些問題。
當以數列的最大值來決定統計數組的長度,其實并不嚴謹。例如下面的數列:
95、94、91、98、99、90、99、93、91、92
在這個數列中最大值是99,但最小的整數是90。如果創建長度為100的數組,那么前面從0到89的空間位置就都浪費了!
怎么解決這個問題呢?
很簡單,只要不再以輸入數列的最大值+1作為統計數組的長度,而是以數列最大值 - 最小值 + 1作為統計數組的長度即可。
同時,數列的最小值作為一個偏移量,用于計算整數在統計數組中的索引。
以剛才的數列為例,統計出數組的長度為99 - 90 + 1 = 10,偏移量等于數列的最小值90。對于第1個整數95,對應的統計數組索引時95 - 90 = 5。
如圖所示:
注意:以上確實對計數排序進行了優化。此外,樸素版的計數排序只是簡單地按照統計數組的索引輸出元素值,并沒有真正給原始數列進行排序。
如果只是單純地給整數排序,這樣做并沒有問題。但如果在現實業務里,例如給學生的考試分數進行排序,遇到相同的分數就會分不清是誰。
什么意思呢?讓我們看看下面的例子。
姓名 | 成績 |
---|---|
小灰 | 90 |
大黃 | 99 |
小紅 | 95 |
小白 | 94 |
小綠 | 95 |
給出一個學生成績表,要求按照成績從高到底進行排序,如果成績相同,則遵循原表固有順序。
那么,當我們填充統計數組以后,只知道有兩個成績并列為95分的同學,卻不知道哪一個是小紅,哪一個是小綠。
那么如何解決呢?在這種情況下,需要稍微改變之前的邏輯,在填充完統計數組以后,對統計數組做一下變形。
仍然以剛才的學生成績為例,將之前的統計數組變形成下面的樣子。
這是如何變形的呢?其實就是從統計數組的第2個元素開始,每一個元素都加上前面所有元素之和。
為什么要相加呢?初次接觸的讀者可能會覺得莫名其妙。
這樣相加的目的,是讓統計數組存儲的元素值,等于相應整數的最終排序位置的序號。例如索引是9的元素值為5,代表原始數列的整數9,最終的排序在第5位。
接下來,創建輸出數組sortedArray,長度和輸入數列一致。然后從后向前遍歷輸入數列。
第1步,遍歷成績表最后一行的小綠同學的成績。
小綠的成績是95分,找到countArray索引是5的元素,值是4,代表小綠的成績排名位置在第4位。
同時,給countArray索引是5的元素值減1,從4變成3,代表下次再遇到95分的成績時,最終排名是第3.
姓名 | 成績 |
---|---|
小灰 | 90 |
大黃 | 99 |
小紅 | 95 |
小白 | 94 |
小綠 | 95 |
第2步,遍歷成績倒數第2行的小白同學的成績。
小白的成績是94分,找到countArray索引是4的元素,值是2,代表小白的成績排名位置在第2位。
同時,給countArray索引是4的元素值減1,從2變成1,代表下次再遇到94分的成績時(實際上已經遇不到了),最終排名是1。
第3步,遍歷成績表倒數第2行的小紅同學的成績。
小紅的成績是95分,找到countArray索引是5的元素,值是3(最初是4,減1變成了3),代表小紅的成績排名位置在第3位。同時,給countArray索引是5的元素值減1,從3變成2,代表下次再遇到95分的成績時(實際上已經遇不到了),最終排名是第2。
這樣一來,同樣是95分的小紅和小綠就能夠清除地排出順序了,也正因為此,優化版本的計數排序屬于穩定排序。
后面的遍歷過程以此類推,這里就不再詳細描述了。
整體實現代碼:
import java.util.Arrays;
/**
* 描述:計數排序
* <p>
* Create By ZhangBiao
* 2020/5/31
*/
public class CountSort {
/**
* 計數排序
*
* @param arr
* @return
*/
public static int[] countSort(int[] arr) {
// 1、得到數列的最大值
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 2、根據數列最大值確定統計數組的長度
int[] countArray = new int[max + 1];
// 3、遍歷數列,填充統計數組
for (int i = 0; i < arr.length; i++) {
countArray[arr[i]]++;
}
// 4、遍歷統計數組,輸出結果
int index = 0;
int[] sortedArray = new int[arr.length];
for (int i = 0; i < countArray.length; i++) {
for (int j = 0; j < countArray[i]; j++) {
sortedArray[index++] = i;
}
}
return sortedArray;
}
/**
* 優化后的計數排序
*
* @param arr
* @return
*/
public static int[] countSort2(int[] arr) {
// 1、得到數列的最大值和最小值,并算出差值d
int max = arr[0];
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}
int d = max - min;
// 2、創建統計數組并統計對應元素的個數
int[] countArray = new int[d + 1];
for (int i = 0; i < arr.length; i++) {
countArray[arr[i] - min]++;
}
// 3、統計數組做變形,后面的元素等于前面的元素之和
for (int i = 1; i < countArray.length; i++) {
countArray[i] += countArray[i - 1];
}
// 4、倒序遍歷原始數列,從統計數組找到正確位置,輸出到結果數組
int[] sortedArray = new int[arr.length];
for (int i = arr.length - 1; i >= 0; i--) {
sortedArray[countArray[arr[i] - min] - 1] = arr[i];
countArray[arr[i] - min]--;
}
return sortedArray;
}
public static void main(String[] args) {
int[] arr = new int[]{95, 94, 91, 98, 99, 90, 99, 93, 91, 92};
int[] sortedArray = countSort2(arr);
System.out.println(Arrays.toString(sortedArray));
}
}
計數排序的局限性
1、當數列最大和最小值差距過大時,并不適合用計數排序
例如給出20個隨機數,范圍在0到1億之間,這時如果使用計數排序,需要創建長度為1億的數組。不但嚴重浪費空間,而且時間復雜度也會隨之升高。
2、當數列元素不是整數時,也不適合用計數排序
如果數列中的元素都是小數,如25.213,或0.00 000 001這樣的數字,則無法創建對應的統計數組。這樣顯然無法進行計數排序。
對于這些局限性,另一種線性時間排序算法做出了彌補,這種排序算法叫做桶排序。