在本节中我们所讨论的算法是基于归并这个简单的操作,即将两个有序的数组归并成一个更大的有序数组.很快人们就根据这个操作发明了一种简单的归并排序的算法:归并排序
要将一个数组排序,可以先递归的将他分成两半分别排序,然后将结果归并起来.你将会看到归并排序最吸引人的性质是他能够保证将任意长度为N的数组排序所需的时间和NlogN成正比,缺点是他需要额外空间和N成正比.简单的归并排序如图所示
1.原地归并的抽象方法
实现归并的一种直截了当的方法是将两个不同的有序数组归并到第三个数组之中,两个数组中的元素都应该是实现了Comparable接口.实现的方法很简单,创建一个适当大小的数组然后将两个输入数组中的元素一个个从小到大放入数组之中
原地归并的抽象方法
package 排序.归并排序;
/**
* 原地归并的方法
*/
public class SiteMerge {
public static void merge(Comparable[] a, int lo, int mid, int hi){
int i = lo, j = mid+1;
int [] aux = new int[i+j];
for (int k = lo; k <= hi; k ++){
aux[k]= (int) a[k];
}
//最小的归并排序都会2个数或者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++];//右半段当前元素大于左半端当前元素
}
}
/**
* 判断v是否小于w
* @param v
* @param w
* @return
*/
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w) < 0;
}
}
该方法现将所有元素复制到aux[]中,然后在归并回a[]中.方法在归并时(第二个for循环)进行了四个条件判断:
- 左半端用尽(取右半段元素)
- 右半段用尽(取左半端元素)
- 右半段的当前元素小于左半端的当前元素(取右半段元素)
- 右半段当前元素大于左半端当前元素(取左半端当前元素)
2自顶向下的归并排序
一下算法基于原地归并的抽象实现了另一种递归归并,这也是应用高效算法设计中分治思想的最典型的一个例子.这段归并代码是归纳证明算法能够正确的将数组排序的基础:如果它能够将两个子数组排序,他就能够归并两个子数组来将整个数组排序
package 排序.归并排序;
/**
* 自顶向下归并排序
*/
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){
//将数组排序
if (hi <= lo) return;
int mid = lo+(hi-lo)/2;
sort(a, lo, mid);
sort(a, mid+1,hi);
}
private static void merge(Comparable []a, int lo, int mid, int hi){
int i = lo, j = hi;
for (int k = 0; k < lo+hi; k ++){
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 w, Comparable v){
return w.compareTo(v) < 0;
}
}
- 命题:对于长度为Nd的任意数组,自顶向下的归并排序需要1/2NlogN至NlogN次比较
注意对较小规模子数组还是使用插入排序
3自底向上归并排序
递归实现的归并排序是算法设计中分治思想的典型应用.我们将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题
尽管我们考虑的问题是归并两个大数组,实际上我们归并的数组都非常小.实现归并排序的另一种方法是先归并那些微型数组,然后再成对归并得到的子数组,如此这般知道我们将整个数组归并在一起.这种实现方法比标准递归方法所用的代码量更少.首先我们进行的是两两归并(把,每个元素想象成大小为1的数组),然后进行四四归并(将一个大小为2的数组归并成大小为4的数组),然后是八八归并,一直下去.在,每一轮归并中,最后一次归并的第二个子数组可能比第一个子数组要小(但这对merge()方法不是问题),如果不是的话所有归并中两个数组大小都应该是一样的,而在下一轮中子数组的大小可能会翻倍,过程如下图所示
算法实现如下
package 排序.归并排序;
/**
* 自底向上归并排序
*/
public class MergeBU {
private static Comparable [] aux;//归并所需辅助数组
public static void sort(Comparable[] a){
aux = new Comparable[a.length];
int N = a.length;
//子数组大小从一开始以两倍递增不断增大
for (int sz=1; sz < N; 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));
}
}
}
private static void merge(Comparable[] a, int lo, int mid, int hi){
int i = lo, j = hi;
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(a[j], a[i])) a[k]=aux[j++];//右半段当前元素小于左半端当前元素
else a[k] = aux[i++];//右半段当前元素大于左半端当前元素
}
}
private static boolean less(Comparable w, Comparable v){
return w.compareTo(v) < 0;
}
}
自底向上的归并排序会多次遍历整个数组,根据数组大小进行两两归并.子数组的大小sz的初始值为1,每次加倍.最后一个子数组的大小只有在数组大小是sz的偶数倍的时候才会等于sz(否则他会比sz小)
- 命题H: 对于长度为N的任意数组,自底向上的归并排序需要1/2NlgN至NlgN次比较,最多访问数组6NlgN次