归并排序是一种递归的排序方法,它把两个有序的数组归并成一个更大的有序数组。要将一个数组排序,可以先将它分成两半分别排序,然后再将结果归并起来。归并排序的复杂度是O(NlogN),不过需要额外的N空间。
归并排序有两种方法,一种是自顶向下的方法,一种是自底向上的方法。先来看一下自顶向下的方法:
public class Merge {
public static void merge(Comparable[] a, int low, int mid, int high, Comparable[] aux) {
//把a[low..mid]和a[mid+1..high]归并
if (a[mid].compareTo(a[mid+1]) < 0) return;
int i = low, j = mid + 1;
for (int k = low; k <= high; k++) {
aux[k] = a[k];
}
for (int k = low; k <= high; k++) {
//如果左边元素已经用尽
if (i > mid) a[k] = aux[j++];
//如果右边数组已经用尽
else if (j > high) a[k] = aux[i++];
//如果左边当前元素小于右边当前元素
else if (aux[i].compareTo(aux[j]) < 0) a[k] = aux[i++];
else a[k] = aux[j++];
}
}
//自顶向下的归并排序
public static void sortUB(Comparable[] a) {
Comparable[] aux = new Comparable[a.length];
sortUB(a, 0, a.length-1, aux);
}
private static void sortUB(Comparable[] a, int low, int high, Comparable[] aux) {
//将数组a[low..high]排序
if(high <= low) return;
int mid = (low + high) / 2;
sortUB(a, low, mid, aux);
sortUB(a, mid+1, high, aux);
merge(a, low, mid, high, aux);
}
}
对于辅助数组aux
,最好作为参数传递到实际进行排序的sortUB
函数中,如果在递归的时候每次都创建一个辅助数组的话,会导致大的时间开销。
对于上面的程序,有几点可以进行优化的地方,首先,对于小的数组可以进行插入排序或者选择排序,减少递归调用的消耗。还可以按照降序把数组a
的后半部分复制到辅助数组aux
,然后将其归并回数组a
中,这样可以去掉内循环中检测某半边是否用尽的代码,代码如下所示:
public static void mergeReverse(Comparable[] a, int low, int mid, int high, Comparable[] aux) {
if (a[mid].compareTo(a[mid+1]) < 0) return;
//辅助数组的前半部分为a[]的正序
for (int k = low; k <= mid; k++) {
aux[k] = a[k];
}
//辅助数组的后半部分为a[]的逆序
for (int k = mid+1; k <= high; k++) {
aux[k] = a[high - k + mid + 1];
}
//从辅助数组的两端向中间逼近,由于aux前半部是正序,后半部分是逆序,所以无论i和j哪个过了中间界之后都会停下来
// 这样做避免了内循环中检测某半边是否用尽的代码
int i = low, j = high;
for (int k = low; k <= high; k++) {
if (aux[i].compareTo(aux[j]) < 0) a[k] = aux[i++];
else a[k] = aux[j--];
}
}
还有一种优化方法是去掉复制数组用的时间,这可以通过在递归的每个层次中交替使用原始数组和辅助数组来实现,不过先来看一下自底向上的排序方法。自底向上的方法是先归并小的数组,然后再继续归并得到的子数组,直至整个数组都被归并到一起。
//自底向上的归并排序
public static void sortBU(Comparable[] a) {
Comparable[] aux = new Comparable[a.length];
int n = a.length;
for (int size = 1; size < n; size*=2) { //子数组的大小
// 对每个子数组进行归并
for (int i = 0; i < n - size ; i+=2*size) {
merge(a, i, i+size-1, Math.min(i+2*size-1, n-1), aux);
}
}
}
在上面代码的基础上消除递归中数组复制的时间开销:
package chapter2;
/**
* Created by jia on 17-5-15.
*/
public class MergePlus {
//自底向上的归并排序
public static void sortBU(Comparable[] a) {
Comparable[] aux = new Comparable[a.length];
int n = a.length;
boolean swap = false; //一个记录当前使用的是哪个数组(a还是aux)的标志
for (int size = 1; size < n; size*=2) { //子数组的大小
// 对每个子数组进行归并
for (int i = 0; i <= n - size ; i+=2*size) {
//mid参数不能由(low+high)/2得出,而必须是low+size-1,因为归并排序要求左右两边的子数组都是有序的
merge(a, i, i+size-1, Math.min(i+2*size-1, n-1), aux, swap);
}
swap = !swap; //每层循环之后将swap取反
}
// if (!swap) ArrayPrint.print(a);
// else ArrayPrint.print(aux);
if (swap) a = aux;
// ArrayPrint.print(a);
// ArrayPrint.print(aux);
}
private static void merge(Comparable[] a, int low, int mid, int high, Comparable[] aux, boolean swap) {
if (!swap) merge(a, low, mid, high, aux);
else merge(aux, low, mid, high, a);
}
private static void merge(Comparable[] a, int low, int mid, int high, Comparable[] aux) {
if (low == high) {
aux[low] = a[low];
return;
}
int i = low, j = mid + 1;
for (int k = low; k <= high; k++) {
if (i > mid) aux[k] = a[j++];
else if (j > high) aux[k] = a[i++];
else if (a[i].compareTo(a[j]) < 0) aux[k] = a[i++];
else aux[k] = a[j++];
}
}
public static void main(String[] args) {
String[] a = "10423".split("");
sortBU(a);
}
}
进行性能测试,随机生成100个大小为10000的数组,用插入、自顶向下的归并、优化后的自底向上的归并和未优化的自底向上的归并,结果如下,1.3737515908E10 2.49410522E8 1.95227954E8 3.03551073E8
,单位为纳秒。由以上结果可以看出,去除了数组复制开销的自底向上的归并排序算法的性能确实有较大的提升。