归并排序算法
一、归并排序的概念
归并:即将两个有序的数组归并成一个更大的有序数组。根据归并这一操作,得出一种简单的递归排序算法:归并排序。要将一个数组排序,可以先(递归地)将它分为两半分别排序,然后将结果归并起来。
归并排序的一个重要性质:它能够保证将任意长度为N的数组排序所需时间和NlogN成正比。它的主要缺点则是:它所需的额外空间和N成正比。
二、原地归并的抽象方法
(一)、原地归并的抽象方法的概念
实现归并的一种直截了当的办法是将两个不同的有序数组归并到第三个数组中,两个数组中的元素应该都实现了Comparable
接口。实现的方法很简单,创建一个适当大小的数组然后将两个输入数组的元素一个个从小到大放入这个数组中。
但是,当用归并将要给大数组排序时,需要进行很多次归并,因此在每次归并时都创建一个新数组来存储排序结果会带来问题。
因此,假如有一种原地归并的方法,就可以先将前半部分排序,然后将后半部分排序,然后再数组中移动元素而不需要额外的空间。但实际上已有的实现都非常复杂,尤其是和使用额外空间的方法相比。
但将原地归并并抽象化仍然是有帮助的,与之对应的是我们的方法merge(a,lo,mid,hi),它将子数组a[lo…mid]和a[mid+1…hi]归并成一个有序的数组并将结果存放在a[lo…hi]中。
(二)、原地归并的抽象方法的代码示例
该方法先将所有元素复制到aux[]中,然后再归并回arr[]中。
在第二个for循环(归并)时进行了4个条件判断:左半边用尽(取右半边的元素)、右半边用尽(取左半边的元素)、右半边的当前元素小于左半边的当前元素(取右半边的元素)以及右半边的当前元素大于等于左半边的当前元素(取左半边的元素)。
public static void merge(Comparable[] arr, int lo, int mid, int hi) {
// 将arr[lo...mid]和a[mid+1...hi]归并
int i = lo;
int j = mid + 1;
Comparable[] aux = new Comparable[arr.length];
for (int k = lo; k <= hi; k++) {
aux[k] = arr[k];
}
for (int k = lo; k <= hi; k++) {
if (i > mid) {
arr[k] = aux[j++];
} else if (j > hi) {
arr[k] = aux[i++];
} else if (less(aux[j], aux[i])) {
arr[k] = aux[j++];
} else {
arr[k] = aux[i++];
}
}
}
// 对元素进行比较
private static boolean less(Comparable v, Comparable w) {
// 返回-1/0/1:表示v小于/等于/大于w
return v.compareTo(w) < 0;
}
三、自顶向下的归并排序
(一)、自顶向下的归并排序的概念
自顶向下的归并排序是基于原地归并的抽象实现了另一种递归归并。
这段递归代码是归纳证明算法能够正确地将数组排序的基础:如果它能将两个子数组排序,它就能够通过归并两个子数组来将整个数组排序。
(二)、自顶向下的归并排序的代码示例
public class Merge {
// 归并所需的辅助数组
private static Comparable[] aux;
public static void sort(Comparable[] arr) {
aux = new Comparable[arr.length];
sort(arr, 0, arr.length - 1);
}
private static void sort(Comparable[] arr, int lo, int hi) {
if (hi <= lo) {
return;
}
int mid = lo + (hi - lo) / 2;
sort(arr, lo, mid);
sort(arr, mid + 1, hi);
merge(arr, lo, mid, hi);
}
public static void merge(Comparable[] arr, int lo, int mid, int hi) {
// 将arr[lo...mid]和a[mid+1...hi]归并
int i = lo;
int j = mid + 1;
for (int k = lo; k <= hi; k++) {
aux[k] = arr[k];
}
for (int k = lo; k <= hi; k++) {
if (i > mid) {
arr[k] = aux[j++];
} else if (j > hi) {
arr[k] = aux[i++];
} else if (less(aux[j], aux[i])) {
arr[k] = aux[j++];
} else {
arr[k] = aux[i++];
}
}
}
// 对元素进行比较
private static boolean less(Comparable v, Comparable w) {
// 返回-1/0/1:表示v小于/等于/大于w
return v.compareTo(w) < 0;
}
}
(三)、自顶向下的归并排序的基本性质
1、对于长度为N的任意数组,自顶向下的归并排序需要1/2NlgN——NlgN次比较。
2、对于长度为N的任意数组,自顶向下的归并排序最多需要访问数组6NlgN次。
以上两个性质说明了归并排序所需的实际和NlgN成正比,它表明我们只需要比遍历整个数组多个对数因子的时间就能将一个庞大的数组排序。可以用归并排序处理数百万甚至更大规模的数组,这是插入排序或选择排序做不到的。
归并排序的主要缺点是辅助数组所使用的额外空间和N的大小成正比。
四、自底向上的归并排序
(一)、自底向上的归并排序的概念
递归实现的归并排序是算法设计中分治思想的典型应用。我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决大问题。尽管我们考虑的问题是归并两个大数组,实际上我们归并的数组大多数都非常小。
实现归并排序的另一种方法是先归并那些微型数组,然后再成对归并得到的子数组,如此这般,直到我们将整个数组归并在一起。这种实现方法比标准递归方法所需要的代码量更少。
(二)、自底向上的归并排序的代码示例
public class MergeBU {
// 归并所需的辅助数组
private static Comparable[] aux;
public static void sort(Comparable[] arr) {
// 进行lgN次两两归并
int n = arr.length;
aux = new Comparable[n];
// sz:子数组大小
for (int sz = 1; sz < n; sz = sz + sz) {
// lo:子数组索引
for (int lo = 0; lo < n - sz; lo += sz + sz) {
merge(arr, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, n - 1));
}
}
}
private static void merge(Comparable[] arr, int lo, int mid, int hi) {
// 将arr[lo...mid]和a[mid+1...hi]归并
int i = lo;
int j = mid + 1;
for (int k = lo; k <= hi; k++) {
aux[k] = arr[k];
}
for (int k = lo; k <= hi; k++) {
if (i > mid) {
arr[k] = aux[j++];
} else if (j > hi) {
arr[k] = aux[i++];
} else if (less(aux[j], aux[i])) {
arr[k] = aux[j++];
} else {
arr[k] = aux[i++];
}
}
}
// 对元素进行比较
private static boolean less(Comparable v, Comparable w) {
// 返回-1/0/1:表示v小于/等于/大于w
return v.compareTo(w) < 0;
}
}
(三)、自底向上的归并排序的基本性质
1、对于长度为N 的任意数组,自底向上的归并排序需要1/2NlgN至NlgN次比较,最多访问数组6NlgN次。
当数组长度为2的幂时,自顶向下喝自底向上的归并排序所用的比较次数喝数组访问次数正好相同,只是顺序不同。其他时候,两种方法的比较和数组访问的次序会有所不同。
自底向上的归并排序比较使用用链表组织的数组。这种方法只需要重新组织链表链接就能将链表原地排序(不需要创建任何新的链表节点)。