寫在前面
大家都知道的是,基於比較的排序算法的時間複雜度的下界是 O(n log(n))。這一結論是可以證明的,所以在基於比較的算法中是找不到時間複雜度爲 O(n)的算法的。這時候,非基於比較的算法,如計數排序、基數排序和桶排序,是可以突破這個下界的。但是,非基於比較的排序的使用限制卻是較多的,如計數排序僅能對較小整數進行排序,且要求排序的數據的規模不能過大;基數排序可以對長整數進行排序,但是不適用於浮點數;桶排序可以對浮點數進行排序。
關於時間複雜度的問題在後續將陸續闡明。
基於比較的排序算法可以參考幾種常用的排序算法分析及實現。
計數排序
基本思想:計數排序要求數據的範圍在0到k之間的整數,引入了一個輔助數組C,數組C的大小爲k,存儲了待排序數組中值小於等於C的索引值的個數。
1. 統計出待排序數組值爲i的元素個數,存入輔助數組C的第i項中
2. 對C中的數據進行累加,每一項的值等於本項值加上前一項的值
3. 反向掃描待排序數組,每掃描一項m,將其存入新的數組的第C(m)項,對C(m)的值減一
具體過程
具體代碼
/**
* 計數排序
*
* @param A
* 要排序的數組
* @param k
* 數組中最大的元素值
* @return
*/
public static int[] countingSort(int[] A, int k) {
int n = A.length;
int[] C = new int[k];
int[] B = new int[n];
int i;
for (i = 0; i < k; i++) { // 初始化輔助數組
C[i] = 0;
}
for (i = 0; i < n; i++) { // 計數數組A中值等於C數組下標的個數
C[A[i]]++;
}
for (i = 1; i < k; i++) { // 計數數組A中值小於等於C數組下標的個數
C[i] += C[i - 1];
}
for (i = n - 1; i >= 0; i--) {
B[C[A[i]] - 1] = A[i];
C[A[i]]--;
}
return B;
}
簡要分析
- 計數排序僅適合於小範圍的數據進行排序
- 不能對浮點數進行排序
- 時間複雜度爲 O(n)
- 計數排序是穩定的(排序後值相同的元素相對於原先的位置是不會發生變化的)
基數排序
基本思想:基數排序是將整數按位進行排序,從低位開始,對每一位使用穩定的排序算法如計數排序進行排序,直到最高位排序完成,所有元素排序完成。
在這裏需要注意的是,我實現的基數排序是需要傳入整數數組中位數最長的元素的位數。否則在對每一位進行排序時,僅將每個整數的那一位均爲零作爲程序結束的標誌,將會出現如果測試數據的第某一位全爲零,則算法執行停止,出現錯誤。
具體過程
具體代碼
/**
* 對A中的數據(即整數的某一位組成的數組)進行計數排序,然後將結果保存爲已按照某一位排序的原始數據
*
* @param A
* 原始數據的某一位的數組
* @param k
* 每一位的範圍,一般傳入9(整數某位的大小至多爲9)
* @param D
* 原始數據數組
* @return
*/
public static int[] countingSort_r(int[] A, int k, int[] D) {
int n = A.length;
int[] C = new int[k];
int[] B = new int[n];
int i;
for (i = 0; i < k; i++) { // 初始化輔助數組
C[i] = 0;
}
for (i = 0; i < n; i++) { // 計數數組A中值等於C數組下標的個數
C[A[i]]++;
}
for (i = 1; i < k; i++) { // 計數數組A中值小於等於C數組下標的個數
C[i] += C[i - 1];
}
for (i = n - 1; i >= 0; i--) {
B[C[A[i]] - 1] = D[i]; // 注意這裏
C[A[i]]--;
}
return B;
}
/**
* 計數排序
*
* @param d
* 待排序的數組
*/
public static void radixSort(int[] d, int wordLength) {
int n = d.length;
int[] tmp = new int[n];
int base = 1;
while (wordLength != 0) {
base *= 10;
for (int i = 0; i < n; i++) { // 分離整數的數位
tmp[i] = d[i] % base;
tmp[i] /= base / 10;
}
int[] sorted = countingSort_r(tmp, 10, d); // 對每一位進行計數排序
for (int i = 0; i < n; i++) {
d[i] = sorted[i];
}
wordLength--;
}
}
簡要分析
- 基數排序僅可排序整數
- 與計數排序不同的是,基數排序可以排序大整數
- 對每一位進行排序時,需要使用穩定的排序算法,保證在排序高位時低位的順序不會變
- 時間複雜度爲 O(n)
桶排序
基本思想:對於一組程度爲N的待排序數據,將這些數據劃分爲M個區間(即放入M個桶中)。根據某種映射函數,將這N個數據放入M個桶中。然後對每個桶中的數據進行排序,最後依次輸出,得到已排序數據。桶排序要求待排序的元素都屬於一個固定的且有限的區間範圍內。
對於映射函數的選取,在這裏,假設數據都在[0,1)區間中,所以映射函數可以爲 f(x) = x * 10。
具體過程
具體代碼
/**
* 桶排序(要求待排數組元素的大小範圍在[0,1)之間)
*
* @param d
* 待排序的數組
*/
public static void bucketSort(double[] d) {
int n = d.length;
double[][] bucket = new double[10][10]; // 用一個二維數組來表示桶
for (int i = 0; i < 10; i++) {
bucket[i] = new double[10];
}
int[] count = new int[10];
for (int i = 0; i < 10; i++) { // 將每個桶的元素個數初始化爲零
count[i] = 0;
}
for (int i = 0; i < n; i++) {
double tmp = d[i];
int index = (int) (tmp * 10); // 將數據元素的小數點後第一位作爲桶的索引號
bucket[index][count[index]] = tmp;
int j = count[index]++;
while (j > 0 && tmp < bucket[index][j - 1]) // 對同一個桶內的元素進行插入排序
{
bucket[index][j] = bucket[index][j - 1];
j--;
}
bucket[index][j] = tmp;
}
int m = 0;
for (int i = 0; i < 10; i++) // 按序將桶內元素全部讀出來
{
for (int j = 0; j < count[i]; j++) {
d[m] = bucket[i][j];
m++;
}
}
}
簡要分析
- 桶排序對待排序數據的要求必須在某個指定的範圍內
- 可以用來排序浮點數,與計數排序和基數排序不同
- 桶排序是穩定的
- 速度快,但是耗空間
小結
從整體上來看,這三種排序算法都對數據的要求較爲嚴格,不如其他常用的基於比較的排序算法那樣普遍通用。這也說明了如果可以充分的利用起數據的特性,可以使計數排序的時間複雜度依賴於數據的範圍,桶排序還依賴於空間的大小和數據的分佈情況,基數排序一般情況下是需要用到計數排序。