要将一个数组排序,可以先将它分成两半分别排序,然后将结果归并起来。这就是归并排序。归并排序的主要优势是它能保证将任意长度为n的数组排序所需时间与nlogn成正比,它的主要缺点则是它所需的额外空间与n成正比。
4.1 自顶而下的归并
public static void merge(Comparable[] a, int lo, int mid, int hi) {
int i = lo, j = mid + 1;
for (int k = lo; k < hi; k++) {
aux[k] = a[k];
}
for (int k = lo; k <= hi; k++) {
if (i > mid) {
a[k] = aux[j++];
} else if (j > mid) {
a[k] = aux[i++];
} else if (aux[j] < aux[i]) {
a[k] = aux[j++];
} else {
a[k] = aux[i++];
}
}
}
该方法先将所有元素复制到aux[]中,然后再归并到a[]中。方法在归并时用到4个条件判断:左半边用尽取右半边的元素,右半边用尽取左半边元素,右半边当前元素小于左半边当前元素取右半边元素,右半边当前元素大于左半边当前元素去左半边元素。
public class MergeSort {
private static Comparable[] aux;
public static void sort(Comparable[] a) {
aux = new Comparable[a.length];
sort(a, 0, a.length - 1);
}
public static void sort(Comparable[] a, int lo, int hi) {
if (hi < lo) {
return;
}
int mid = lo + (hi - lo) / 2;
sort(a, lo, mid);
sort(a, mid + 1, hi);
merge(a, lo, mid, hi);
}
}
对于长度为n的任意数组,自顶而下的归并排序需要1/2nlogn至nlogn比较。
对于长度为n的任意数组,自顶而下的归并排序最多需要访问数组6nlogn次。
4.2 自底而上的归并排序
实现归并排序的另一种方法是先归并那些微型数组,然后再成对归并得到的子数组,直到将整个数组归并到一起。这种实现方法比标准递归方法所需要的代码量更少。
public class MergeSort {
private static Comparable[] aux;
public static void sort(Comparable[] a) {
int n = a.length;
aux = new Comparable[n];
for (int sz = 1; sz < n ; sz = sz + sz ) {
for (int lo = 0; lo < n - sz ; lo += sz + sz ) {
merge(a, lo, lo + sz - 1; Math.min(lo + sz + sz - 1, n - 1))
}
}
}
}
自底而上的归并排序会多次遍历整个数组,根据子数组大小进行两两归并。子数组大小的sz初始值为1,每次加倍。最后一个子数组的大小只有在数组大小是sz偶数倍的时候才会等于sz,否则它会比sz小。
对于长度为n的任意数组,自底而上的归并排序需要1/2nlogn至nlogn次比较,最多访问数组6nlogn次。
当数组长度为2的幂时,自顶而下和自底而上的归并排序所用的比较次数和数组访问次数正好相同,只是顺序不同。其他时候,两种方法的比较和数组访问的次序会有所不同。
自底而上的归并排序比较适合用链表组织的数据。这种方法只需要重新组织链表链接就能将链表原地排序(不需要创建任何新的链表节点)。
4.4 复杂度
没有任何基于比较的算法能够保证使用少于logn!~nlogn次比较将长度为n的数组排序。
归并排序是一种渐进最优的基于比较排序的算法。
然而,归并排序也有很多局限性。例如:归并排序的空间复杂度不是最优的;在实践中不一定会遇到最坏情况;除了比较算法的其他操作(如访问数组)也可能很重要;不进行比较也能将某些数据排序。