算法(第4版)-2.2 归并排序
归并:将两个有序的数组归并成一个更大的有序数组。
归并算法:先(递归地)将它分为两半分别排序,然后将结果归并起来。
· 优点:保证将任意长度为N的数组排序所需时间和NlogN成正比;
· 缺点:所需的额外空间和N成正比。
2.2.1 原地归并的抽象方法
public static void merge(Comparable[] a, 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++) { // 将a[lo..hi]复制到aux[lo..hi] aux[k] = a[k]; } 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++]; // 右 > 左 } }
2.2.2 自顶向下的归并排序
public class Merge { private static Comparable[] aux; // 归并所需的辅助数组 public static void sort(Comparable[] a) { aux = new Comparable[a.length]; // 一次性分配空间 sort(a, 0, a.length - 1); } private static void sort(Comparable[] a, int lo, int hi) { // 将数组a[lo..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); // 归并结果 } public static void merge(Comparable[] a, 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++) { // 将a[lo..hi]复制到aux[lo..hi] aux[k] = a[k]; } 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++]; // 右 > 左 } } private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } private static void exch(Comparable[] a, int i, int j) { Comparable t = a[i]; a[i] = a[j]; a[j] = t; } private static void show(Comparable[] a) { // 在单行中打印数组 for (int i = 0; i < a.length; i++) StdOut.print(a[i] + " "); StdOut.println(); } public static boolean isSorted(Comparable[] a) { // 测试数组元素是否有序 for (int i = 1; i < a.length; i++) if (less(a[i], a[i - 1])) return false; return true; } public static void main(String[] args) { // 从标准输入读取字符串,将它们排序并输出 String[] a = In.readStrings(); sort(a); assert isSorted(a); show(a); } }
1. sort()方法的作用其实在于安排多次merge()方法调用的正确顺序。
2. 对于长度为N的任意数组,自顶向下的归并排序需要1/2NlgN至NlgN次比较,最多需要访问数组6NlgN次。
3. 通过以下方式我们还可能大幅度缩短归并排序的运行时间:
· 对小规模子数组使用插入排序
· 测试数组是否已经有序
· 不将元素复制到辅助数组
2.2.3 自底向上的归并排序
import java.lang.Math; public class MergeBU { private static Comparable[] aux; // 归并所需的辅助数组 public static void sort(Comparable[] a) { // 进行lgN次两两归并 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)); } public static void merge(Comparable[] a, 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++) { // 将a[lo..hi]复制到aux[lo..hi] aux[k] = a[k]; } 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++]; // 右 > 左 } } private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } private static void exch(Comparable[] a, int i, int j) { Comparable t = a[i]; a[i] = a[j]; a[j] = t; } private static void show(Comparable[] a) { // 在单行中打印数组 for (int i = 0; i < a.length; i++) StdOut.print(a[i] + " "); StdOut.println(); } public static boolean isSorted(Comparable[] a) { // 测试数组元素是否有序 for (int i = 1; i < a.length; i++) if (less(a[i], a[i - 1])) return false; return true; } public static void main(String[] args) { // 从标准输入读取字符串,将它们排序并输出 String[] a = In.readStrings(); sort(a); assert isSorted(a); show(a); } }
1. 首先我们进行的是两两归并(把每个元素想象成一个大小为1的数组),然后是四四归并(将两个大小为2的数组归并成一个有4个元素的数组),然后是八八的归并,一直下去。在每一轮归并中,最后一次归并的第二个子数组可能比第一个子数组要小(但这对merge()方法不是问题),如果不是的话所有的归并中两个数组大小都应该一样,而在下一轮中子数组的大小会翻倍。
2. 对于长度为N的任意数组,自底向上的归并排序需要1/2NlgN至NlgN次比较,最多需要访问数组6NlgN次。
· 当数组长度为2的幂时,自顶向下和自底向上的归并排序所用的比较次数和数组访问次数正好相同,只是顺序不同;
· 其他时候,两种方法的比较和数组访问的次序会有所不同。
3. 自底向上的归并排序比较适合用链表组织的数据。
2.2.4 排序算法的复杂度
1. 没有任何基于比较的算法能够保证使用少于lg(N!)~NlgN次比较将长度为N的数组排序。
即最好情况至少是lg(N!),最坏情况至少是NlgN。
2. 归并排序在最坏情况下的比较次数和任意基于比较的排序算法所需的最少比较次数都是~NlgN。