Q:什么是归并排序?
A:它是建立在归并操作上的一种有效的排序算法;是采用分治法的一个非常典型的应用;是一种稳定的
基本思想
要将一个数组排序,可以先(递归地)将它分成两半分别排序,然后将结果归并起来。
优点?它能保证将任意长度为 N 的数组排序所需时间和 NlogN 成正比;
缺点?所需的额外空间和 N 成正比。
原地归并的抽象方法
Q:为什么需要原地归并?
A:因为用归并将一个大数组排序时,需要进行多次归并,而且每次归并会都创建一个新数组来存储排序结果会带来问题。
Q:原地归并实现了什么?
A:可以先将前半部分排序,再将后半部分排序,然后数组中移动元素而不需要使用额外的空间。(将两个有序的数组归并为一个有序的数组)
Q:如何实现归并?
A:创建一个适当大小的数组,然后将两个输入数组中的元素一个个从小到大方法这个数组中。
代码实现
根据排序算法类的模板实现归并排序(提醒:点蓝字查看详情)
/**
* 将子数组 arr[lo...mid] 和 arr[mid+1...hi] 归并成一个有序的数组并将结果存放在 arr[lo...hi] 中。
* 将所有元素复制到一个辅助数组中,再把归并的结果放回原数组中
*/
private static void merge(Comparable[] arr, int lo, int mid, int hi) {
// 将 arr[lo...mid] 和 arr[mid+1...hi] 归并
int indexI = lo;
int indexJ = mid + 1;
// 将 a[lo...hi] 复制到 aux[lo...hi]
// System.arraycopy(arr, lo, aux, lo, hi - lo + 1);
for (int indexK = lo; indexK <= hi; indexK++) {
aux[indexK] = arr[indexK];
}
// 归并回到 arr[lo...hi]
for (int indexK = lo; indexK <= hi; indexK++) {
// 左半边用尽(取右半边的元素)
if (indexI > mid) {
arr[indexK] = aux[indexJ++];
}
// 右半边用尽(取左半边的元素)
else if (indexJ > hi) {
arr[indexK] = aux[indexI++];
}
// 右半边的当前元素小于左半边的当前元素(取右半边的元素)
else if (less(aux[indexJ], aux[indexI])) {
arr[indexK] = aux[indexJ++];
}
// 右半边的当前元素大于左半边的当前元素(取左半边的元素)
else {
arr[indexK] = aux[indexI++];
}
}
}
自顶向下的归并排序(化零为整,递归解决)
由于以上的原地归并只能将两个有序的数组归并成一个有序的数组,所以得基于原地归并的抽象去实现一种递归归并。
要对子数组 arr[lo...hi] 进行排序,先将它分为 arr[lo...mid] 和 arr[mid+1...hi] 两部分,分别通过递归调用将它们单独排序,最后将有序的子数组归并为最终的排序结果。
Q:为什么它能将正确的排序?
A:如果它能将两个子数组排序,那么它就可以通过归并两个子数组来将整个数组排序。
运行轨迹
代码实现
根据排序算法类的模板实现选择排序(提醒:点蓝字查看详情)
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) {
// 将数组 arr[lo...hi] 排序
if (hi <= lo) return;
int mid = lo + ((hi - lo) >> 1);
sort(arr, lo, mid); // 将左半边排序
sort(arr, mid + 1, hi); // 将右半边排序
merge(arr, lo, mid, hi); // 归并结果
}
性能分析
最佳情况:T(n) = O(n)
最差情况:T(n) = O(nlogn)
平均情况:T(n) = O(nlogn)
对于长度为 N 的任意数组,自顶向下的归并排序需要 1/2NlgN - NlgN 次比较。
对于长度为 N 的任意数组,自顶向下的归并排序最多需要访问数组 6NlgN 次(2N 次用来复制、2N 次用来将排好序的元素移动回来、另外最多比较 2N 次)。
Q:主要缺点是什么
A:辅助数组所使用的额外空间和 N 的大小成正比。
自底向上的归并排序(循序渐进的解决)
实现归并的另一种方法:先归并那些微型数组,然后再成对归并得到子数组。首先两两归并,然后四四归并,然后八八归并,一直下去。
运行轨迹
代码实现
根据排序算法类的模板实现选择排序(提醒:点蓝字查看详情)
private static Comparable[] aux; // 归并所需的辅助数组
public static void sortBU(Comparable[] arr) {
int N = arr.length;
aux = new Comparable[N];
// sz 的初始值为 1 , 每次加倍
for (int sz = 1; sz < N; sz = sz + sz) { // sz子数组大小
for (int lo = 0; lo < N - sz; lo += sz + sz) { // lo:子数组索引
// 最后一个子数组的大小,只有在数组大小是 sz 的偶数倍时,才会等于sz,否则会比 sz 小
merge(arr, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
}
}
}
性能分析
对于长度为 N 的任意数组,自底向上的归并排序需要 1/2NlgN - NlgN 次比较,最多访问数组 6NlgN 次。(每一边访问数组 6N 次,比较次数 N/2 - N)
当数组长度为 2 的幂时,自顶向下和自底向上的归并排序所用的比较次数和数组访问次数正好相同,只是顺序不同。
自底向上的归并比较适合用链表组织的数据。
总结
没有任何基于比较的算法能够保证使用少于 lg(N!) - NlgN 次比较将长度为 N 的数组排序。
归并排序是一种渐进最优的基于比较排序的算法。
优化方案
①、直接将辅助数组作为参数传入,而不直接使用静态数组。
②、对小规模子数组使用插入排序,一般可以将归并排序的时间缩短 10% ~ 15%;
③、判断测试数组是否已经有序,如果 arr[mid] <= arr[mid+1],我们就认为数组已经是有序的并跳过merge() 方法,可以是任意有序的子数组算法的运行时间变为线性的。
④、merge() 方法中不将元素复制到辅助数组,节省数组复制的时间。调用两种排序方法,一种:将数据从输入数组排序到辅助数组;另一种:将数据从辅助数组排序到输入数组。
重点:在每个层次交换输入数组和辅助数组的角色。
优化代码
/**
* 归并排序优化方案(其实并不是特别明显,稳定性也不好)
*
* @author TinyDolphin
* 2017/11/6 11:45.
*/
public class MergePlus {
// 经验之谈:数组的长度为 7 时,切换
private static final int CUTOFF = 7;
private static void merge(Comparable[] src, Comparable[] dst, int lo, int mid, int hi) {
int indexI = lo;
int indexJ = mid + 1;
for (int indexK = lo; indexK <= hi; indexK++) {
if (indexI > mid) {
dst[indexK] = src[indexJ++];
} else if (indexJ > hi) {
dst[indexK] = src[indexI++];
} else if (less(src[indexJ], src[indexI])) {
dst[indexK] = src[indexJ++];
} else {
dst[indexK] = src[indexI++];
}
}
}
// 将数组 arr 排序到数组 aux
private static void sort(Comparable[] src, Comparable[] dst, int lo, int hi) {
// 优化方案②:应该在子数组长度为 7 的时候切换到插入排序
if (hi <= lo + CUTOFF) {
insertionSort(dst, lo, hi);
return;
}
int mid = lo + ((hi - lo) >> 1);
// 优化方案④:在每个层次交换输入数组和辅助数组的角色
sort(dst, src, lo, mid);
sort(dst, src, mid + 1, hi);
//优化方案③:判断测试数组是否已经有序
if (!less(src[mid + 1], src[mid])) {
System.arraycopy(src, lo, dst, lo, hi - lo + 1);
return;
}
// 优化方案④:merge() 方法中不将元素复制到辅助数组
merge(src, dst, lo, mid, hi);
}
public static void sort(Comparable[] arr) {
// 优化方案①:直接将辅助数组作为参数传入
Comparable[] aux = arr.clone();
sort(aux, arr, 0, arr.length - 1);
}
private static void insertionSort(Comparable[] arr, int lo, int hi) {
for (int indexI = lo; indexI <= hi; indexI++) {
for (int indexJ = indexI; indexJ > lo && less(arr[indexJ], arr[indexJ - 1]); indexJ--) {
exch(arr, indexJ, indexJ - 1);
}
}
}
/**
* 比较两个元素的大小
*
* @param comparableA 待比较元素A
* @param comparableB 待比较元素B
* @return 若 A < B,返回 true,否则返回 false
*/
private static boolean less(Comparable comparableA, Comparable comparableB) {
return comparableA.compareTo(comparableB) < 0;
}
/**
* 将两个元素交换位置
*
* @param arr 待交换元素所在的数组
* @param indexI 第一个元素索引
* @param indexJ 第二个元素索引
*/
private static void exch(Comparable[] arr, int indexI, int indexJ) {
Comparable temp = arr[indexI];
arr[indexI] = arr[indexJ];
arr[indexJ] = temp;
}
/**
* 打印数组的内容
*
* @param arr 待打印的数组
*/
private static void show(Comparable[] arr) {
for (int index = 0; index < arr.length; index++) {
System.out.print(arr[index] + " ");
}
System.out.println();
}
/**
* 判断数组是否有序
*
* @param arr 待判断数组
* @return 若数组有序,返回 true,否则返回 false
*/
public static boolean isSort(Comparable[] arr) {
for (int index = 1; index < arr.length; index++) {
if (less(arr[index], arr[index - 1])) {
return false;
}
}
return true;
}
}
优化测试代码
【高效复制数组的方法】,提示:点击蓝色字体查看方法详情。
public class Main {
public static void main(String[] args) {
int length = 10000000; // 千万数据量级别
Integer[] arr = new Integer[length];
Integer[] arr2 = new Integer[length];
for (int index = 0; index < length; index++) {
arr[index] = new Random().nextInt(length) + 1;
}
//高效复制数组的方法
System.arraycopy(arr, 0, arr2, 0, arr.length);
long start = System.currentTimeMillis();
Merge.sort(arr);
long end = System.currentTimeMillis();
System.out.println("耗费时间:" + (end - start) + "ms");
assert Merge.isSort(arr);
start = System.currentTimeMillis();
MergePlus.sort(arr2);
end = System.currentTimeMillis();
System.out.println("耗费时间:" + (end - start) + "ms");
assert MergePlus.isSort(arr2);
}
}
优化测试结果
注意:优化结果虽然差不多,但是当其数组接近有序的时候,速度有了可观的提升。
注意:编译器默认不适用 assert 检测(但是junit测试中适用),所以要使用时要添加参数虚拟机启动参数-ea 具体添加过程,请参照eclipse 和 IDEA 设置虚拟机启动参数