歸併排序

4. 歸併排序 請點評

 

插入排序算法採取增量式(Incremental)的策略解決問題,每次添一個元素到已排序的子序列中,逐漸將整個數組排序完畢,它的時間複雜度是O(n2)。下面介紹另一種典型的排序算法--歸併排序,它採取分而治之(Divide-and-Conquer)的策略,時間複雜度是Θ(nlgn)。歸併排序的步驟如下:

 

   1.

 

      Divide: 把長度爲n的輸入序列分成兩個長度爲n/2的子序列。

   2.

 

      Conquer: 對這兩個子序列分別採用歸併排序。

   3.

 

      Combine: 將兩個排序好的子序列合併成一個最終的排序序列。

 

在描述歸併排序的步驟時又調用了歸併排序本身,可見這是一個遞歸的過程。

 

例 11.2. 歸併排序

 

#include <stdio.h>

 

#define LEN 8

int a[LEN] = { 5, 2, 4, 7, 1, 3, 2, 6 };

 

void merge(int start, int mid, int end)

{

int n1 = mid - start + 1;

int n2 = end - mid;

int left[n1], right[n2];

int i, j, k;

 

for (i = 0; i < n1; i++) /* left holds a[start..mid] */

left[i] = a[start+i];

for (j = 0; j < n2; j++) /* right holds a[mid+1..end] */

right[j] = a[mid+1+j];

 

i = j = 0;

k = start;

while (i < n1 && j < n2)

if (left[i] < right[j])

a[k++] = left[i++];

else

a[k++] = right[j++];

 

while (i < n1) /* left[] is not exhausted */

a[k++] = left[i++];

while (j < n2) /* right[] is not exhausted */

a[k++] = right[j++];

}

 

void sort(int start, int end)

{

int mid;

if (start < end) {

mid = (start + end) / 2;

printf("sort (%d-%d, %d-%d) %d %d %d %d %d %d %d %d/n", 

      start, mid, mid+1, end, 

      a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]);

sort(start, mid);

sort(mid+1, end);

merge(start, mid, end);

printf("merge (%d-%d, %d-%d) to %d %d %d %d %d %d %d %d/n", 

      start, mid, mid+1, end, 

      a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]);

}

}

 

int main(void)

{

sort(0, LEN-1);

return 0;

}

 

執行結果是:

 

sort (0-3, 4-7) 5 2 4 7 1 3 2 6

sort (0-1, 2-3) 5 2 4 7 1 3 2 6

sort (0-0, 1-1) 5 2 4 7 1 3 2 6

merge (0-0, 1-1) to 2 5 4 7 1 3 2 6

sort (2-2, 3-3) 2 5 4 7 1 3 2 6

merge (2-2, 3-3) to 2 5 4 7 1 3 2 6

merge 0-1, 2-3) to 2 4 5 7 1 3 2 6

sort (4-5, 6-7) 2 4 5 7 1 3 2 6

sort (4-4, 5-5) 2 4 5 7 1 3 2 6

merge (4-4, 5-5) to 2 4 5 7 1 3 2 6

sort (6-6, 7-7) 2 4 5 7 1 3 2 6

merge (6-6, 7-7) to 2 4 5 7 1 3 2 6

merge (4-5, 6-7) to 2 4 5 7 1 2 3 6

merge (0-3, 4-7) to 1 2 2 3 4 5 6 7

 

 

sort函數把a[start..end]平均分成兩個子序列,分別是a[start..mid]和a[mid+1..end],對這兩個子序列分別遞歸調用sort函數進行排序,然後調用merge函數將排好序的兩個子序列合併起來,由於兩個子序列都已經排好序了,合併的過程很簡單,每次循環取兩個子序列中最小的元素進行比較,將較小的元素取出放到最終的排序序列中,如果其中一個子序列的元素已取完,就把另一個子序列剩下的元素都放到最終的排序序列中。爲了便於理解程序,我在sort函數開頭和結尾插了打印語句,可以看出調用過程是這樣的:

 

圖 11.4. 歸併排序調用過程

歸併排序調用過程

 

