一般在提到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; } } }