歸併排序
歸併排序(MERGE-SORT)是利用歸併的思想實現的排序方法,該算法採用經典的分治(divide-and-conquer)策略(分治法將問題分(divide)成一些小的問題然後遞歸求解,而治(conquer)的階段則將分的階段得到的各答案”修補”在一起,即分而治之)。
首先我們可以思考:如何將兩個有序的序列合併,並且合併後依然有序呢?
這個比較簡單,我們只需要用兩個指針,一開始都指向兩個序列的頭部,然後開始比較兩個指針所指數的大小,將小的那個放入一個新建的空間中,然後將它從相應的隊列中刪除,同時相應隊列的指針後移一位;如此往復這個操作,那麼兩個有序的序列就合併了。
public int[] merge(int arrA[], int arrB[]) {
// 創建合併後的數組
int[] result = new int[arrA.length + arrB.length];
int p1 = 0; // arrA的指針
int p2 = 0; // arrB的指針
int i = 0; // 當前要放入result的位置
// 比較兩個指針所指數的大小,將小的那個放入新的數組中,同時相應隊列的指針後移
while(p1 < arrA.length && p2 < arrB.length) {
result[i++] = arrA[p1] < arrB[p2] ? arrA[p1++] : arrB[p2++];
}
// 現在必有一個序列已經過完了,下面的循環只會進入一個
while(p1 < arrA.length) {
// 當前arrB走完了,將arrA中剩下的直接放入結果數組中
result[i++] = arrA[p1++];
}
while(p2 < arrB.length) {
// 當前arrA走完了,將arrB中剩下的直接放入結果數組中
result[i++] = arrB[p2++];
}
return result;
}
歸併排序就用到了這樣的思想:先把大的數組拆分成兩個序列,將這兩個序列分別進行排序,然後再合併起來。
可能有人會想,拆分成的兩個序列又怎麼排序呢?很簡單,我們繼續將它拆分,當其不能拆分了,就認爲這個不能拆分的個體已經排好序了,然後就可以進行合併了。
假設初始的數組是[5,6,4,7,3,2]
,我們可以先將其分成兩個小的部分進行排序,進而再合併起來。
1. 首先拆分成兩個部分:[5,6,4]
和[7,3,2]
,將這兩個部分分別排序後,再用外排的方式將兩者合併排序起來。但是這兩個部分怎麼分別排序呢?我們可以繼續拆分
2. 將[5,6,4]
和[7,3,2]
兩個部分再進行拆分,得到:[5,6]
,[4]
和[7,3]
,[2]
;現在又可以繼續將它們拆分
3. 繼續拆分得到{[5]
,[6]
和[4]
} {[7]
和[3]
,[2]
}; 現在已經無法繼續拆分了,就可以進行合併了
4. 第一次歸併後:[5,6]
和[4]
,[3,7]
和[2]
5. 第二次歸併後:[4,5,6]
和[2,3,7]
6. 第三次歸併後:[2,3,4,5,6,7]
算法實現
public class MergeSort {
public void mergeSort(int[] arr) {
if(arr == null || arr.length < 2) {
return ;
}
mergeSort(arr, 0, arr.length - 1);
}
/**
* 將數組在指定範圍內進行拆分合並
*/
public void mergeSort(int[] arr, int l, int r) {
// 已經無法再拆分了
if(l == r) {
return ;
}
// 從中間拆分
int mid = l + ((r-l) >> 1);
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
// 進行合併
merge(arr, l, mid, r);
}
/**
* 合併,l到m,m+1到r 兩段分別有序
*/
public void merge(int[] arr, int l, int m, int r) {
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = m + 1;
while (p1 <= m && p2 <= r) {
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= m) {
help[i++] = arr[p1++];
}
while (p2 <= r) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[l + i] = help[i];
}
}
}
時間複雜度
這裏到了遞歸的算法,每一個遞歸都可以轉換成非遞歸的方式。
遞歸分治法有一個Master公式,可以用來求解時間複雜度
簡單的提一下Master公式:
T(N) = a * T(N/b) + O(N^d), N表示樣本量,N/b表示子過程的樣本量,a表示子過程執行次數,O(N^d)表示除去子過程之外,剩下的時間複雜度
我們這裏是將數組從中間進行拆分,分成兩段分別排序,所以子過程樣本量是N/2;左右兩個子過程,所以執行兩次,a爲2;剩下了一個合併的排序,時間複雜度爲O(N)
所以最終的Master公式是:T(n)=2T(n/2)+O(N),
得到公式後,就可以去套答案了:
1. log(b,a) > d: 時間複雜度O(N^log(b,a))
2. log(b,a) = d: 時間複雜度O(N^d * logN)
3. log(b,a) < d: 時間複雜度O(N^d)
所以最終的時間複雜度是:O(N*logN)
空間複雜度
O(N)
穩定性
歸併排序是穩定的排序