Mergesort
1 归并排序 Mergesort
1.1 原地归并![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/9bd67dced8806fa661e431215587351d.png)
- 抽象的“原位归并”
- 需要一个辅助数组记录数据,i和j比较,小的放回原数组,index右移1位,两个子数组继续比较;如果两边数组值一样,取左边数组的
package Chapter02;
public class Merge {
private static void merge(Comparable[] a,Comparable[] aux,int lo,int mid,int hi){
assert Riqi.isSorted(a,lo,mid); //precondition: a[lo...mid] sorted
assert Riqi.isSorted(a,mid+1,hi); //precondition: a[mid+1...hi] sorted
//copy
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
//merge
int i = lo, j = mid+1;
for (int m = lo; m <= hi ; m++) {
if (i > mid) a[m] = aux[j++];//如果i走到边界,说明左边子数组已经全部移上去了,就把j移上去
else if (j > hi) a[m] = aux[i++];//如果j走到边界,说明右边子数组已经全部移上去了,就把i移上去
else if (Riqi.less(aux[j],aux[i])) a[m] = aux[j++];//将j移到原数组,然后j自增
else a[m] = aux[i++];//将i移到原数组,然后i自增
}
assert Riqi.isSorted(a,lo,hi); //precondition: a[lo...hi] sorted
}
}
}
- assert后跟boolean:在最后表明做了什么,在前面表明要做什么
- assert默认是禁用的
1.2 自顶向下的归并排序
package Chapter02;
import java.util.Arrays;
public class Merge {
private static void merge(Comparable[] a,Comparable[] aux,int lo,int mid,int hi){
assert Riqi.isSorted(a,lo,mid); //precondition: a[lo...mid] sorted
assert Riqi.isSorted(a,mid+1,hi); //precondition: a[mid+1...hi] sorted
//copy
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
//merge
int i = lo, j = mid+1;
for (int m = lo; m <= hi ; m++) {
if (i > mid) a[m] = aux[j++];//如果i走到边界,说明左边子数组已经全部移上去了,就把j移上去
else if (j > hi) a[m] = aux[i++];//如果j走到边界,说明右边子数组已经全部移上去了,就把i移上去
else if (Riqi.less(aux[j],aux[i])) a[m] = aux[j++];//将j移到原数组,然后j自增
else a[m] = aux[i++];//将i移到原数组,然后i自增
}
assert Riqi.isSorted(a,lo,hi); //precondition: a[lo...hi] sorted
}
//不要将辅助数组在这里创建,会多出许多额外的小数组的花费
private static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){
if (hi <= lo) return;//先检查下标,如果检查不通过,后续不运行 false则继续运行后面代码
int mid = lo + (hi - lo) / 2;
//反复套娃,直到最小的数组长度变为2
sort(a,aux,lo,mid); //左半边排序
sort(a,aux,mid+1,hi);//右半边排序
merge(a,aux,lo,mid,hi); //归并结果
}
private static void sort(Comparable[] a){
Comparable[] aux = new Comparable[a.length];
sort(a,aux,0,a.length-1);
}
public static void main(String[] args) {
Comparable[] a = {32,23,1,134,5,23,856,54,90};
sort(a);
System.out.println(Arrays.toString(a)); // [1, 5, 23, 23, 32, 54, 90, 134, 856]
}
}
- 如果执行了return语句,那么后面的语句将会不执行
- 把原数组不断对半分,直到变成两两一组
1.3 复杂度
- 在数据量巨大的情况下,归并排序与插入排序相比具有优越性
- 合并时最多需要比较N次(比一次,放回原数组一个元素):如果左右没有一边提前排完,就需要N次比较
- 数组访问次数:merge所需次数是6N(从a拷贝到aux算2次数组访问) —> 2N次用来复制,2N次用来将排好序的元素移动回去,另外最多比较2N次
1.3.1 图示法
- 假设N=2n
- 2n需要不断除以2,共除log2N次,即log2N层
- 每层归并都需要比较N次,将所有层相加即为总比较次数
1.3.2 代数法
1.3.2 数学归纳法
- 归纳假设
- log2(2N) = log2(N) + log2(2) = log2(N) + 1
1.4 内存占用
- 辅助数组需要占内存一半的空间,所以不能实现真正的原地排序
- 希尔排序、插入排序等是真正的原地排序
1.5 归并排序的改进
1.5.1 引入插入排序
- 对于小的数组采用归并排序,不停递归、调用方法使性能损失
- 可以对小于某个数值的小数组采用插入排序
private static final int CUTOFF = 7; //设置判断是否使用插入排序的初始值
//不要将辅助数组在这里创建,会多出许多额外的小数组的花费
private static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){
//if (hi <= lo) return;//先检查下标,如果检查不通过,后续不运行 false则继续运行后面代码
//小数组使用插入排序
if(hi <= lo + CUTOFF - 1){ //末项 - 首项 <= 6 数组长度小于7
Insertion.sort(a, lo, hi);
return;}
int mid = lo + (hi - lo) / 2;
//反复套娃,直到最小的数组长度变为2
sort(a,aux,lo,mid); //左半边排序
sort(a,aux,mid+1,hi);//右半边排序
merge(a,aux,lo,mid,hi); //归并结果
}
1.5.2 子数组有序则直接复制
- 检测待归并的两个子数组是否已经有序,如果有序,则可跳过此轮归并
- 只需检测前一半最大的数是否小于后一半最小的数
private static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){
if (hi <= lo) return;//先检查下标,如果检查不通过,后续不运行 false则继续运行后面代码
int mid = lo + (hi - lo) / 2;
//反复套娃,直到最小的数组长度变为2
sort(a,aux,lo,mid); //左半边排序
sort(a,aux,mid+1,hi);//右半边排序
if (!Riqi.less(a[mid+1],a[mid])) return;//如果数组已经有序则直接复制,不再merge 使用!非可以将等于包含进来
merge(a,aux,lo,mid,hi); //归并结果
}
1.5.3 节省拷贝到辅助数组的时间
- 通过再递归中交换参数来避免每次归并时都要复制数组到辅助数组
- 每轮递归时,转换原数组和辅助数组的角色
package Chapter02;
import java.util.Arrays;
public class Merge {
private static void merge(Comparable[] a,Comparable[] aux,int lo,int mid,int hi){
assert Riqi.isSorted(a,lo,mid); //precondition: a[lo...mid] sorted
assert Riqi.isSorted(a,mid+1,hi); //precondition: a[mid+1...hi] sorted
int i = lo, j = mid+1;
//将a数组归并到aux数组
for (int m = lo; m <= hi ; m++) {
if (i > mid) aux[m] = a[j++];//如果i走到边界,说明左边子数组已经全部移上去了,就把j移上去
else if (j > hi) aux[m] = a[i++];//如果j走到边界,说明右边子数组已经全部移上去了,就把i移上去
else if (Riqi.less(a[j],a[i])) aux[m] = a[j++];//将j移到原数组,然后j自增
else aux[m] = a[i++];//将i移到原数组,然后i自增
}
assert Riqi.isSorted(a,lo,hi); //precondition: a[lo...hi] sorted
}
private static final int CUTOFF = 7; //设置判断是否使用插入排序的初始值
//不要将辅助数组在这里创建,会多出许多额外的小数组的花费
private static void sort(Comparable[] a,Comparable[] aux,int lo,int hi){
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(aux,a,lo,mid); //左半边排序
sort(aux,a,mid+1,hi);//右半边排序
merge(a,aux,lo,mid,hi); //归并 转换a和aux的角色
}
private static void sort(Comparable[] a){
Comparable[] aux = a.clone();
sort(a,aux,0,a.length-1);
}
public static void main(String[] args) {
Comparable[] a = {32,23,1,134,5,23,856,54,90};
sort(a);
System.out.println(Arrays.toString(a)); // [1, 5, 23, 23, 32, 54, 90, 134, 856]
}
}
2 自底向上的归并排序 bottom-up mergesort
- 能够遍历整个序列且不需要递归
package Chapter02;
import javax.swing.*;
public class MergeBU {
//归并所需的辅助数组
private static Comparable[] aux;
private static void merge(Comparable[] a,int lo,int mid,int hi){
assert Riqi.isSorted(a,lo,mid); //precondition: a[lo...mid] sorted
assert Riqi.isSorted(a,mid+1,hi); //precondition: a[mid+1...hi] sorted
//copy
for (int k = lo; k <= hi; k++) {
aux[k] = a[k];
}
//merge
int i = lo, j = mid+1;
for (int m = lo; m <= hi ; m++) {
if (i > mid) a[m] = aux[j++];//如果i走到边界,说明左边子数组已经全部移上去了,就把j移上去
else if (j > hi) a[m] = aux[i++];//如果j走到边界,说明右边子数组已经全部移上去了,就把i移上去
else if (Riqi.less(aux[j],aux[i])) a[m] = aux[j++];//将j移到原数组,然后j自增
else a[m] = aux[i++];//将i移到原数组,然后i自增
}
assert Riqi.isSorted(a,lo,hi); //precondition: a[lo...hi] sorted
}
public static void sort(Comparable[] a){
//进行lgN次两两归并
int N = a.length;
Comparable[] aux = new Comparable[N];
for (int sz = 1; sz < N; sz = sz+sz) {//外循环:子数组长度每轮翻倍
//内循环:每个子数组归并
for (int lo = 0; lo < N-sz; lo += sz+sz) { //sz为子数组大小 lo = lo+sz+sz 跳到下一个子数组起始位置
merge(a,lo,lo+sz-1,Math.min(lo+sz+sz-1,N-1)); //最末子数组也许长度不够sz,长度不够则选数组最后一个元素
}
}
}
}
- 时间复杂度为logN,而每一轮需要进行N次比较,因此总复杂度为NlogN
3 排序算法的复杂度 sorting complexity
- lower bound就是要找一个最小的比较次数;optimal algorithm是lower bound与upper bound相等时
- 全排列这棵树至少有N! 个叶子结点,因为N个不同的主键(元素)会有N! 种不同的排列
- 二叉树的组合学性质是高度为h的树最多只可能有2h个叶子结点,此时该树为完全树(每一层每个点都有两个叉)
- 因此树的高度大于等于log2(N!),根据Stirling公式,正比于NlgN,即排序算法复杂度的下限
- 归并排序时间复杂度最优,但空间复杂度不是最优
- 以上证明都基于所有元素不同且完全打乱的前提
4 比较器 comparators
- 比较器接口:对同一数据的不同排序
package Chapter02;
import java.util.Comparator;
public class Student {
//原码见 algs4/Selection
public static void sort(Object[] a, Comparator comparator){
int N = a.length;
for (int i = 0; i < N; i++) {
for (int j = i; j >0 && Riqi.less(comparator,a[j],a[j-1]); j--) {
Selection.exch(a,j,j-1);
}
}
}
//按照不同的键对同一组数据进行排序
public static final Comparator<Student> BY_NAME = new ByName();
public static final Comparator<Student> BY_SECTION = new BySection();
private final String name;//这里的对象要赋值,否则带final报错
private final int section;
private static class ByName implements Comparator<Student>{
public int compare(Student v,Student w){
return v.name.compareTo(w.name);
}
}
private static class BySection implements Comparator<Student>{
public int compare(Student v,Student w){
return v.section - w.section; //这里不会溢出
}
}
}
- 根据极坐标排序
- 用几何方法非常复杂,可以用counterclockwise
- 源码见algs4 --> Point2D
5 稳定性 stability
- 不是所有排序都能在按section排列的情况下,name仍然按顺序排,这就是stability
- A stable sort preserves the relative order of items with equal keys (一个稳定的排列是指拥有相同关键字的记录保留其相对顺序)
- 插入排序和归并排序是稳定的,选择排序和希尔排序不是
- 插入排序是稳定的,因为拥有相同值的两个元素不会交换,即不会越过对方
- 长距离交换使得相同值被越过
- 归并排序遇到相同值总取左边的,不会打乱顺序