轉自:http://blog.csdn.net/ns_code/article/details/20306991
前言
之所以把歸併排序和快速排序放在一起探討,很明顯兩者有一些相似之處:這兩種排序算法都採用了分治的思想。下面來逐個分析其實現思想。
歸併排序
實現思想
歸併的含義很明顯就是將兩個或者兩個以上的有序表組合成一個新的有序表。歸併排序中一般所用到的是2-路歸併排序,即將含有n個元素的序列看成是n個有序的子序列,每個子序列的長度爲1,而後兩兩合併,得到n/2個長度爲2或1的有序子序列,再進行兩兩合併。。。直到最後由兩個有序的子序列合併成爲一個長度爲n的有序序列。2-路歸併的核心操作是將一維數組中前後相鄰的兩個有序序列歸併爲一個有序序列。
下面一系列圖展示了2-路歸併排序的過程:
原始無序序列:
第一次需要對各相鄰元素進行兩兩歸併,歸併後結果如下:
第三次需要對上圖中相鄰色塊的元素進行兩兩歸併,歸併後的結果如下:
接下來便是最後一次兩兩歸併了,歸併後便的到了有序的序列,如下:
第一次實現的代碼
根據2-路歸併操作的思想,站在節省輔助空間的角度上考慮,我寫出的歸併操作的代碼如下:
- /*
- 將有序的arr[start...mid]和有序的arr[mid+1...end]歸併爲有序的arr[start...end]
- */
- void Merge(int *arr,int start,int mid,int end)
- {
- int i = start;
- int j = mid+1;
- int k = 0;
- //brr爲輔助數組,
- int *brr = (int *)malloc((end-start+1)*sizeof(int));
- //比較兩個有序序列中的元素,將較小的元素插入到brr中
- while(i<=mid && j<=end)
- {
- if(arr[i]<=arr[j])
- brr[k++] = arr[i++];
- else
- brr[k++] = arr[j++];
- }
- //將arr序列中剩餘的元素複製到brr中
- //這兩個語句只可能執行其中一個
- while(i<=mid)
- brr[k++] = arr[i++];
- while(j<=end)
- brr[k++] = arr[j++];
- //將brr中的元素複製到arr中,使arr[start...end]有序
- for(i=0;i<k;i++)
- arr[i+start] = brr[i];
- //釋放brr所佔的內存,並將其置爲空
- free(brr);
- brr = 0;
- }
調用上面的函數,得到的歸併排序的代碼應該是這樣的:
- /*
- 對arr[start...end]內的元素進行歸併排序
- 歸併排序後的順序爲從小到大
- */
- void MSort(int *arr,int start,int end)
- {
- if(start < end)
- {
- int mid = (start+end)/2;
- MSort(arr,start,mid); //左邊遞歸排序
- MSort(arr,mid+1,end); //右邊遞歸排序
- Merge(arr,start,mid,end); //左右序列歸併
- }
- }
- /*
- 將該排序算法封裝起來
- */
- void Merge_Sort(int *arr,int len)
- {
- MSort(arr,0,len-1);
- }
輸入任意數組來測試,結果也是正確的。
第二次實現的代碼
我看很多書上或者網上的例子給出的代碼都要在Merge函數中多傳入一個用來保存歸併後有序序列的數組,並在MSort函數中另外聲明一個臨時數組,傳給該參數,這樣每次遞歸調用的時候都要在局部聲明一個臨時數組,很不好。正當我對自己的代碼感覺良好時,看到了下面這段話:
Merge例程是精妙的。如果對Merge的每個調用均局部聲明一個臨時數組(本人備註:即在MSort函數中聲明),那麼在任意時刻就可能有logN個臨時數組處於活動期,這對於小內存的機器是致命的。另一方面,如果Merge例程動態分配並釋放最小量臨時內存,那麼由malloc佔用的時間會很多。嚴密測試指出,由於Merge位於MSort的最後一行,因此在任一時刻只需要一個臨時數組活動,而且可以使用該臨時數組的任意部分。(摘自Weiss數據結構與算法分析)
很明顯,我沒有考慮malloc所帶來的效率損耗,而且這裏說得很好,由於Merge位於MSort的最後一行,因此每一次遞歸調用中只會存在一個臨時數組,而不會有上一層遞歸中聲明的臨時數組(已經釋放掉了)。
爲了避免遞歸使用malloc和free,我們還是用這種經典的實現方式的好,代碼(一塊貼上完整的測試代碼)如下:
- /*******************************
- 歸併排序
- Author:蘭亭風雨 Date:2014-02-28
- Email:[email protected]
- ********************************/
- #include<stdio.h>
- #include<stdlib.h>
- /*
- 將有序的arr[start...mid]和有序的arr[mid+1...end]歸併爲有序的brr[0...end-start+1],
- 而後再將brr[0...end-start+1]複製到arr[start...end],使arr[start...end]有序
- */
- void Merge(int *arr,int *brr,int start,int mid,int end)
- {
- int i = start;
- int j = mid+1;
- int k = 0;
- //比較兩個有序序列中的元素,將較小的元素插入到brr中
- while(i<=mid && j<=end)
- {
- if(arr[i]<=arr[j])
- brr[k++] = arr[i++];
- else
- brr[k++] = arr[j++];
- }
- //將arr序列中剩餘的元素複製到brr中
- //這兩個語句只可能執行其中一個
- while(i<=mid)
- brr[k++] = arr[i++];
- while(j<=end)
- brr[k++] = arr[j++];
- //將brr中的元素複製到arr中,使arr[start...end]有序
- for(i=0;i<k;i++)
- arr[i+start] = brr[i];
- }
- /*
- 藉助brr數組對arr[start...end]內的元素進行歸併排序
- 歸併排序後的順序爲從小到大
- */
- void MSort(int *arr,int *brr,int start,int end)
- {
- if(start < end)
- {
- int mid = (start+end)/2;
- MSort(arr,brr,start,mid); //左邊遞歸排序
- MSort(arr,brr,mid+1,end); //右邊遞歸排序
- Merge(arr,brr,start,mid,end); //左右序列歸併
- }
- }
- /*
- 將該排序算法封裝起來
- */
- void Merge_Sort(int *arr,int len)
- {
- int *brr = (int *)malloc(len*sizeof(int));
- MSort(arr,brr,0,len-1);
- free(brr);
- brr = 0;
- }
- int main()
- {
- int num;
- printf("請輸入排序的元素的個數:");
- scanf("%d",&num);
- int i;
- int *arr = (int *)malloc(num*sizeof(int));
- printf("請依次輸入這%d個元素(必須爲整數):",num);
- for(i=0;i<num;i++)
- scanf("%d",arr+i);
- printf("歸併排序後的順序:");
- Merge_Sort(arr,num);
- for(i=0;i<num;i++)
- printf("%d ",arr[i]);
- printf("\n");
- free(arr);
- arr = 0;
- return 0;
- }
小總結
歸併排序的最好最壞和平均時間複雜度都是O(n*logn),但是需要額外的長度爲n的輔助數組(每次遞歸調用前都會釋放上次遞歸中傳入到Merge函數的brr數組),因此空間複雜度爲O(n),而不會因爲棧的最大深度爲O(logn)而積累至O(n*logn)。佔用額外空間是歸併排序不足的地方,但是它是幾個高效排序算法(快速排序、堆排序、希爾排序)中唯一穩定的排序方法。
快速排序
如名所示,快速排序是已知的平均時間複雜度均爲O(n*logn)的幾種排序算法中效率最高的一個,該算法之所以特別快,主要是由於非常精煉和高度優化的內部循環,它在最壞情況下的時間複雜度爲O(n*n),但只要稍加努力(正確選擇樞軸元素)就可以避免這種情形。
本部分的重點在於對分治思想的理解和代碼的書寫,不打算過多地討論樞軸元素的選擇,因爲這本身就不是一個簡單的問題,筆者對此也沒有什麼研究,更不敢造次。先來看實現思想。
實現思想
快速排序的基本思想如下:
1、從待排序列中任選一個元素作爲樞軸;
2、將序列中比樞軸大的元素全部放在樞軸的右邊,比樞軸小的元素全部放在其左邊;
3、以樞軸爲分界線,對其兩邊的兩個子序列重複執行步驟1和2中的操作,直到最後每個子序列中只有一個元素。
一趟快速排序(以排序後從小到大爲例)的具體做法如下:
附設兩個元素指針low和high,初值分別爲該序列的第一個元素的序號和最後一個元素的序號,設樞軸元素的值爲val,則首先從high所指位置起向前搜索到第一個值小於val的元素,並將其和val互換位置,而後從low所指位置起向後搜索到第一個值大於val的元素,並將其和val交換位置,如此反覆 ,直到low=high爲止。
我們上面說交換位置,只是爲了便於理解,我們在前面幾篇內部排序的博文中一直在強調,應儘量避免比較多的元素交換操作,因此下面的分析和代碼的實現中,我們並不是採取交換操作,而是先將樞軸元素保存在val變量中,然後每次遇到需要交換的元素時,先將該元素賦給val所在的位置,而後再將該元素所在位置“挖空”,之後的每一次比較,就用需要交換的元素來填充上次“挖空”的位置,同時將交換過來的元素所在的位置再“挖空”,以等待下次填充。
同樣爲了便於理解,我們以下面的序列爲例來展示快速排序的思想。
下圖爲無序序列的初始狀態,我們選取val爲第一個元素4,low和high分別指向4和5:
進行1次比較之後(即從high開始遇到比val小的元素):
進行2次比較之後(即從low開始遇到比val大的元素):
進行3次比較之後(再次從high開始向左搜索):
進行4次比較之後(再次從low開始向右搜索):
進行5次比較之後(再次從high開始向左搜索),此時向左high向左移動一個位置後,出現low=high,第一趟排序結束,我們將val插入到此時被挖空的位置,也即low或high所指向的位置。因此第一趟排序後的結果如下:
而後,我們分別對3、1、2和6、7、5進行同樣操作,最終便可以得到如下有序序列:
其中,加粗的元素爲每趟比較時選取的樞軸元素,我們這裏每次均選擇子序列的第一個元素爲樞軸。在這裏,4爲第一趟排序時選擇的樞軸,3和6爲第二趟排序時左右子序列分別選擇的樞軸,第二趟排序後,數組的後半部分已經有序,前半部分雖然也有序了,但是根據定義,我們需要再選1作爲樞軸,來對子序列{1,2}進行第三趟排序,這樣最終便得到了該有序序列。
實現代碼
很明顯,實現快速排序要用到遞歸,我們根據以上思想,實現的代碼如下(包含完整測試代碼):
- /*******************************
- 快速排序
- Author:蘭亭風雨 Date:2014-02-28
- Email:[email protected]
- ********************************/
- #include<stdio.h>
- #include<stdlib.h>
- void Quick_Sort(int *,int,int);
- int findPoss(int *,int,int);
- int main()
- {
- int num;
- printf("請輸入排序的元素的個數:");
- scanf("%d",&num);
- int i;
- int *arr = (int *)malloc(num*sizeof(int));
- printf("請依次輸入這%d個元素(必須爲整數):",num);
- for(i=0;i<num;i++)
- scanf("%d",arr+i);
- printf("快速排序後的順序:");
- Quick_Sort(arr,0,num-1);
- for(i=0;i<num;i++)
- printf("%d ",arr[i]);
- printf("\n");
- free(arr);
- arr = 0;
- return 0;
- }
- /*
- 快速排序函數,通過遞歸實現
- */
- void Quick_Sort(int *a,int low,int high)
- {
- int pos;
- if(low < high)
- {
- pos = findPoss(a,low,high);
- Quick_Sort(a,low,pos-1); //左邊子序列排序
- Quick_Sort(a,pos+1,high); //右邊子序列排序
- }
- }
- /*
- 該函數返回分割點數值所在的位置,a爲待排序數組的首地址,
- low剛開始表示排序範圍內的第一個元素的位置,逐漸向右移動,
- high剛開始表示排序範圍內的最後一個位置,逐漸向左移動
- */
- int findPoss(int *a,int low,int high)
- {
- int val = a[low];
- while(low < high)
- {
- while(low<high && a[high]>=val)
- high--;
- a[low] = a[high];
- while(low<high && a[low]<=val)
- low++;
- a[high] = a[low];
- }
- //最終low=high
- a[low] = val;
- return low;
- }
小總結
通常,快速排序被認爲在所有同數量級(平均時間複雜度均爲O(n*logn))的排序方法中,平均性能最好。但是若初始記錄已經基本有序,這樣每次如果還選擇第一個元素作爲樞軸元素,則再通過樞軸劃分子序列時,便會出現“一邊倒”的情況,此時快速排序就完全成了冒泡排序,這便是最壞的情況,時間複雜度爲O(n*n)。所以通常樞軸元素的選擇一般基於“三者取中”的原則,即比較首元素、末元素、中間元素的值,取三者中中間大小的那個。經驗表明,採取這種方法可以大大改善快速排序在最壞情況下的性能。
快速排序需要一個棧來實現遞歸,很明顯,如果序列中的元素是雜亂無章的,而且每次分割後的兩個子序列的長度相近,則棧的最大深度爲O(logn),而如果出現子序列“一邊倒”的情況,則棧的最大深度爲O(n)。因此就平均情況來看,快速排序的空間複雜度爲O(logn)。