2.2.11 改进。实现 2.2.2 节所述的对归并排序的三项改进:加快小数组的排序速度,检测数组是否已经有序以及通过在递归中交换参数来避免数组复制。
2.2.2.1 对小规模子数组使用插入排序
用不同的方法处理小规模问题能改进大多数递归算法的性能,因为递归会使小规模问题中方法的调用过于频繁,所以改进对它们的处理方法就能改进整个算法。对排序来说,我们已经知道插入排序(或者选择排序)非常简单,因此很可能在小数组上比归并排序更快。和之前一样,一幅可视轨迹图能够很好地说明归并排序的行为方式。图 2.2.4 中的可视轨迹图显示的是改良后的归并排序的所有操作。使用插入排序处理小规模的子数组(比如长度小于 15)一般可以将归并排序的运行时间缩短 10% ~ 15%(请见练习 2.2.23)。
2.2.2.2 测试数组是否已经有序
我们可以添加一个判断条件,如果 a[mid]
小于等于 a[mid+1]
,我们就认为数组已经是有序的并跳过 merge()
方法。这个改动不影响排序的递归调用,但是任意有序的子数组算法的运行时间就变为线性的了(请见练习 2.2.8)。
2.2.2.3 不将元素复制到辅助数组
我们可以节省将数组元素复制到用于归并的辅助数组所用的时间(但空间不行)。要做到这一点我们要调用两种排序方法,一种将数据从输入数组排序到辅助数组,一种将数据从辅助数组排序到输入数组。这种方法需要一些技巧,我们要在递归调用的每个层次交换输入数组和辅助数组的角色(请见练习 2.2.11)。
这里我们要重新强调第 1 章中提出的一个很容易遗忘的要点。在每一节中,我们会将书中的每个算法都看做某种应用的关键。但在整体上,我们希望学习的是为每种应用找到最合适的算法。我们并不是在推荐读者一定要实现所提到的这些改进方法,而是提醒大家不要对算法初始实现的性能盖棺定论。研究一个新问题时,最好的方法是先实现一个你能想到的最简单的程序,当它成为瓶颈的时候再继续改进它。实现那些只能把运行时间缩短某个常数因子的改进措施可能并不值得。你需要用实验来检验一项改进,正如本书中所有练习所演示的那样。
对于归并排序,刚才列出的三个建议都很容易实现且在应用归并排序时是十分有吸引力的——比如本章最后讨论的情况。
2.2.2.1
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) { // 将数组a[lo..hi]排序 if (hi <= lo) return; int mid = lo + (hi - lo) / 2; sort(a, aux, lo, mid); // 将左半边排序 sort(a, aux, mid + 1, hi); // 将右半边排序 if (hi - lo < 15) { // 判断长度,长度小于15的时候,使用插入排序 insertion(a, lo, hi); } else { merge(a, aux, lo, mid, hi); } } public static void insertion(Comparable[] a, int lo, int hi) { // 将a[]按升序排列 for (int i = lo; i <= hi; i++) { // 将 a[i] 插入到 a[i-1]、a[i-2]、a[i-3]...之中 for (int j = i; j > lo && less(a[j], a[j - 1]); j--) exch(a, j, j - 1); } }
2.2.2.2
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) { // 将数组a[lo..hi]排序 if (hi <= lo) return; int mid = lo + (hi - lo) / 2; sort(a, aux, lo, mid); // 将左半边排序 sort(a, aux, mid + 1, hi); // 将右半边排序if (!less(a[mid + 1], a[mid])) return; // 判断条件,如果a[mid]
小于等于a[mid+1] 跳过
if (hi - lo < 0) { insertion(a, lo, hi); } else { merge(a, aux, lo, mid, hi); // 归并结果(代码见“原地归并的抽象方法”) } }
2.2.2.3
在第三个优化的时候,遇到一个问题下面是,第三个单独优化
public static void sort(Comparable[] a) { Comparable[] aux = new Comparable[a.length]; // 一次性分配空间 for (int k = 0; k < a.length; k++) { aux[k] = a[k]; } sort(aux, a, 0, a.length - 1); } private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) { // 将数组a[lo..hi]排序 if (hi <= lo) return; int mid = lo + (hi - lo) / 2; sort(aux, a, lo, mid); // 将左半边排序 sort(aux, a, mid + 1, hi); // 将右半边排序 merge(a, aux, lo, mid, hi); // 归并结果(代码见“原地归并的抽象方法”) } public static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) { // 将a[lo..mid] 和 a[mid+1..hi] 归并 int i = lo, j = mid + 1; for (int k = lo; k <= hi; k++) { if (i > mid) aux[k] = a[j++]; else if (j > hi) aux[k] = a[i++]; else if (less(a[j], a[i])) aux[k] = a[j++]; else aux[k] = a[i++]; } // 归并回到a[lo..hi] }
a[mid]
小于等于
a[mid+1],没有调用 merge()
方法。但我们需要用merge()把结果合并会原数组。否则在最后merge()的时候,无法正常排序。
a[mid]
小于等于
a[mid+1],把aux和a给同步。
if (!less(a[mid + 1], a[mid])) { for (int k = lo; k <= hi; k++) { aux[k] = a[k]; } // 将a[lo..hi]复制到aux[lo..hi] return; }
但是a[mid]
小于等于 a[mid+1]会失去它的意义,在有序的情况下,会进行同步操作,运行时间就不会为线性的了。
如有错误欢迎指正!