注册一亩三分地论坛,查看更多干货!
您需要 登录 才可以下载或查看附件。没有帐号?注册账号
x
感谢之前的同学给我指出的错误, 我之前太自信以至于代码都是裸敲上去的,本着对各位负责的态度,以后我将会事先把程序编译验证通过。
注:若无特殊说明, 排序目的为递增。
坦白来说,我很讨厌这个算法,虽然很好实现, 很好理解, 虽然理论时间复杂度是严格的n times log(n) base 2(because T(n) = 2T(n/2) + n-1 ), 虽然相比于快速排序来说是有常数时间的优势。但是,希望各位读者明白一个事实,理论时间复杂度并不等于执行的复杂度, 尤其是当你意识到计算机里还有cache这个概念就会了然。 在nlogn这个级别的算法中 , 实际速度最快的就是quicksort, 笔者曾经做过大规模数据实验,当元素大于1000W的时候会有显著的差别,不考虑特殊的数据情况速度由快到慢依次是 quicksort, mergesort, heapsort。 最后一个是堆排序,笔者并没有写。 这里也征求下意见, 第七篇笔者打算在堆排序算法和线性排序算法中选一个写, 笔者是更加偏向后者, 因为笔者打算涉及到n^2, nlogn与n的所有排序算法。
言归正传,何为归并排序(Merge Sort)? 我们先引入一个问题: 两个有序好的数组, 如何合并成一个有序数组?
直接说算法步骤:
1: 比较两个数组的头元素, 将较小的元素(也就是当前两数组最小的元素)从原有数组删除,并排入新数组中;
2: 重复步骤1, 直到任一个数组元素个数为0。
3: 如果还有数组含有元素, 那么原封不动接在新数组的尾部。
最终我们得到了合并后的有序数组。
举例说明, 将两个有序数组a={1,5,7} b={0,4,6,8,9}合并成一个有序数组c, 每次比较a和b的头元素,较小(用红色字体标出)的元素插入c
step 1:
a={1,5,7}
b={4,6,8,9}
c={0}
step 2:
a={5,7}
b={4,6,8,9}
c={0,1}
step 3:
a={5,7}
b={6,8,9}
c={0,1,4}
step 4:
a={7}
b={6,8,9}
c={0,1,4,5}
step 5:
a={7}
b={8,9}
c={0,1,4,5,6}
step6
a={}
b={8,9}
c={0,1,4,5,6,7}
此时此刻, 数组a已经为空, b数组还剩2元素, 直接接在c后面即可, 得到
a={}
b={}
c={0,1,4,5,6,7,8,9}
完毕。
以上是归并算法的精髓思想之一,叫做归并(Merge),时间复杂度显然为线性。 有了这个背景知识, 我们发觉, 1:如果一个数组的左半部分和右半部分都有序, 那么只要通过Merge步骤, 就能在线性时间复杂度内实现全部有序; 2: 单独一个元素本身就是有序; 提示到这里, 读者已经恍然大悟了, 递归!分治! 归并! 又是一个divide and conquer思想的典型算法, 只不过归并算法中的conquer具体化变成了Merge而已。
于是我们的算法核心部分呼之欲出 private void Sort(int[] a, int left, int right) {
if (left < right) {
int mid = (left+right) / 2;
//递归sort左半部分
Sort(a, left, mid);
//递归sort右半部分
Sort(a, mid+1, right);
//合并成一个有序数列
Merge(a, left, mid, right);
}
}
期中left,mid, right是待排序数组的第一个, 中间和最后一个元素。 非常好理解, 那么Merge如何实现呢? 也非常简单, 用两个索引变量从头往后扫描待归并的两个数组即可。
归并代码如下:
private void Merge(int[] n, int start, int middle, int end) { int p1=start, p2=middle+1; int [] temp = new int[end - start +1]; int ptemp = 0; //比较和选择最小的元素插入新数组中。 直到任一数组为空 while (p1<=middle & p2<=end) { if (n[p1]<n[p2]) temp[ptemp++] = n[p1++]; else temp[ptemp++] = n[p2++]; } //若存在数组还有剩余元素, 原封不动接在新数组尾部即可 while (p1<=middle) temp[ptemp++] = n[p1++]; while (p2<=end) temp[ptemp++] = n[p2++]; //将临时数组temp的结果更新回原数组n [] for (int i = start; i<=end; i++) n = temp[i-start]; }
这是最常见也是最臃肿的代码, 其实也没什么可以简化的了。 相信各位读者也注意到了, Merge函数中每次都要开辟一个Temp数组,每次都要更新回原数组, 这使得运行时计算机要成几何倍数的多运算,这也是归并排序实战不如quicksort的瓶颈所在。在算法导论一书第二章描述了许多重要的算法包括heapsort, quicksort和linear sort, 但是同为nlgn的Mergesort并没有列入在内, 而是与insertion sort进行对比和引导式学习, 笔者私人推测这个现象也侧面体现了这个算法的效果。anyway, 笔者自己研究过实现方式的常数项的优化,但效果并不显著, 这里就不贴出来了, 主要还是以了解思想为主。
|