本文介紹了歸並排序的基本思想,遞歸方法的一般寫法,最后一步步手寫歸並排序,並對其性能進行了分析。
基本思想
歸並排序是建立在歸並操作上的一種有效的排序算法,該算法是采用分治法的一個非常典型的應用。即先使每個子序列有序,再將已有序的子序列合並,得到完全有序的序列。這里給出一種遞歸形式的歸並排序實現。
遞歸方法的一般寫法
遞歸方法的書寫主要有三步:
- 明確遞歸方法的功能邊界;
- 得到遞歸的遞推關系;
- 給定遞歸的終止條件。
遞歸方法均可按照這三步進行,切忌不要陷入遞歸實現的細節中。下面以歸並排序算法的書寫為例,來談一下遞歸方法的具體寫法。
手寫歸並排序
首先,明確遞歸方法的功能,這里我們定義方法的功能為,給定一個數組及左右邊界,方法完成數組邊界內元素的排序,如下:
private static void mergeSort(int[] arr,int left,int right);
先假設我們已經有了這么一個方法,不用管具體的實現。
接着,尋找遞推關系,什么是遞推關系呢?就是如何由子問題的求解,來得到原問題的求解,還是舉例說明,有如下的數組

我們將其拆分為左右兩部分,如下

遞推關系就是,假如左右兩部分都已經有序了,如何使整個數組有序?這個問題其實就是給定了一個數組,數組的左半部分有序,右半部分也有序,如何使整個數組有序?
首先,定義兩個指針,分別指向左側部分起始位置和右側部分起始位置,同時創建一個輔助數組和指向其初始位置的輔助指針

接着比較,左指針和右指針所對應的元素的大小,較小的元素填充至輔助數組,同時其對應的指針和輔助指針均加1,如下:

依次進行,直至某左指針指向中間位置或者右指針指向數組的末尾,此時要將將剩余的元素填充至輔助數組。所有的元素填充完成后,再將輔助數組中的元素填充回原數組即可。具體的代碼如下:
/**
*
* @param arr 要合並的數組
* @param left 左邊界
* @param mid 中間的分界
* @param right 右邊界
*/
private static void merge(int[] arr,int left,int mid,int right){
int[] helpArr = new int[right - left + 1];//首先定義一個輔助數組
int lPoint = left;//左指針
int rPoint = mid + 1;//右指針
int i = 0;//輔助指針
while(lPoint <= mid && rPoint <= right){//比較並填充輔助數組
if(arr[lPoint] <= arr[rPoint])
helpArr[i++] = arr[lPoint++];
else
helpArr[i++] = arr[rPoint++];
}
while(lPoint <= mid){//將剩余元素填充至輔助數組
helpArr[i++] = arr[lPoint++];
}
while(rPoint <= right){
helpArr[i++] = arr[rPoint++];
}
for(int j = 0;j < helpArr.length;j ++){//將輔助數組中的元素回填至原數組
arr[left + j] = helpArr[j];
}
}
最后,確定終止條件,一般是數組為空或者數組中只有一個元素,返回即可。
現在我們可以寫出整個歸並排序的代碼了,如下:
private static void mergeSort(int[] arr,int left,int right){
if(arr == null || right == left)//終止條件
return ;
int mid = left + (right - left) / 2;//確定分割的邊界
mergeSort(arr,left,mid);//對左半部分調用遞歸方法,使其有序
mergeSort(arr,mid + 1,right);//對右半部分調用遞歸方法,使其有序
merge(arr,left,mid,right);//合並左右兩部分,使整個數組有序
}
為了保證形式的統一,再對函數進行一下封裝,如下,這就是我們的歸並排序了。
/**
* 歸並排序算法
* @param arr
*/
public static void mergeSort(int[] arr){
mergeSort(arr,0,arr.length - 1);//調用寫好的遞歸版歸並排序方法
}
至此,我們便完成了歸並排序算法的代碼實現。
性能分析
在分析歸並排序算法性能之前,先介紹幾個基礎的概念。
時間復雜度:一個算法執行所消耗的時間;
空間復雜度:運行完一個算法所需的內存大小;
原地排序:在排序過程中不申請多余的存儲空間,只利用原來存儲待排數據的存儲空間進行比較和交換的數據排序。
非原地排序:需要利用額外的數組來輔助排序。
穩定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,則為穩定排序。
非穩定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,則為非穩定排序。
下面我們分析下歸並排序算法的性能。
首先是時間復雜度。歸並排序算法在排序時首先將問題進行分解,然后解決子問題,再合並,所以總時間=分解時間+解決子問題時間+合並時間。分解時間就是把一個數組分解為左右兩部分,時間為一常數,即O(1);解決子問題時間是兩個遞歸方法,把一個規模為n的問題分成兩個規模分別為n/2的子問題,時間為2T(n/2);合並時間復雜度為O(n)。所以總時間T(n)=2T(n/2)+O(n)。這個遞歸問題的時間復雜度可以用下面的公式來計算

這個公式可針對形如:T(n) = aT(n/b) + f(n)的遞歸方程進行時間復雜度求解。帶入可知,歸並排序的時間復雜度為O(nlogn)。此外在最壞、最佳、平均情況下歸並排序時間復雜度均為O(nlogn)。
空間復雜度分析:在排序過程中使用了一個與原數組等長的輔助數組,估空間復雜度為O(n)。
穩定性分析:由排序過程可以知道,歸並排序是一種穩定排序。
是否原地排序:排序過程中用到了輔助數組,所以是非原地排序。
本文源碼已同步至github,地址:https://github.com/zhanglianchao/AllForJava/tree/master/src/algorithm/sort
覺得文章有用的話,點贊+關注唄,好讓更多的人看到這篇文章,也激勵博主寫出更多的好文章。
更多關於算法、數據結構和計算機基礎知識的內容,歡迎掃碼關注我的原創公眾號「超悅編程」。

