一般在提到Merge Sort時,大家都很自然地想到Divide-and-Conqure, O(n lgn)的時間復雜度以及額外的O(n)空間。O(n)的extra space似乎成了Merge Sort最明顯的缺點,但實際上這一點是完全可以克服的,也就是說,我們完全可以實現O(n lgn) time 以及 O(1) space 的Merge Sort。對於這種不用額外空間(即常數大小的額外空間)的算法,有一個通用的名字叫做In-place Algorithms,因此我們稱該歸並算法為in-place merge sort,也就是原地歸並排序。
下面總結一下原地歸並排序特點:
空間:不需要輔助數組即可歸並,空間復雜度為O(1)
時間:時間復雜度為O(nlogn)
關鍵在於merge這個函數。兩段遞增的子數組arr[begin…mid-1]和arr[mid…end],i=begin,j=mid,k=end
i往后移動,找到第一個arr[i]>arr[j]的索引
j往后移動,再找第一個arr[j]>arr[i]的索引
然后我們將i到mid的部分和mid到j-1的部分對調,較小的部分就調到前面去了,然后從后面的部分與j到k的部分又是兩個遞增的子數組,繼續迭代即可。
a) 對調旋轉
旋轉又稱循環移動,假設有這樣一個序列:e0, e1, …, ei-1, ei, ei+1, …, en-1, en。現在我們需要把它向左循環移動i個位置變成:ei, ei+1, …, en-1, en, e0, e1, …, ei-1。為了盡可能的節約內存和保證較快的速度,我們可以在時間復雜度O(n),空間復雜度O(1)的情況下達到目的。一種解決方案如下:
把原始序列看成兩個子序列:e0, e1, …, ei-1和ei, ei+1, …, en-1, en
把這兩個子序列分別逆序得:ei-1, …, e1, e0和en, en-1, …, ei+1, ei
也就是得到了這樣一個序列:ei-1, …, e1, e0, en, en-1, …, ei+1, ei
再把上面的序列整體逆序得:ei, ei+1, …, en-1, en, e0, e1, …, ei-1
以上旋轉過程的時間復雜度為O(n/2) + O(n/2) + O(n) = O(2n) = O(n),逆序時僅需要一個元素的輔助空間,空間復雜度O(1)。
下面舉例說明一種原地歸並排序的思想。
在了解原地歸並的思想之前,先回憶一下一般的歸並算法,先是將有序子序列分別放入臨時數組,然后設置兩個指針依次從兩個子序列的開始尋找最小元素放入歸並數組中;那么原地歸並的思想亦是如此,就是歸並時要保證指針之前的數字始終是兩個子序列中最小的那些元素。文字敘述多了無用,見示例圖解,一看就明白。
假設我們現在有兩個有序子序列如圖a,進行原地合並的圖解示例如圖b開始
如圖b,首先第一個子序列的值與第二個子序列的第一個值20比較,如果序列一的值小於20,則指針i向后移,直到找到比20大的值,即指針i移動到30;經過b,我們知道指針i之前的值一定是兩個子序列中最小的塊。
如圖c,先用一個臨時指針記錄j的位置,然后用第二個子序列的值與序列一i所指的值30比較,如果序列二的值小於30,則j后移,直到找到比30大的值,即j移動到55的下標;
如圖d,經過圖c的過程,我們知道數組塊 [index, j) 中的值一定是全部都小於指針i所指的值30,即數組塊 [index, j) 中的值全部小於數組塊 [i, index) 中的值,為了滿足原地歸並的原則:始終保證指針i之前的元素為兩個序列中最小的那些元素,即i之前為已經歸並好的元素。我們交換這兩塊數組的內存塊,交換后i移動相應的步數,這個“步數”實際就是該步歸並好的數值個數,即數組塊[index, j)的個數。從而得到圖e如下:
重復上述的過程,如圖f,相當於圖b的過程,直到最后,這就是原地歸並的一種實現思想,具體代碼如下。
#include <iostream> #include <algorithm> using namespace std; //將長度為n的數組逆序 void reverse(int *A,int n) { int i=0; int j=n-1; while (i<j) { swap(A[i],A[j]); i++; j--; } } //將數組向左循環移位i個位置 void exchange(int *A,int n,int i) { reverse(A,i); reverse(A+i,n-i); reverse(A,n); } //數組兩個有序部分的歸並 void Merge(int *A,int begin,int mid,int end) { int i=begin; int j=mid; int k=end; while (i<j&&j<=k) { int step=0; while (i<j&&A[i]<=A[j]) i++; while (j<=k&&A[j]<A[i]) { j++; step++; } exchange(A+i,j-i,j-i-step); i=i+step; } } void MergeSort(int *A,int l,int r) { if(l<r) { int mid=(l+r)/2; MergeSort(A,l,mid); MergeSort(A,mid+1,r); Merge(A,l,mid+1,r); } } int main() { int arr[]={0,1,5,6,9,2,3,4,7,4,1,8}; int len=sizeof(arr)/sizeof(arr[0]); for (int i=0;i<len;i++) { cout<<arr[i]<<" "; } cout<<endl; MergeSort(arr,0,len-1); for (i=0;i<len;i++) { cout<<arr[i]<<" "; } cout<<endl; return 0; }
*******************************************************
package base; import java.util.Arrays; /** * Created by damon on 9/18/16. * 原地歸並排序 */ public class InPlaceMergeSort { //詳見: http://www.cnblogs.com/xiaorenwu702/p/5880841.html public static void main(String[] args){ int a[] = {49, 38, 65, 97, 76, 13, 27, 49, 78, 49, 34, 12, 64, 5, 4, 62, 99, 98, 54, 56, 17, 18, 23, 34, 15, 35, 25, 53, 51}; mergeSort(a); } private static void swip(int[] a, int p,int q){ int temp = a[p]; a[p]=a[q]; a[q]=temp; } private static void reverse(int[] a, int p,int q){ int i = p; int j = q; while(i<j){ swip(a,i,j); i++; j--; } } private static void exchange(int[] a, int p,int q,int r){ reverse(a,p,q); reverse(a,q+1,r); reverse(a,p,r); } public static void mergeSort(int[] a){ realMergeSort(a,0,a.length-1); System.out.println("mergeSort-->>" + Arrays.toString(a)); } private static void realMergeSort(int[] a,int p,int r){ if(p<r){ int q= (p+r)/2; realMergeSort(a,p,q); realMergeSort(a,q+1,r); merge(a,p,q,r); } } private static void merge(int[] a, int p,int q,int r){ int q1 = q+1; int step=0; while(p<=q&&q1<=r){ while(p<=q&&a[p]<=a[q1]){ p++; } while(q1<=r&&a[q1]<a[p]){ q1++; step++; } exchange(a,p,q,q1-1); p=p+step; } } }