1. 比較排序算法的下界
到目前為止,我們已經介紹了幾種能在O(nlgn)時間內排序n個數的算法:歸並排序和堆排序達到了最壞情況下的上界;快速排序在平均情況下達到該上界。
如果仔細觀察,我們會發現:在排序的最終結果中,各元素之間的次序依賴於它們之間的比較。我們把這類排序算法統稱為比較排序。到目前為止我們介紹的排序算法都是比較排序。下面我們來論證一個事實:任何比較排序算法在最壞情況下都要經過Ω(n lgn)次比較。
在證明之前,我們先介紹一種由比較排序抽象而來的決策樹模型。決策樹是一棵完全二叉樹,它可以表示在給定輸入規模情況下,某一特定排序算法對所有元素的比較操作,而控制,數據移動等操作都被忽略了。如下圖,它顯示的是插入排序算法作用於包含三個元素的輸入序列的決策樹情況。
從決策樹中我們可以看出:從根結點到任意一個可到達葉結點之間的最長簡單路徑的長度,表示的就是對應排序算法中最壞情況下的比較次數。因此,一個比較排序算法中的最壞情況的排序次數就等於決策樹的高度。並且,當決策樹中所有排列都是以可到達的葉結點的形式出現時,該決策樹高度的下界也就是比較排序算法運行時間的下界。下面我們正式給出證明。
考慮一棵高度為h,具有l個可到達葉結點的決策樹。它對應一個對n個元素進行的比較排序。因為輸入數據有n!種可能的排列都是葉結點,所以n!≤l。由於在一棵高度為h的二叉樹中,葉結點的數目不多於2^h,我們得到:
n! ≤ l ≤ 2^h,
兩邊取對數得:
h ≥ lg(n!) = Ω(nlgn)
2. 計數排序
我們先假設待排序序列各元素均在區間[0, k]上。
計數排序的思想是:在待排序序列中,如果我們能統計出有多少元素小於或等於某一個元素,我們也就知道了該元素的正確位置。例如,對於待排序序列{2,5,3,0,2,3,0,3},我們統計出有8個元素小於等於5(包括5自己),那么5這個元素就應該被排序到第8位。
下面給出算法的偽代碼描述:
其中數組A[1~n]是待排序數組;數組B[1~n]用來存放已排好序的元素。C[0~k]用來存放上面所說的統計數(具體的說C[i]就表示在數組A中,小於或等於i的元素的總個數)。
下面這幅圖描述的是對序列{2,5,3,0,2,3,0,3}排序的過程:
下面我們給出算法的Java實現代碼:
public static void main(String[] args) { int[] array = { 2, 5, 3, 0, 2, 3, 0, 3 }; printArray(countingSort(array, 5)); } /** * 計數排序 * * @param array * 待排序數組(假定各元素的范圍是0~max,包括0和max) * @param max * 待排序數組中的最大值 */ public static int[] countingSort(int[] array, int max) { int[] result = new int[array.length]; int[] temp = new int[max + 1]; // 以下循環操作完成后,temp的第i個位置保存着array中,值為i的元素的總個數 for (int i : array) { temp[i]++; } // 以下循環操作完成后,temp的第i個位置保存着array中,值小於或等於i的元素的總個數 for (int i = 1; i < temp.length; i++) { temp[i] += temp[i - 1]; } for (int i = array.length - 1; i > -1; i--) { result[temp[array[i]] - 1] = array[i]; temp[array[i]]--; } return result; } /** * 打印數組 */ public static void printArray(int[] array) { for (int i : array) { System.out.print(i + " "); } System.out.println(); }
3. 算法分析
我們現在來分析計數排序的時間代價。
在偽代碼中,第2~3行時間代價θ(k);第4~5行時間為θ(n);第7~8行時間為θ(k),第10~12行時間為θ(n)。因此,總的運行時間是θ(k+n)。當k= O(n)時,運行時間為θ(n)。
可以看出,計數排序的下界優於我們上面論證的比較排序算法的下界時間Ω(nlgn)。這是因為計數排序並不是比較排序算法。事實上,在代碼中從未出現比較某兩個元素大小的代碼。相反,計數排序是使用輸入元素的實際值來確定其在數組中的位置。此時,比較排序算法的模型對計數排序不再適用。