算法原理:
概述:
ACwing的y总对归并排序有一个口诀,我觉得很便于记忆和理解,叫作:
“双路归并,物归原主”
归并排序的核心步骤就在于“归并”,我们定义“归并”这个操作,指的就是把两个已经是有序的数组合并起来,并且保证他们合并起来以后仍然有序。
利用这个操作,只要我们能把待排序数组分成有序的两半,再利用上面定义的这个“归并”把它俩合起来,那这整个数组就有序了。
归并(Merge):
那么“归并”操作说是这样说,具体做起来如何实现呢?这样实现:
//假设待排序数组为R,
//我们现在要把R中[lower,mid]这段和[mid+1,upper]这段给“归并”起来
void Merge(int R[],int lower, int mid,int upper)
{
int* temp=new int[upper-lower+1];//我们需要借用的临时数组
int i=lower,j=mid+1,k=0; //i指针用于在[lower,mid]这个区间内索引,j用于在[mid+1,upper]内
//k用于往temp里放数据时索引下标位置
while(i<=mid&&j<=upper)
{
//同时扫描要合并的两段,把小的元素优先放前面
if(R[i]<R[j]) temp[k++]=R[i++];
else temp[k++]=R[j++];
}
//有可能一段已经放完了,另一段有剩余元素,
while(i<=mid) temp[k++]=R[i++];
while(j<=mid) temp[k++]=R[j++];
//至此,我们已经完成了“双路归并”,成功把本来已有序的[lower,mid]和[mid+1,upper]这两段给合并起来,
//并且合并之后仍是有序的,这个合并出来的新数组就存放在temp里
//现在,只需“物归原主”,即把temp找原样搬回R中
for(int t=0,i=lower;i<=upper;i++,t++)
{
R[i]=temp[t];
}
//“双路归并,物归原主”结束
delete* temp;
}
如果看完代码觉得难以理解“归并”究竟是怎样操作的话,我们可以看一个直观的例子(与上面的代码对比结合着看哦):
我们现在有两个有序的区间,分别为R[low——mid]和R[mid+1——high]。一开始,初始化i=lower,j=mid+1,k=0,X就是temp临时数组,
比较R[i]和R[j],谁小谁就放到下面的临时数组中,显然R[i]小,无论是R[i]还是R[j],只要放下去了,指针自动后移指向下一个元素,也就是如果R[i]被放下去,i++;如果R[j]被放下去,那么j++。
一直重复这个过程,直到R[low——mid]和R[mid+1——high]这两个区间有一个被遍历完了。
显然可见,这样的操作,每次往X中放元素,放的都是“两个区间中还剩下的元素中的最小的那个 ”,这样就保证了X是有序的。
现在,右边那个区间被遍历完了,最后一个放入X的元素是39,依据这种规则,那么左边那个区间中剩下的元素,肯定都大于39,那么就挨个放下来(当然如果是左边区间先完了右边区间有剩,那就右边挨个放下来),对应的正是代码中这两行:
至此,正如上面代码中注释所说,“双路归并”已经完成了,我们已经实现了把“两个有序的区间合并起来,且仍保持有序”,只不过结果现在存放在临时数组X里,而我们要排序的是目标数组R,所以接下来自然应该“物归原主”,将X复制回R,然后删除临时创建的临时数组就可以了。
“归并”操作就完成了,那么接上面所说,只要我们能把待排序数组分成有序的两半,再利用上面定义的这个“归并”把它俩合起来,那这整个数组就有序了。
问题是如何保证左右两半有序?当然需要先把两半各自再分成有序的两半,再归并。那如何保证半个数组的两半是有序的,这样不久无穷尽的分下去了么?什么时候能合并呢?没错,这就构成了自顶向下的递归,而递归问题的出口,我们知道,往往就在递归的最底层。
答案当然是,当分成的子数组只有一个元素时,那么这个子数组是不是有序的?当然是。
我给你一个数组a,只有一个元素1,那么a是一个有序数组,
再给你一个数组b,只有一个元素2,那么b是一个有序数组,
现在让你,“将两有序数组a,b合并,并保证合并后仍是有序数组”,能不能做到?当然能,比较1和2谁大谁小,1小放前面,2大放后面,合成了一个新的数组c ,
int c={1,2};
我们这不就把两个长度为1的有序数组,合并成一个长度为2的有序数组了?那么在此基础上,递归回溯,我们是不是能把两个长度为2的有序数组,合并成一个长度为4的有序数组?能不能把两个长度为4的有序数组,合并成一个长度为8的有序数组?
这样一直向上回溯,像我们刚才说的,我们最终能不能把整个待排序数组,变成有序的呢?答案是,当然能。
实现:
递归(自顶向下):
void MergeSort(int R[], int m, int n)
{
if (m < n)
{
int k = (m + n) / 2;
MergeSort(R, m, k);
MergeSort(R, k + 1, n);
Merge(R, m, k, n);
}
}
用一张图来直观理解:
递归不断向下深入的过程,就是不断折半、不断缩短区间的过程,直至区间长度为1以后开始向上回溯,回溯的过程中逐步由长度为1的区间归并为长度为2,由长度为2归并为长度为4,这样逐步将整个长度为n的我们待排序的区间给归并排序了。
非递归(自底向上):
void Mpass(int R[],int n,int L){
//合并相邻的两个长度为L的子数组
for(int i=1; i+2*L-1≤n; i+=2*L)
Merge(R, i, i+L–1, i+2*L–1);
//处理余留的长度小于2*L的子数组
if(i+L–1 < n)
Merge(R, i, i+L–1, n); //L<剩余部分长度<2L
}
void MergeSort(int R[], int n){
for(int L=1; L<n; L*=2)
Mpass (R, n, L);
}
Mpass每执行一次,是把待排序区间R划分成一个个长度为L的子区间,并将两两相邻的子区间归并,那么子区间的长度,当然应该从1开始取。MergeSort中L从1到n的循环,恰恰对应了刚才递归实现的自底向上的回溯。
时间复杂度分析:
显而易见,每次Merge,要遍历被Merge的两个子区间,所以复杂度O(n),而一共要进行几次Merge?自然取决于这个高度k,也即:
所以最好、最坏、平均情况下,时间复杂度均为O(nlogn)
稳定性分析:
归并排序是稳定的,(两个值相等的元素,排序后相对次序保持不变),但我太菜无法给出证明,所以就放一个例子吧: