- 時間復雜度:O(nlogn)
- 空間復雜度:O(N),歸並排序需要一個與原數組相同長度的數組做輔助來排序
- 穩定性:歸並排序是穩定的排序算法,
temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
這行代碼可以保證當左右兩部分的值相等的時候,先復制左邊的值,這樣可以保證值相等的時候兩個元素的相對位置不變。
歸並排序是建立在歸並操作上的一種有效的排序算法。該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合並,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合並成一個有序表,稱為2-路歸並。把長度為n的輸入序列分成兩個長度為n/2的子序列;對這兩個子序列分別采用歸並排序;將兩個排序好的子序列合並成一個最終的排序序列。
從下圖可以看出,每次合並操作的平均時間復雜度為O(n),而完全二叉樹的深度為|log2n|。總的平均時間復雜度為O(nlogn)。而且,歸並排序的最好,最壞,平均時間復雜度均為O(nlogn)。
圖示過程
(1) 歸並排序的流程
(2) 合並兩個有序數組的流程

歸並排序有兩種實現方式: 基於遞歸的歸並排序和基於循環的歸並排序。(也叫自頂向下的歸並排序和自底向上的歸並排序)
這兩種歸並算法雖然實現方式不同,但還是有共同之處的:
1. 無論是基於遞歸還是循環的歸並排序, 它們調用的核心方法都是相同的:完成一趟合並的算法,即兩個已經有序的數組序列合並成一個更大的有序數組序列 (前提是兩個原序列都是有序的!)
2. 從排序軌跡上看,合並序列的長度都是從小(一個元素)到大(整個數組)增長的
C++
一、基於遞歸的歸並排序(自頂向下的歸並排序)
void MergeSort (int arr [], int low,int high) { if(low>=high) { return; } // 終止遞歸的條件,子序列長度為1 int mid = low + (high - low)/2; // 取得序列中間的元素 MergeSort(arr,low,mid); // 對左半邊遞歸 MergeSort(arr,mid+1,high); // 對右半邊遞歸 merge(arr,low,mid,high); // 合並 } void merge(int arr[],int low,int mid,int high){ //low為第1有序區的第1個元素,i指向第1個元素, mid為第1有序區的最后1個元素 int i=low, j=mid+1, k=0; //mid+1為第2有序區第1個元素,j指向第1個元素 int *temp=new int[high-low+1]; //temp數組暫存合並的有序序列 while(i<=mid&&j<=high){ if(arr[i]<=arr[j]) //較小的先存入temp中 temp[k++]=arr[i++]; else temp[k++]=arr[j++]; } while(i<=mid)//若比較完之后,第一個有序區仍有剩余,則直接復制到t數組中 temp[k++]=arr[i++]; while(j<=high)//同上 temp[k++]=arr[j++]; for(i=low,k=0;i<=high;i++,k++)//將排好序的存回arr中low到high這區間 arr[i]=temp[k]; delete []temp;//釋放內存,由於指向的是數組,必須用delete [] }
基於遞歸歸並排序的優化方法
優化一:對小規模子數組使用插入排序
用不同的方法處理小規模問題能改進大多數遞歸算法的性能,因為遞歸會使小規模問題中方法調用太過頻繁,所以改進對它們的處理方法就能改進整個算法。因為插入排序非常簡單, 因此一般來說在小數組上比歸並排序更快。 這種優化能使歸並排序的運行時間縮短10%到15%。
怎么切換呢?只要把作為停止遞歸條件的
if(low>=high) { return; }
改成
if(high - low <= 10) { // 數組長度小於10的時候
InsertSort(int arr[], int low,int high) // 切換到插入排序
return;
}
就可以了,這樣的話,這條語句就具有了兩個功能:
1. 在適當時候終止遞歸
2. 當數組長度小於M的時候(high-low <= M), 不進行歸並排序,而進行插排
優化二: 測試待排序序列中左右半邊是否已有序
通過測試待排序序列中左右半邊是否已經有序, 在有序的情況下避免合並方法的調用。
因為a[low...mid]和a[mid...high]本來就是有序的,存在a[low]<a[low+1]...<a[mid]和a[mid+1]<a[mid+2]...< a[high]這兩種關系, 如果判斷出a[mid]<=a[mid+1]的話,我們就認為數組已經是有序的並跳過merge() 方法。
void sort (int a[], int low,int high) {
if(low>=high) {
return;
} // 終止遞歸的條件
int mid = low + (high - low)/2; // 取得序列中間的元素
sort(a,low,mid); // 對左半邊遞歸
sort(a,mid+1,high); // 對右半邊遞歸
if(a[mid]<=a[mid+1]) return; // 避免不必要的歸並
merge(a,low,mid,high); // 單趟合並
}
優化三:去除原數組序列到輔助數組的拷貝
在上面介紹的基於遞歸的歸並排序的代碼中, 我們在每次調用merge方法時候,我們都把arr對應的序列拷貝到輔助數組temp中去。
在遞歸調用的每個層次交換輸入數組和輸出數組的角色,從而不斷地把輸入數組排序到輔助數組,再將數據從輔助數組排序到輸入數組,節省數組復制的時間。
注意, 外部的sort方法和內部sort方法接收的a和aux參數剛好是相反的
在這里我們做了兩個操作:
- 在排序前拷貝一個和原數組元素完全一樣的輔助數組(不再是創建一個空數組了!)
- 在遞歸調用的每個層次交換輸入數組和輸出數組的角色
因為外部sort和merge的參數順序是相同的,所以,無論遞歸過程中輔助數組和原數組的角色如何替換,對最后一次調用的merge而言(將整個數組左右半邊合為有序的操作), 最終被排為有序的都是原數組,而不是輔助數組!
void MergeSortCore(int* data, int* copy, int first, int last); void MergeSort(int* data, int length) { if(data == NULL || length <= 0) return; int* copy = new int[length]; for(int i = 0; i < length; i++) copy[i] = data[i]; MergeSortCore(data, copy, 0, length - 1); for(int i = 0; i < length; i++) data[i] = copy[i]; delete[] copy; return; } void MergeSortCore(int* data, int* copy, int first, int last) { if(first == last) copy[first] = data[first]; else { int mid = first + (last - first)/2; MergeSortCore(copy, data, first, mid); MergeSortCore(copy, data, mid + 1, last); //merge int i = first, j = mid + 1; int k = first; while(i <= mid && j <= last) { if(data[i] <= data[j]) copy[k++] = data[i++]; else copy[k++] = data[j++]; } while(i <= mid) copy[k++] = data[i++]; while(j <= last) copy[k++] = data[j++]; } }
注意:優化結果雖然差不多,但是當其數組接近有序的時候,速度有了可觀的提升。
二、基於循環的歸並排序(自底向上)
void sort(int a []){ int N = a.size(); for (int size = 1; size < N; size *= 2){ // low+size=mid+1,為第二個分區第一個元素,它 < N,確保最后一次合並有2個區間 for(int low = 0; low + size < N;low += 2 * size) { mid = low + size - 1; high = low + 2 * size - 1; if(high > N-1) high = N - 1; merge(a,low, mid, high); } } }