归并排序
要将一个数组排序,可以先(递归的)将它分成层两半分别排序,然后将结果归并起来。
它能保证任意长度为N的数组排序所需时间和NlogN成正比;它的缺点则是它所需的额外空间和N成正比。
原地归并的抽象方法
实现归并的一种直截了当的办法是将两个不同的有序数组归并到第三个数组中,两个数组中的元素应该都实现了Comparable借口。实现的方法很简单,创建一个适当大小的数组然后将两个输入数组的元素从一个个从小到大放入这个数组中。
但是由于递归的原因,这种方法效率不高。我们更希望有一种能够在原地归并的方法,这样就可以先将前半部分排序,再将后半部分排序,然后在数组中移动元素而不需要额外的空间(即不需要第三个数组了)。
示意图:
自顶向下的归并排序
也就是上面那张图描述的。代码:
public class Merge {
// This class should not be instantiated.
private Merge() { }
// stably merge a[lo .. mid] with a[mid+1 ..hi] using aux[lo .. hi]
private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
// precondition: a[lo .. mid] and a[mid+1 .. hi] are sorted subarrays
assert isSorted(a, lo, mid);
assert isSorted(a, mid+1, hi);
// copy to aux[]
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
// merge back to a[]
int i = lo, j = mid+1;
for (int k = lo; k <= hi; k++) {
if (i > mid) a[k] = aux[j++];
else if (j > hi) a[k] = aux[i++];
else if (less(aux[j], aux[i])) a[k] = aux[j++];
else a[k] = aux[i++];
}
// postcondition: a[lo .. hi] is sorted
assert isSorted(a, lo, hi);
}
private static void merge(Comparable[] a, int[] index, int[] aux, int lo, int mid, int hi) {
// copy to aux[]
for (int k = lo; k <= hi; k++) {
aux[k] = index[k];
}
// merge back to a[]
int i = lo, j = mid+1;
for (int k = lo; k <= hi; k++) {
if (i > mid) index[k] = aux[j++];
else if (j > hi) index[k] = aux[i++];
else if (less(a[aux[j]], a[aux[i]])) index[k] = aux[j++];
else index[k] = aux[i++];
}
}
// mergesort a[lo..hi] using auxiliary array aux[lo..hi]
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) {
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(a, aux, lo, mid);
sort(a, aux, mid + 1, hi);
merge(a, aux, lo, mid, hi);
}
public static void sort(Comparable[] a) {
Comparable[] aux = new Comparable[a.length];
sort(a, aux, 0, a.length-1);
assert isSorted(a);
}
// is v < w ?
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
// exchange a[i] and a[j]
private static void exch(Object[] a, int i, int j) {
Object swap = a[i];
a[i] = a[j];
a[j] = swap;
}
private static boolean isSorted(Comparable[] a) {
return isSorted(a, 0, a.length - 1);
}
private static boolean isSorted(Comparable[] a, int lo, int hi) {
for (int i = lo + 1; i <= hi; i++)
if (less(a[i], a[i-1])) return false;
return true;
}
public static int[] indexSort(Comparable[] a) {
int N = a.length;
int[] index = new int[N];
for (int i = 0; i < N; i++)
index[i] = i;
int[] aux = new int[N];
sort(a, index, aux, 0, N-1);
return index;
}
// mergesort a[lo..hi] using auxiliary array aux[lo..hi]
private static void sort(Comparable[] a, int[] index, int[] aux, int lo, int hi) {
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(a, index, aux, lo, mid);
sort(a, index, aux, mid + 1, hi);
merge(a, index, aux, lo, mid, hi);
}
// print array to standard output
private static void show(Comparable[] a) {
for (int i = 0; i < a.length; i++) {
StdOut.println(a[i]);
}
}
public static void main(String[] args) {
String[] a = StdIn.readAllStrings();
Merge.sort(a);
show(a);
}
}
merge的精髓(也就是排序):左半边用尽,则取右半边元素;右半边用尽,则取左半边元素;右半边的当前元素小于左半边的当前元素,则取右半边元素;右半边的当前元素大于左半边的当前元素,则取左半边的元素。实际上大部分发生的都是后面两句话,前面两句只是特殊情况而已。
改进:
对小规模子数组采用插入排序:
因为递归会使小规模问题中方法的调用过于频繁,所以改进对它们的处理方法就能改进整个算法。使用插入排序处理小规模的子数组,一般可以将归并排序的运行时间虽短10%~15%。无代码
测试数组是否已经有序:可以添加一个判断条件,如果a[mid]小于a[mid+1],我们就任务数组已经是有序的并跳过merge方法(指的是两个sort后面的merge)。这个改动不影响排序的递归调用,但是任意有序的子数组算法的运行时间就变成线性的了。
不将元素复制到辅助数组:我们可以节省将数组复制到用于归并的辅助数组所用的时间。要做到这一点我们要调用两种排序方法,一种将数据从输入数组排序到辅助数组,一种将数据从辅助数组排序到输入数组,这种方法需要一些技巧,我们要在递归调用的每个层次交换输入数组和输出数组的角色。无代码
自底向上的归并排序
实现归并排序的另一种方法是先归并那些微型数组,然后再成对归并得到的数组,如此这般,直到我们将这个数组归并在一起
就是上面的算法倒过来
public class MergeBU {
// This class should not be instantiated.
private MergeBU() { }
// stably merge a[lo..mid] with a[mid+1..hi] using aux[lo..hi]
private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
// copy to aux[]
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
// merge back to a[]
int i = lo, j = mid+1;
for (int k = lo; k <= hi; k++) {
if (i > mid) a[k] = aux[j++]; // this copying is unneccessary
else if (j > hi) a[k] = aux[i++];
else if (less(aux[j], aux[i])) a[k] = aux[j++];
else a[k] = aux[i++];
}
}
public static void sort(Comparable[] a) {
int N = a.length;
Comparable[] aux = new Comparable[N];
for (int n = 1; n < N; n = n+n) {
for (int i = 0; i < N-n; i += n+n) {
int lo = i;
int m = i+n-1;
int hi = Math.min(i+n+n-1, N-1);
merge(a, aux, lo, m, hi);
}
}
assert isSorted(a);
}
// is v < w ?
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
// exchange a[i] and a[j]
private static void exch(Object[] a, int i, int j) {
Object swap = a[i];
a[i] = a[j];
a[j] = swap;
}
private static boolean isSorted(Comparable[] a) {
for (int i = 1; i < a.length; i++)
if (less(a[i], a[i-1])) return false;
return true;
}
// print array to standard output
private static void show(Comparable[] a) {
for (int i = 0; i < a.length; i++) {
StdOut.println(a[i]);
}
}
public static void main(String[] args) {
String[] a = StdIn.readAllStrings();
MergeBU.sort(a);
show(a);
}
}
注意:当数组长度为2的幂时,自顶向下和自底向上的归并排序所用的次数和数组访问的次数正好相同,只是顺序不同。其他时候,两种方法的比较和数组的访问次数会有所不同
排序算法的复杂度
归并排序的局限性:
归并排序的空间复杂度不是最优的
在实践中不一定会遇到最坏情况
除了比较,算法的其他操作(例如访问数组)也可能很重要;
不进行比较也能将某些数据排序