by: 潘雲登
date: 2009-7-12
email: [email protected]
homepage:
對於商業目的下對本文的任何行為需經作者同意。
寫在前面
1. 本文內容對應《演算法導論》(第2版)》第2章和第7章。
2. 比較了歸併排序與快速排序之間的不同策略,啟發對分治演算法的深入思考。
分治法有很多演算法在結構上是遞迴的:為了解決乙個給定的問題,演算法要一次或多次地遞迴呼叫其自身來解決相關的子問題。這些演算法通常採用分治策略(divide-and-conquier):將原問題劃分成n個規模較小而結構與原問題相似的子問題;遞迴地解決這些子問題,然後再合併其結果,就得到原問題的解。分治模式在每一層遞迴上都有三個步驟:
² 分解(divide):將原問題分解成一系列子問題;
² 解決(conquer):遞迴地解各子問題。若子問題足夠小,則直接求解;
² 合併:將子問題的結果合併成原問題的解。
自底向上的歸併排序
歸併排序演算法完全依照分治模式,直觀的操作如下:
² 分解:將n個元素分成各含n/2個元素的子串行;
² 解決:用歸併排序法對兩個子串行遞迴地排序;
² 合併:合併兩個已排序的子串行以得到排序結果。
觀察下面的例子,可以發現:歸併排序在分解時,只是單純地將原問題分解為兩個規模減半的子問題;在分解過程中,沒有任何對原問題所含資訊的利用,沒有任何嘗試對問題求解的動作;這種分解持續進行,直到子問題規模降足夠小(為1),這時子問題直接得解;然後,自底向上地合併子問題的解,這時才真正利用原問題的特定資訊,執行求解動作,對元素進行比較。
4 2 5 7 1 2 6 3
4 | 2 | 5 | 7 | 1 | 2 | 6 | 3
4 2 5 7 | 1 2 6 3
2 4 | 5 7 | 1 2 | 3 6
4 2 | 5 7 | 1 2 | 6 3
2 4 5 7 | 1 2 3 6
4 | 2 | 5 | 7 | 1 | 2 | 6 | 3
1 2 2 3 4 5 6 7
這種自底向上分治策略的程式設計模式如下:
如果問題規模足夠小,直接求解,否則
單純地分解原問題為規模更小的子問題,並持續這種分解;
執行求解動作,將子問題的解合併為原問題的解。
由於在自底向上的歸併過程中,每一層需要進行i組n/i次比較,而且由於進行的是單純的對稱分解,總的層數總是lg n,因此,歸併排序在各種情況下的時間代價都是θ(n lg n)。試想,能夠加大分組的力度,即每次將原問題分解為大於2的子問題,來降低執行時間?
歸併排序演算法的**如下:
* p: 左陣列第乙個元素下標
* q: 左陣列最後乙個元素下標
* r: 右陣列最後乙個元素下標
void merge_no_sentinel(int *array, int p, int q, int r)
int n1, n2, i, j, k;
int *left=null, *right=null;
n1 = q-p+1;
n2 = r-q;
left = (int *)malloc(sizeof(int)*(n1));
right = (int *)malloc(sizeof(int)*(n2));
for(i=0; ileft[i] = array[p+i];
for(j=0; jright[j] = array[q+1+j];
i = j = 0;
k = p;
while(iif(left[i] <= right[j])
array[k++] = left[i++];
else
array[k++] = right[j++];
for(; iarray[k++] = left[i];
for(; jarray[k++] = right[j];
free(left);
free(right);
left = null;
right = null;
void merge_sort(int *array, int p, int r)
int q;
if(p < r)
q = (int)((p+r)/2);
merge_sort(array, p, q);
merge_sort(array, q+1, r);
merge_no_sentinel(array, p, q, r);
自頂向下的快速排序
快速排序也是基於分治策略,它的三個步驟如下:
² 分解:陣列a[p..r]被劃分為兩個(可能空)子陣列a[p..q-1]和a[q+1..r],使得a[p..q-1]中的每個元素都小於等於a(q),而且,小於等於a[q+1..r]中的元素,下標q也在這個分解過程中進行計算;
² 解決:通過遞迴呼叫快速排序,對子陣列a[p..q-1]和a[q+1..r]排序;
² 合併:因為兩個子陣列是就地排序的,將它們的合併並不需要操作,整個a[p..r]已排序。
可以看到:快速排序與歸併排序不同,對原問題進行單純的對稱分解;其求解動作在分解子問題開始前進行,而問題的分解基於原問題本身包含的資訊;然後,自頂向下地遞迴求解每個子問題。可以通過下面的例子,觀察快速排序的執行過程。由於在快速排序過程中存在不是基於比較的位置交換,因此,快速排序是不穩定的。
4 2 5 7 1 2 6 | 3
2 1 2 | 3 | 7 4 5 6
1 | 2 | 2 | 3 | 4 5 | 6 | 7
1 | 2 | 2 | 3 | 4 | 5 | 6 | 7
這種自頂向下分治策略的程式設計模式如下:
如果問題規模足夠小,直接求解,否則
執行求解動作,將原問題分解為規模更小的子問題;
遞迴地求解每個子問題;
因為求解動作在分解之前進行,在對每個子問題求解之後,不需要合併過程。
快速排序的執行時間與分解是否對稱有關,而後者又與選擇了哪乙個元素來進行劃分有關。如果劃分是對稱的,則執行時間與歸併排序相同,為θ(n lg n)。如果每次分解都形成規模為n-1和0的兩個子問題,快速排序的執行時間將變為θ(n2)。快速排序的平均情況執行時間與其最佳情況相同,為θ(n lg n)。
快速排序演算法的**如下:
void swap(int *a, int *b)
int temp;
temp = *a;
*a = *b;
*b = temp;
* p: 陣列第乙個元素的下標
* r: 陣列最後乙個元素的下標
* 返回值為分組後主元的下標
int partition(int *array, int p, int r)
int i, j, pivot;
pivot = array[r];
i = p-1;
for(j=p; j<=r-1; j++)
if(array[j] <= pivot)
i++;
swap(&array[i], &array[j]);
swap(&array[i+1], &array[r]);
return i+1;
void quick_sort(int *array, int p, int r)
int q;
if(p < r)
q = partition(array, p, r);
quick_sort(array, p, q-1);
quick_sort(array, q+1, r);
通常,我們可以向乙個演算法中加入隨機化成分(參考第5章內容),以便對於所有輸入,它均能獲得較好的平均情況效能。將這種方法用於快速排序時,不是始終採用a[r]作為主元,而是從子陣列a[p..r]中隨機選擇乙個元素,即將a[r]與從a[p..r]中隨機選出的乙個元素交換。
#include
#include
int randomized_partition(int *array, int p, int r)
int i;
srand(time(null));
i = rand()%(r-p)+p;
swap(&array[i], &array[r]);
return partition(array, p, r);
void randomized_quick_sort(int *array, int p, int r)
int q;
if(p < r)
q = randomized_partition(array, p, r);
randomized_quick_sort(array, p, q-1);
randomized_quick_sort(array, q+1, r);
歸併排序和快速排序
歸併排序 先將問題分解為小問題即乙個個子序列,再將子串行按順序合併。class mergesort mergesort a,0 a.length 1 for int t a public static void mergesort int a,int m,int n public static vo...
歸併排序和快速排序
歸併排序的陣列排序任務可以如下完成 1 把前一半排序 2 把後一半排序 3 把兩半歸併到乙個新的有序陣列,然後再拷貝回原陣列,排序完成。include using namespace std void merge int a,int s,int m,int e,int tmp while p1 m ...
快速排序和歸併排序
遞推公式 merge sort p,r merge merge sort p,q merge sort q 1 r 終止條件 p r 10組測試資料 for let i 0 i 10 i 生成10個隨機元素的測試陣列 function gettestdata return ret 排序函式 func...