归并排序
两个有序数组归并成一个更大的有序数组,就叫归并。
归并排序是一种递归排序算法,就对一个数组来说,可以先将它(递归地)分成两半分别排序,然后将结果归并起来。
原地归并的抽象方法
两个不同的有序数组如何实现归并?
一个最简单直接地方法就是创建一个最够大的第三数组,然后将两个有序数组的元素从大到小的排到第三数组中,这就叫原地归并。
/**
* 原地归并的抽象方法
* 归并,数组两边一定要是有序的
*/
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;
//将a[lo..hi]复制到aux[lo..hi]
for(int k=lo; k<=hi; k++){
aux[k] = a[k];
}
//归并回到a[lo..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(aux[j], aux[i])){
a[k] = aux[j++];
}else{
a[k] = aux[i++];
}
}
}
主要操作就是第二个for循环里的四个判断:
1、数组1走完(将数组2当前元素放入数组3)
2、数组2走完(将数组1当前元素放入数组3)
3、数组1当前元素小于数组2当前元素(将数组1当前元素放入数组3)
4、数组2当前元素小于等于数组1当前元素(将数组2当前元素放入数组3)
自顶向下的归并排序
基于原地归并的抽象实现一种递归归并。
这段递归代码是归纳证明算法能正确地将数组排序的基础:**如果能将两个子数组排序,就能通过归并两个子数组来对整个数组排序。**这一切是通过递归实现的,也叫递归归并。
完整代码:
public class 归并排序 {
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);
//左右归并
merge(a, lo, mid, hi);
}
//归并,数组两边一定要是有序的
public static void merge(Comparable[] a, int lo, int mid, int hi) {
int i = lo, j = mid + 1;
Comparable[] aux = new Comparable[hi + 1];
for (int k = lo; k <= 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[i], aux[j])) {
a[k] = aux[i++];
} else {
a[k] = aux[j++];
}
}
}
public static boolean less(Comparable v, Comparable w){
//对元素进行比较
return v.compareTo(w)<0;
}
public static void exch(Comparable[] a,int i,int j){
//交换元素
Comparable t=a[i];
a[i]=a[j];
a[j]=t;
}
public static void show(Comparable[] a){
//在单行中打印数组
for(int i=0;i<a.length;i++){
System.out.print(a[i]+"");
System.out.println();
}
}
public static boolean isSorted(Comparable[] a){
//测试数组元素是否有序
for(int i=0;i<a.length;i++){
if(less(a[i],a[i-1])){
return false;
}
}
return true;
}
public static void main(String[] args){
String[] a={"4","2","5","2","7","5","3","2"};
sort(a);
assert isSorted(a);
show(a);
}
}
以一个数组为例展示调用过程:
sort(a,0,7)
将左半部分排序
左sort(a,0,3)
->左sort(a,0,1)
->>左sort(a,0,0)
->>右sort(a,1,1)
->>merge(a,0,0,1)
->右sort(a,2,3)
->>左sort(a,2,2)
->>右sort(a,3,3)
->>merge(a,2,2,3)
->merge(a,0,1,3)
右sort(a,4,7)
->左sort(a,4,5)
->>左sort(a,4,4)
->>右sort(a,5,5)
->>merge(a,4,4,5)
->右sort(a,6,7)
->>左sort(a,6,6)
->>右sort(a,7,7)
->>merge(a,6,6,7)
->merge(a,4,5,7)
merge(a,0,3,7)
算法第四版的配图:
对于长度为N的任意数组,自顶向下的归并排序需要 1/2NlogN 至 NlogN次比较
对于长度为N的任意数组,自顶向下的归并排序最多需要访问数组 6NlogN次
每次归并最多需要访问数组6N次,2N次复制,2N次将排好的元素移动回去,另外最多比较2N次,另最多有logN次归并。
自底向上的归并排序
先归并小数组,再归并大数组
public static void sort(Comparable[] a) {
int n = a.length;
aux = new Comparable[n];
for (int sz = 1; sz < n; sz = sz + sz) //sz子数组的大小
for (int lo = 0; lo < n - sz; lo += sz + sz) //lo子数组索引
merge(a, lo, lo + sz - 1, Math.min(lo + 2 * sz - 1, n - 1));
}
自底向上的归并排序会多次遍历整个数组,根据子数组大小进行两两归并。子数组的大小sz的初始值为1,每次加倍。最后一个子数组的大小只有在数组大小是sz的偶数倍的时候才会等于sz(否则它会比sz小)。
对于长度为N的任意数组,自底向上的归并排序需要1/2NlgN至NlgN次比较,最多访问数组6NlgN次。
复杂度
时间复杂度:NlogN
空间复杂度:
对数组来说,空间复杂度为N;对链表来说,递归方法空间复杂度为N,迭代方法为O(1)
三项优化
①对小规模子数组使用插入排序
用不同的方法处理小规模数组能改进大多递归算法的性能,在小数组上上,插入排序可能比并归排序更快。
②测试数组是否有序
根据归并排序的特点,每次归并的两个小数组都是有序的,当a[mid]<=a[mid+1]时我们可以跳过merge方法,这样并不影响排序的递归调用。
③不将元素复制到辅助数组
我们可以节省将数组复制到辅助数组的时间,这需要一些技巧。先克隆原数组到辅助数组,然后在之后的递归交换输入数组和辅助数组的角色(通过看代码更容易理解)
参考文章:
《算法第四版》
https://www.cnblogs.com/Unicron/p/9637488.html