上一篇文章中咱們總結了經常使用的比較排序算法,主要有冒泡排序,選擇排序,插入排序,歸併排序,堆排序,快速排序等。html
這篇文章中咱們來探討一下經常使用的非比較排序算法:計數排序,基數排序,桶排序。在必定條件下,它們的時間複雜度能夠達到O(n)。ios
這裏咱們用到的惟一數據結構就是數組,固然咱們也能夠利用鏈表來實現下述算法。git
計數排序用到一個額外的計數數組C,根據數組C來將原數組A中的元素排到正確的位置。算法
通俗地理解,例若有10個年齡不一樣的人,假如統計出有8我的的年齡不比小明大(即小於等於小明的年齡,這裏也包括了小明),那麼小明的年齡就排在第8位,經過這種思想能夠肯定每一個人的位置,也就排好了序。固然,年齡同樣時須要特殊處理(保證穩定性):經過反向填充目標數組,填充完畢後將對應的數字統計遞減,能夠確保計數排序的穩定性。數組
計數排序的步驟以下:數據結構
計數排序的實現代碼以下:函數
#include<iostream> using namespace std; // 分類 ------------ 內部非比較排序 // 數據結構 --------- 數組 // 最差時間複雜度 ---- O(n + k) // 最優時間複雜度 ---- O(n + k) // 平均時間複雜度 ---- O(n + k) // 所需輔助空間 ------ O(n + k) // 穩定性 ----------- 穩定 const int k = 100; // 基數爲100,排序[0,99]內的整數 int C[k]; // 計數數組 void CountingSort(int A[], int n) { for (int i = 0; i < k; i++) // 初始化,將數組C中的元素置0(此步驟可省略,整型數組元素默認值爲0) { C[i] = 0; } for (int i = 0; i < n; i++) // 使C[i]保存着等於i的元素個數 { C[A[i]]++; } for (int i = 1; i < k; i++) // 使C[i]保存着小於等於i的元素個數,排序後元素i就放在第C[i]個輸出位置上 { C[i] = C[i] + C[i - 1]; } int *B = (int *)malloc((n) * sizeof(int));// 分配臨時空間,長度爲n,用來暫存中間數據 for (int i = n - 1; i >= 0; i--) // 從後向前掃描保證計數排序的穩定性(重複元素相對次序不變) { B[--C[A[i]]] = A[i]; // 把每一個元素A[i]放到它在輸出數組B中的正確位置上 // 當再遇到重複元素時會被放在當前元素的前一個位置上保證計數排序的穩定性 } for (int i = 0; i < n; i++) // 把臨時空間B中的數據拷貝回A { A[i] = B[i]; } free(B); // 釋放臨時空間 } int main() { int A[] = { 15, 22, 19, 46, 27, 73, 1, 19, 8 }; // 針對計數排序設計的輸入,每個元素都在[0,100]上且有重複元素 int n = sizeof(A) / sizeof(int); CountingSort(A, n); printf("計數排序結果:"); for (int i = 0; i < n; i++) { printf("%d ", A[i]); } printf("\n"); return 0; }
下圖給出了對{ 4, 1, 3, 4, 3 }進行計數排序的簡單演示過程spa
計數排序的時間複雜度和空間複雜度與數組A的數據範圍(A中元素的最大值與最小值的差加上1)有關,所以對於數據範圍很大的數組,計數排序須要大量時間和內存。設計
例如:對0到99之間的數字進行排序,計數排序是最好的算法,然而計數排序並不適合按字母順序排序人名,將計數排序用在基數排序算法中,可以更有效的排序數據範圍很大的數組。code
基數排序的發明能夠追溯到1887年赫爾曼·何樂禮在打孔卡片製表機上的貢獻。它是這樣實現的:將全部待比較正整數統一爲一樣的數位長度,數位較短的數前面補零。而後,從最低位開始進行基數爲10的計數排序,一直到最高位計數排序完後,數列就變成一個有序序列(利用了計數排序的穩定性)。
基數排序的實現代碼以下:
#include<iostream> using namespace std; // 分類 ------------- 內部非比較排序 // 數據結構 ---------- 數組 // 最差時間複雜度 ---- O(n * dn) // 最優時間複雜度 ---- O(n * dn) // 平均時間複雜度 ---- O(n * dn) // 所需輔助空間 ------ O(n * dn) // 穩定性 ----------- 穩定 const int dn = 3; // 待排序的元素爲三位數及如下 const int k = 10; // 基數爲10,每一位的數字都是[0,9]內的整數 int C[k]; int GetDigit(int x, int d) // 得到元素x的第d位數字 { int radix[] = { 1, 1, 10, 100 };// 最大爲三位數,因此這裏只要到百位就知足了 return (x / radix[d]) % 10; } void CountingSort(int A[], int n, int d)// 依據元素的第d位數字,對A數組進行計數排序 { for (int i = 0; i < k; i++) { C[i] = 0; } for (int i = 0; i < n; i++) { C[GetDigit(A[i], d)]++; } for (int i = 1; i < k; i++) { C[i] = C[i] + C[i - 1]; } int *B = (int*)malloc(n * sizeof(int)); for (int i = n - 1; i >= 0; i--) { int dight = GetDigit(A[i], d); // 元素A[i]當前位數字爲dight B[--C[dight]] = A[i]; // 根據當前位數字,把每一個元素A[i]放到它在輸出數組B中的正確位置上 // 當再遇到當前位數字同爲dight的元素時,會將其放在當前元素的前一個位置上保證計數排序的穩定性 } for (int i = 0; i < n; i++) { A[i] = B[i]; } free(B); } void LsdRadixSort(int A[], int n) // 最低位優先基數排序 { for (int d = 1; d <= dn; d++) // 從低位到高位 CountingSort(A, n, d); // 依據第d位數字對A進行計數排序 } int main() { int A[] = { 20, 90, 64, 289, 998, 365, 852, 123, 789, 456 };// 針對基數排序設計的輸入 int n = sizeof(A) / sizeof(int); LsdRadixSort(A, n); printf("基數排序結果:"); for (int i = 0; i < n; i++) { printf("%d ", A[i]); } printf("\n"); return 0; }
下圖給出了對{ 329, 457, 657, 839, 436, 720, 355 }進行基數排序的簡單演示過程
基數排序的時間複雜度是O(n * dn),其中n是待排序元素個數,dn是數字位數。這個時間複雜度不必定優於O(n log n),dn的大小取決於數字位的選擇(好比比特位數),和待排序數據所屬數據類型的全集的大小;dn決定了進行多少輪處理,而n是每輪處理的操做數目。
若是考慮和比較排序進行對照,基數排序的形式複雜度雖然不必定更小,但因爲不進行比較,所以其基本操做的代價較小,並且若是適當的選擇基數,dn通常不大於log n,因此基數排序通常要快過基於比較的排序,好比快速排序。因爲整數也能夠表達字符串(好比名字或日期)和特定格式的浮點數,因此基數排序並非只能用於整數排序。
桶排序也叫箱排序。工做的原理是將數組元素映射到有限數量個桶裏,利用計數排序能夠定位桶的邊界,每一個桶再各自進行桶內排序(使用其它排序算法或以遞歸方式繼續使用桶排序)。
桶排序的實現代碼以下:
#include<iostream> using namespace std; // 分類 ------------- 內部非比較排序 // 數據結構 --------- 數組 // 最差時間複雜度 ---- O(nlogn)或O(n^2),只有一個桶,取決於桶內排序方式 // 最優時間複雜度 ---- O(n),每一個元素佔一個桶 // 平均時間複雜度 ---- O(n),保證各個桶內元素個數均勻便可 // 所需輔助空間 ------ O(n + bn) // 穩定性 ----------- 穩定 /* 本程序用數組模擬桶 */ const int bn = 5; // 這裏排序[0,49]的元素,使用5個桶就夠了,也能夠根據輸入動態肯定桶的數量 int C[bn]; // 計數數組,存放桶的邊界信息 void InsertionSort(int A[], int left, int right) { for (int i = left + 1; i <= right; i++) // 從第二張牌開始抓,直到最後一張牌 { int get = A[i]; int j = i - 1; while (j >= left && A[j] > get) { A[j + 1] = A[j]; j--; } A[j + 1] = get; } } int MapToBucket(int x) { return x / 10; // 映射函數f(x),做用至關於快排中的Partition,把大量數據分割成基本有序的數據塊 } void CountingSort(int A[], int n) { for (int i = 0; i < bn; i++) { C[i] = 0; } for (int i = 0; i < n; i++) // 使C[i]保存着i號桶中元素的個數 { C[MapToBucket(A[i])]++; } for (int i = 1; i < bn; i++) // 定位桶邊界:初始時,C[i]-1爲i號桶最後一個元素的位置 { C[i] = C[i] + C[i - 1]; } int *B = (int *)malloc((n) * sizeof(int)); for (int i = n - 1; i >= 0; i--)// 從後向前掃描保證計數排序的穩定性(重複元素相對次序不變) { int b = MapToBucket(A[i]); // 元素A[i]位於b號桶 B[--C[b]] = A[i]; // 把每一個元素A[i]放到它在輸出數組B中的正確位置上 // 桶的邊界被更新:C[b]爲b號桶第一個元素的位置 } for (int i = 0; i < n; i++) { A[i] = B[i]; } free(B); } void BucketSort(int A[], int n) { CountingSort(A, n); // 利用計數排序肯定各個桶的邊界(分桶) for (int i = 0; i < bn; i++) // 對每個桶中的元素應用插入排序 { int left = C[i]; // C[i]爲i號桶第一個元素的位置 int right = (i == bn - 1 ? n - 1 : C[i + 1] - 1);// C[i+1]-1爲i號桶最後一個元素的位置 if (left < right) // 對元素個數大於1的桶進行桶內插入排序 InsertionSort(A, left, right); } } int main() { int A[] = { 29, 25, 3, 49, 9, 37, 21, 43 };// 針對桶排序設計的輸入 int n = sizeof(A) / sizeof(int); BucketSort(A, n); printf("桶排序結果:"); for (int i = 0; i < n; i++) { printf("%d ", A[i]); } printf("\n"); return 0; }
下圖給出了對{ 29, 25, 3, 49, 9, 37, 21, 43 }進行桶排序的簡單演示過程
桶排序不是比較排序,不受到O(nlogn)下限的影響,它是鴿巢排序的一種概括結果,當所要排序的數組值分散均勻的時候,桶排序擁有線性的時間複雜度。