序言
前面的章節主要講述的排序算法都是通過比較來得到已排序好的數列,我們通常稱這一類排序算法爲比較排序
。比如:插入排序(直接插入排序、折半插入排序、希爾排序),交換排序(冒泡排序,快速排序),選擇排序(簡單選擇排序,堆排序),歸併排序。這些排序算法都有一個共同的特點,就是基於比較。本篇博客主要介紹三個非比較排序算法:計數排序,基數排序,桶排序。他們是以線性時間運行,打破了以Ω(nlgn)爲下界。
在講下面的內容之前,我看到一篇博客,講述的比較排序的算法以動圖形式表示,便於快速理解:比較排序和決策樹
排序算法的下界
上面說到比較排序是指通過比較來決定元素間的相對次序,而在比較算法中通常只有<=, >,因爲通常等於在排序算法中是沒有意義的,基於排序算法,我們劃分爲這兩種形式,於是我們就可以用二叉樹來表示,我們管這種二叉樹叫做決策樹
。
決策樹模型(decision-tree model)
可以說任何一種比較排序算法都可以用決策樹來表示,決策樹的規模通常是隨着輸入規模n的一種指數級增長的二叉樹。它可以表示在給定輸入規模情況下,其一特定排序算法對所有元素的比較操作。其中的控制、數據移動等其他操作都被忽略了。有書中舉例的輸入規模爲3的待排序數列抽象成決策樹:
我們可以看出對於n個數的排序,我們可以得到 n!種排列順序,在決策樹中就是有 n!個葉結點。
最壞情況的下界
一個排序算法最壞情況的比較次數就等於其決策樹的高度,同時,當決策樹的每個排序情況都可以用可到達的葉結點表示時,該決策樹高度的下界也就是該排序算法運行時間的下界。
定理8.1: 在最壞情況下,任何比較排序算法都需要做Ω(nlgn)次比較。
推論8.2: 堆排序和歸併排序都是漸進最優的比較排序算法。
計數排序
計數排序假設n個輸入元素中的每一個都是在0到k區間內的一個整數,其中k爲某個整數。當k=O(n)時,排序的運行時間爲Θ(n)。
計數排序的基本思想是:對每一個輸入元素 x,確定小於x的元素個數。利用這一信息,就可以直接把x放到它在輸出數組中的位置上了。例如,如果有17個元素小於x,則x就應該在第18個輸出位置上。當有幾個元素相同時,這一方案要略做修改。因爲不能把它們放在同一個輸出位置上。
同時計數排序算法是一種穩定的算法,它的優勢在於在對一定範圍內的整數排序時,它的複雜度爲Ο(n+k)(其中k是整數的範圍),快於任何比較排序算法,當然這是一種犧牲空間換取時間的做法,而且當O(k)>O(nlgn)的時候其效率反而不如基於比較的排序(基於比較的排序的時間複雜度在理論上的下限是O(n*log(n)),如歸併排序,堆排序)
算法的步驟如下:
1.找出待排序的數組中最大和最小的元素
2.統計數組中每個值爲i的元素出現的次數,存入數組C的第i項
3.對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
4.反向填充目標數組:將每個元素i放在新數組的第C(i)項,每放一個元素就將C(i)減去1
根據書中的僞代碼爲:
COUNTING-SORT(A, B, k)
let C[0..k] be a new array
for i = O to k
C[i] = 0
for j=1 to A.length
C[A[j]]=C[A[j]]+1
for i=1 to k
C[i] = C[i] + C[i-1]
for j=A.length downto 1
B[C[A[j]]]=A[j]
C[A[j]]=C[A[j]]- 1
我將其轉化爲C語言形式:
void Counting_Sort(int* a, int* b, int k, int length) {
/*
這是一個計數排序算法,數組a是一個待排數列,數組b是已經排好的數列,
k爲數組中最大值,length爲數組的長度
*/
int* c; //定義一個儲存空間的數組
int i, j;
c = (int*)malloc((k + 1) * sizeof(int));
for (i = 0; i <= k; i++)
c[i] = 0;
for (j = 0; j < length; j++) //計數
c[a[j]] += 1;
for (i = 1; i <= k; i++)
c[i] = c[i] + c[i - 1];
for (j = length - 1; j >= 0; j--) {
b[c[a[j]]] = a[j];
c[a[j]] = c[a[j]] - 1;
}
free(c);
}
實現過程爲:
基數排序
基數排序是一種用在卡片排序機上的算法,普通的卡片有80列,每一列有12個孔,操作員根據卡片給定列上的數字來選定應該放入哪個孔,從而對所有的列的數字完成排序,如下一種直觀的顯示:
對於10進制數來說,每列只會用到10個數字,有多少位數就表示有多少列,對於每一列的數字,我們可以採用任何排序算法,但最好使用穩定排序,由於每一列的輸入的所有元素都爲0~10之間的元素(對於10進制數),很明顯,我們應該採用計數排序來進行(這裏就是計數排序穩定性的體現了)。如下爲使用計數排序來實現基數排序的代碼:
#include<stdio.h>
#define MAX 20
//#define SHOWPASS
#define BASE 10
void print(int *a, int n) {
int i;
for (i = 0; i < n; i++) {
printf("%d\t", a[i]);
}
}
void radixsort(int *a, int n) {
int i, b[MAX], m = a[0], exp = 1;
for (i = 1; i < n; i++) {
if (a[i] > m) {
m = a[i];
}
}
while (m / exp > 0) {
int bucket[BASE] = { 0 };
for (i = 0; i < n; i++) {
bucket[(a[i] / exp) % BASE]++;
}
for (i = 1; i < BASE; i++) {
bucket[i] += bucket[i - 1];
}
for (i = n - 1; i >= 0; i--) {
b[--bucket[(a[i] / exp) % BASE]] = a[i];
}
for (i = 0; i < n; i++) {
a[i] = b[i];
}
exp *= BASE;
#ifdef SHOWPASS
printf("\nPASS : ");
print(a, n);
#endif
}
}
int main() {
int arr[MAX];
int i, n;
printf("Enter total elements (n <= %d) : ", MAX);
scanf("%d", &n);
n = n < MAX ? n : MAX;
printf("Enter %d Elements : ", n);
for (i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
printf("\nARRAY : ");
print(&arr[0], n);
radixsort(&arr[0], n);
printf("\nSORTED : ");
print(&arr[0], n);
printf("\n");
return 0;
}
過程用動態圖表示,可以比較直觀的看出:
桶排序
桶排序的思想
- 得到無序數組的取值範圍
- 根據取值範圍創建對應數量的"桶"
- 遍歷數組,把每個元素放到對應的"桶"中
- 按照順序遍歷桶中的每個元素,依次放到數組中,即可完成數組的排序。
其中"桶"是一種容器,這個容器可以用多種數據結構實現,包括數組、隊列或者棧。
複雜度
- 時間複雜度:遍歷數組求最大值最小值爲O(n),遍歷數組放入"桶"中複雜度爲O(n),遍歷桶取出每個值的複雜度爲O(n),最終的時間複雜度爲O(3n),也就是O(n)
- 空間複雜度:額外的空間取決於元素的取值範圍,總的來說爲O(n)
- 穩定性:桶排序是否穩定取決於"桶"用什麼數據結構實現,如果是
隊列
,那麼可以保證相同的元素"取出去"後的相對位置與"放進來"之前是相同的,即排序是穩定
的,而如果用棧
來實現"桶",則排序一定是不穩定
的,因爲桶排序可以做到穩定,所以桶排序是穩定的排序算法
僞代碼爲:
過程大致是這樣的:
例如要對大小爲[1…1000]範圍內的n個整數A[1…n]排序,可以把桶設爲大小爲10的範圍,具體而言,設集合B[1]存儲[1…10]的整數,集合B[2]存儲(10…20]的整數,……集合B[i]存儲((i-1)10, i10]的整數,i = 1,2,…100。總共有100個桶。然後對A[1…n]從頭到尾掃描一遍,把每個A[i]放入對應的桶B[j]中。 然後再對這100個桶中每個桶裏的數字排序,這時可用冒泡,選擇,乃至快排,一般來說任何排序法都可以。最後依次輸出每個桶裏面的數字,且每個桶中的數字從小到大輸出,這樣就得到所有數字排好序的一個序列了。
C語言代碼實現:
#include<stdio.h>
#define Max_len 10 //數組元素個數
// 打印結果
void Show(int arr[], int n)
{
int i;
for ( i=0; i<n; i++ )
printf("%d ", arr[i]);
printf("\n");
}
//獲得未排序數組中最大的一個元素值
int GetMaxVal(int* arr, int len)
{
int maxVal = arr[0]; //假設最大爲arr[0]
for(int i = 1; i < len; i++) //遍歷比較,找到大的就賦值給maxVal
{
if(arr[i] > maxVal)
maxVal = arr[i];
}
return maxVal; //返回最大值
}
//桶排序 參數:數組及其長度
void BucketSort(int* arr , int len)
{
int tmpArrLen = GetMaxVal(arr , len) + 1;
int tmpArr[tmpArrLen]; //獲得空桶大小
int i, j;
for( i = 0; i < tmpArrLen; i++) //空桶初始化
tmpArr[i] = 0;
for(i = 0; i < len; i++) //尋訪序列,並且把項目一個一個放到對應的桶子去。
tmpArr[ arr[i] ]++;
for(i = 0, j = 0; i < tmpArrLen; i ++)
{
while( tmpArr[ i ] != 0) //對每個不是空的桶子進行排序。
{
arr[j ] = i; //從不是空的桶子裏把項目再放回原來的序列中。
j++;
tmpArr[i]--;
}
}
}
int main()
{ //測試數據
int arr_test[Max_len] = { 8, 4, 2, 3, 5, 1, 6, 9, 0, 7 };
//排序前數組序列
Show( arr_test, Max_len );
//排序
BucketSort( arr_test, Max_len);
//排序後數組序列
Show( arr_test, Max_len );
return 0;
結語
本篇博客參照算法導論,並用到其他人的博客幫助理解。由於自身知識水平有限,對於算法導論書中所提到的算法分析過程,推導過程,定理的證明過程不做解釋。待日後,個人水平提高後再做補充。