归并排序原理介绍
归并排序利用“分治”的思想,将一个长度为n的待排数组不断的二分为原来的一半;经过logn次之后,每段就将只剩下1个元素,这样的元素段一共有n段,至此,“归“的部分已经完成。(请结合下图理解)这部分的时间复杂度为logn。
剩下的就是”并“的部分,每次”并“操作都对相邻两段内的元素进行排序,完成排序之后合并为一段,总段数变为一半,经过logn次合并排序之后,整个数组变为有序的一整段,排序完毕。这部分的时间复杂度为n*logn。由于合并过程中的排序需要先把原数组中的元素复制到临时数组,待排好序之后再返回至原数组,所以需要开辟跟原数组大小一样的空间,因此空间复杂度为O(n)。
具体的原理图如下:
归并排序代码
递归完成”归“部分函数:
//原版
void merge_sort(int p[], int l, int r)
{
int temp;
int mid = (l + r) / 2;
if (r<=l)
return;
merge_sort(p, l, mid);
merge_sort(p, mid + 1, r);
merge(p, l, r);
}
//优化版
void merge_sort(int p[], int l, int r)
{
int temp;
int mid = (l + r) / 2;
if (r - l <= 1)
{
if (p[r] < p[l])
{
temp = p[r];
p[r] = p[l];
p[l] = temp;
}
return;
}
merge_sort(p, l, mid);
merge_sort(p, mid + 1, r);
merge(p, l, r);
优化版本的代码主要是针对"归"函数的分隔次数做出了修改:
归并函数原版中merge_sort( )是进行logn次切割,到最后剩下n段,每段含有一个元素;
归并函数优化版中merge_sort( )是进行(logn)-1次切割,到最后剩下n/2段,每段含有两个或者一个元素;
这样做的好处是分别减少n次、n/2次对merge_sort( )、merge( )的递归调用。我们可以很容易看到,当将n/2段分隔为n段的时候,需要调用merge_sort() n次,但其实际上这n次中的每一次调用都只是执行了内部的if(r<=l) return;然而函数调用的入栈和出栈开销十分大;同理,调用n/2次merge( )函数完成n个无序段到n/2个无序段的合并也是开销巨大的。所以将上述两个操作纳入merge_sort( )中,避免低效的函数调用,可以大大减少代码执行时间。
原版递归调用merge_sort( )函数完成的工作:
优化版递归调用merge_sort( )函数完成的工作:
”并“部分函数:
void merge(int p[], int l, int r)
{
int* tmp=new int[r-l+1];
int mid = (r-l) / 2;
int i = 0;
int j = (r - l) / 2+1;
for (int k = 0; k < r - l + 1; k++)
tmp[k] = p[l + k];
int k = 0;
while(i<=mid||j<=r-l)
{
if (i > mid)
p[k + l] = tmp[j++];
else if (j > r-l)
p[k + l] = tmp[i++];
else if (tmp[i] < tmp[j])
p[k + l] = tmp[i++];
else
p[k + l] = tmp[j++];
k++;
}
delete[] tmp;
}
因此,优化版的归并排序原理图为:
介绍到这里,归并排序是不是说完了呢?其实,还有一个有趣的事情是,虽然我们看到原理图里面的归并过程十分清晰,但如果要根据原理图来构思递归代码还是很吃力的。这是为什么?答案就在于,归并排序执行过程与原理图所展现的过程在顺序上有差别。下面,我将通过图解的方式展现merge_sort( )函数的实际执行过程,也方便大家了解整个递归过程(优化版):
蓝色的序号0-9分别表示这九个函数的执行顺序,圆圈序号1-7则表示数组A的排序过程