圖中S表示sort函數,M表示merge函數,整個控制流程沿虛線所示的方向調用和返回。由於sort函數遞歸調用了自己兩次,所以各函數之間的調用關係呈樹狀結構。畫這個圖只是爲了清楚地展現歸併排序的過程,讀者在理解遞歸函數時一定不要全部展開來看,而是要抓住Base Case和遞推關係來理解。我們分析一下歸併排序的時間複雜度,以下分析出自[算法導論]。

 

首先分析merge函數的時間複雜度。在merge函數中演示了C99的新特性--可變長數組,當然也可以避免使用這一特性,比如把left和right都按最大長度LEN分配。不管用哪種辦法,定義數組並分配存儲空間的執行時間都可以看作常數,與數組的長度無關,常數用Θ-notation記作Θ(1)。設子序列a[start..mid]的長度爲n1,子序列[mid+1..end]的長度爲n2,a[start..end]的總長度爲n=n1+n2,則前兩個for循環的執行時間是Θ(n1+n2),也就是Θ(n),後面三個for循環合在一起看,每走一次循環就會在最終的排序序列中確定一個元素,最終的排序序列共有n個元素,所以執行時間也是Θ(n)。兩個Θ(n)再加上若干常數項,merge函數總的執行時間仍是Θ(n),其中n=end-start+1。

 

然後分析sort函數的時間複雜度,當輸入長度n=1,也就是start==end時,if條件不成立,執行時間爲常數Θ(1),當輸入長度n>1時:

 

總的執行時間 = 2 × 輸入長度爲n/2的sort函數的執行時間 + merge函數的執行時間Θ(n)

 

設輸入長度爲n的sort函數的執行時間爲T(n),綜上所述:

 

這是一個遞推公式(Recurrence),我們需要消去等號右側的T(n),把T(n)寫成n的函數。其實符合一定條件的Recurrence的展開有數學公式可以套,這裏我們略去嚴格的數學證明,只是從直觀上看一下這個遞推公式的結果。當n=1時可以設T(1)=c1,當n>1時可以設T(n)=2T(n/2)+c2n,我們取c1和c2中較大的一個設爲c,把原來的公式改爲:

 

這樣計算出的結果應該是T(n)的上界。下面我們把T(n/2)展開成2T(n/4)+cn/2(下圖中的(c)),然後再把T(n/4)進一步展開,直到最後全部變成T(1)=c(下圖中的(d)):

 

把圖(d)中所有的項加起來就是總的執行時間。這是一個樹狀結構,每一層的和都是cn,共有lgn+1層,因此總的執行時間是cnlgn+cn,相比nlgn來說,cn項可以忽略,因此T(n)的上界是Θ(nlgn)。

 

如果先前取c1和c2中較小的一個設爲c,計算出的結果應該是T(n)的下界,然而推導過程一樣,結果也是Θ(nlgn)。既然T(n)的上下界都是Θ(nlgn),顯然T(n)就是Θ(nlgn)。

 

和插入排序的平均情況相比歸併排序更快一些,雖然merge函數的步驟較多,引入了較大的常數、係數和低次項,但是對於較大的輸入長度n,這些都不是主要因素,歸併排序的時間複雜度是Θ(nlgn),而插入排序的平均情況是Θ(n2),這就決定了歸併排序是更快的算法。但是不是任何情況下歸併排序都優於插入排序呢?哪些情況適用插入排序而不適用歸併排序?留給讀者思考。

習題 請點評

 

1、快速排序是另外一種採用分而治之策略的排序算法,在平均情況下的時間複雜度也是Θ(nlgn),但比歸併排序有更小的時間常數。它的基本思想是這樣的:

 

int partition(int start, int end)

{

從a[start..end]中選取一個pivot元素(比如選a[start]爲pivot);

在一個循環中移動a[start..end]的數據,將a[start..end]分成兩半,

使a[start..mid-1]比pivot元素小,a[mid+1..end]比pivot元素大,而a[mid]就是pivot元素;

return mid;

}

 

void quicksort(int start, int end)

{

int mid;

if (end > start) {

mid = partition(start, end);

quicksort(start, mid-1);

quicksort(mid+1, end);

}

}

發佈了22 篇原創文章 · 獲贊 12 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章