2.2归并排序
2.2.1 原地归并的抽象方法
归并:将两个有序的数组归并成一个更大的有序数组。
原地归并的抽象方法merge(a,lo,mid ,hi),将子数组a[lo..mid]和a[id+1..hi]归并成一个有序的数组并将结果存放在a[lo..hi]中,代码如下:
public static voidmerge(Comparator[] a,intlo,intmid,
inthi){
int i =lo, j = mid +1;
for(intk = lo; k <= hi; k++)//将a[lo..hi]复制到aux[lo..hi]中
aux[k] = a[k];
for(intk = 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++];
}
}
代码解释:
1. 前提是子数组是有序的;
2. 将a[lo..hi]复制到aux[lo..hi]中;
3. 左半边用尽:取右半边的元素;
4. 右半边用尽:取左半边的元素;
5. 右半边的当前元素小于左半边的当前元素:取右半边的元素
6. 右半边的当前元素大于等于左半边的当前元素:取左半边的元素
算法复杂度:
需要比较的次数为2^n / 2~ 2^n,其中2^n = N(数组的长度)
2.2.2 自顶向下的归并排序
代码如下:
private static Comparator[] aux; public static void sort(Comparator[] a){ aux = new Comparator[a.length]; sort(a,0,a.length-1); } public static void sort (Comparator[] 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); }
代码执行可用以下图来理解:merge(..0..1) merge(..2..3) merge(..0..3) merge(..4..5) merge(..6..7)merge(..4..7) merge(..0..7) merge(..8..9) merge(..10..11) ……
代码解释:
1. 分治的思想:将大问题分割成小问题分别解决,然后所有小问题的答案来解决整个大问题;
2. Sort()的作用本质是安排多次merge()方法调用的顺序。
算法复杂度:
按照上图依赖树来表示merge()方法的调用,若树有n层,则0到n-1间的任意k,自顶向下的第k层有2^k个数组,每个数组长度为2^n-k,因此每层的比较次数是
2^k * 2^n-k / 2~ 2^k * 2^n-k,即 2^n / 2 ~ 2^n,n层总共为n * 2^n / 2 ~ n * 2^n。
若N = 2^n(长为N的数组),归并排序需要1/2 * NlgN至NlgN,复杂度为NlgN。
长为N的数组,自定向下的归并排序最多需要访问数组6NlgN次(不懂)
应用:
可以归并排序处理数百万甚至更大规模的数组,这是插入排序选择排序做不到的
2.2.2.1改进-对小规模子数组使用插入排序
递归使小规模问题中的方法调用过于频繁。插入排序在处理小规模数组(长度小于15)比归并排序更快,一般可将归并排序的运行时间缩短10%~15%。
2.2.2.2改进-判断数组是否已经有序
添加判断条件:如果a[mid] <= a[mid+1],就认为数组已经有序,跳过merge()方法
2.2.2.3改进-不将元素复制到辅助数组
在递归调用的每个层次交换输入数组和辅助数组(不懂)
2.2.3 自底向上的归并排序
算法思想:先归并微型数组,在成对归并得到的子数组。
先两两归并,再四四归并,在八八归并……最后一次归并的第二个子数组可能比第一个子数组要小,如果不是的话所有归并的两个数组大小是一样的。
算法实现:
public static void sort(Comparator[] a){ int N = a.length; aux = new Comparator[N]; for(int sz = 1; sz < N; sz = sz + sz){//每次归并的数组长度为sz*2 for(int lo = 0; lo < N - sz; lo += sz + sz){ merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));//当最后一次调用merge方法时,第二个数组不等于(小于)第一个数组时取N-1 } } }
算法复杂度:
对于长度为N的数组,比较的次数是1/2 * NlgN~NlgN,最多访问数组6NlgN次。
应用:
自底向上归并排序适合用链表组织数据。
2.2.4 排序算法的复杂度
排序算法的复杂度最小是NlgN,试图寻找复杂度小于NlgN排序算法是无意义的
问答
不同算法适用于不同应用场景的不同输入
归并排序比希尔排序快嘛?实际中运行时间的差距在常数级别内,相对性能取决于具体实现
不把aux[]声明为merge()方法的局部变量是为了避免每次归并时创建一个新的数组
所有元素都相同时,归并排序的时间是线性的(前提是有额外的判定)
如果有多个不同的重复值,运行时间仍然是线性对数的