第二章 排序(二) - 《算法》读书笔记

第二章 排序(二)

2.2 归并排序

2.2.1 原地归并的抽象方法

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;
    for(int k = lo; k <= hi; k++)//先将所有元素复制到aux[]中,再归并到a[]中
        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 aux[i++];//右半边当前元素大于等于左半边当前元素
    }
}

2.2.2 自顶向下的归并排序

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){
        //将数组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); //归并两个子数组
    }
}
  • 归并排序所需时间与NlgN成正比
  • 辅助数组所使用的额外空间与N成正比

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 class MergeBU{
    private static Comparable[] aux;
    public static void sort(Comparable[] a){
        int N = a.length;
        aux = new Comparable[N];
        for(int sz = 1; sz < N; sz = 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));
    }
}
  • 当数组长度为2的幂时,自顶向下和自底向上的归并排序所用的比较次数和数字访问次数正好相同,只是顺序不同。其他时候,两种方法的比较和数组访问的次序会有所不同。
  • 自底向上的归并排序适合用链表组织的数据,只需重新组织链表链接,就能将链表原地排序。

2.2.4 排序算法的复杂度

  • 没有任何基于比较的算法能够保证使用小于~NlogN次比较将长度为N的数组排序
  • 归并排序是一种渐进最优的基于比较排序的算法

2.3 快速排序

2.3.1 基本算法

  • 与归并排序不同,快速排序的递归调用发生在处理整个数字之后,两个子数组有序时,整个数组自然有序。

  • 在快速排序中,切分(partition)的位置取决于数组的内容

  • 算法如下:

public class Quick{
    public static void sort(Comparable[] a){
        StdRandom.shuffle(a); //消除对输入的依赖
        sort(a, 0, a.length - 1);
    }
    private static void sort(Comparable[] a, int lo, int hi){
		if(hi <= lo) return;
        int j = partition(a, lo, hi); //切分
        sort(a, lo, j-1); //将左半部分a[lo..j-1]排序
        sort(a, j+1, hi); //将右半部分a[j+1..hi]排序
    }
}
  • 切分使得数组满足下面三个条件:
    • 对于某个j,a[j]以及排定
    • a[lo]到a[j-1]中的所有元素都不大于a[j]
    • a[j+1]到a[hi]中的所有元素都不小于a[i]
  • 切分方法:
    • 随意地取a[lo]作为切分元素
    • 从数组左端开始向右扫描,直到找到一个大于等于它的元素
    • 从数组右端开始向左扫描,直到找到一个小于等于它的元素
    • 交换它们的位置,如此继续
    • 当两个指针相遇时,我们只需要将切分元素a[lo]和左子数组最右侧的元素交换即可
  • 切分实现如下:
private static int partition(Comparable[] a, int lo, int hi){
    //将数组切分为a[lo..i-1],a[i],a[i+1..hi]
    int i = lo, j = hi + 1; //左右扫描指针
    Comparable v = a[lo]; //切分元素
    while(true){ //扫描左右,检查扫描是否结束并交换元素
        while(less(a[++i], v)) if(i == hi) break;
        while(less(v, a[--j])) if(j == lo) break;
        if(i >= j) break;
        exch(a, i, j);
    }
    exch(a, lo, j); //将v = a[j]放入正确的位置
    return j;
}

2.3.1.1 原地切分

2.3.1.2 别越界

2.3.1.3 保持随机性

2.3.1.4 终止循环

2.3.1.5 处理切分元素值有重复的情况

  • 左侧扫描在遇到大于等于切分元素值的元素时停下
  • 右侧扫描在遇到小于等于切分元素值的元素时停下
  • 在某些典型应用中,可以避免算法的运行时间变为平方级别

2.3.1.6 终止递归

2.3.2 性能特点

  • 将长度为N的无重复数组排序,快速排序平均需要~2NlnN次比较。

  • 快速排序最多需要N2/2次比较,但随机打乱数组能够预防这种情况。

  • 总体来说,快速排序的运行时间在1.39NlgN的某个常数因子的范围之内

  • 相比归并排序,比较次数较多,但移动数据的次数更少,一般会更快

2.3.3 算法改进

2.3.3.1 切换到插入排序

  • 对于小数组,快速排序比插入排序慢
  • 因为递归,快速排序的sort()方法在小数组中也会调用自己
if(hi <= lo + M){ Insertion.sort(a, lo, hi); return; }
  • 转换参数M的最佳值是和系统相关的,但是5~15之间的任意值在大多数情况下都能令人满意

2.3.3.2 三取样切分

  • 使用子数组的一小部分元素的中位数来切分数组
  • 取样大小设为3并用大小居中的元素切分的效果最好
  • 还可以将取样元素放在数组末位,作为“哨兵”来去掉partition()中的数组边界测试

2.3.3.3 熵最优的排序

  • 在有大量重复元素的情况下,快速排序的递归性会使元素全部重复的子数组经常出现

  • 可以将数组切分为三部分,分别对应于小于、等于和大于切分元素的数组元素。

  • 三向切分的快速排序:

    • 从左到右遍历数组一次
    • 维护一个指针lt使得a[lo..lt-1]中的元素都小于v
    • 维护一个指针gt使得a[gt+1..hi]中的元素都大于v
    • 维护一个指针i使得a[lt..i-1]中的元素都等于v
    • a[i..gt]中的元素都还未确定
    • 处理以下情况:
      • a[i]小于v,将a[lt]和a[i]交换,将lt和i加一
      • a[i]大于v,将a[gt]和a[i]交换,将gt减一
      • a[i]等于v,将i加一
    • 这些操作保证数组元素不变且缩小gt-i的值
  • 三向切分的快速排序实现如下:

public class Quick3way{
    public static void sort(Comparable[] a, int lo, int hi){
        if(hi <= lo) return;
        int lt = lo, i = lo + 1, gt = hi;
        Comparable v = a[lo];
        while(i <= gt){
            int cmp = a[i].compareTo(v);
            if(cmp < 0) exch(a, lt++, i++);
            else if(cmp > 0) exch(a, i, gt--);
            else i++;
        }
        sort(a, lo, lt - 1);
        sort(a, gt + 1, hi);
    }
}
  • 对于只有若干不同主键的随机数组,归并排序的时间复杂度是线性对数的,而三向切分快速排序是线性的
  • 不存在任何基于比较的排序算法能够保证在NH-N次比较之内将N个元素排序,其中H为由主键值出现频率定义的香农信息量
  • 对于大小为N的数组,三向切分的快速排序需要~(2ln2)NH次比较
  • 三向切分是信息量最优的
posted @ 2021-01-24 23:20  一天到晚睡觉的鱼  阅读(122)  评论(0)    收藏  举报