《算法》2.3快速排序

1快速排序

1.1快速和归并的比较

归并排序快速排序
排序方式将数组分成两个子数组分别排序,并将两个有序的子数组归并以将整个数组排序当两个子数组都有序时整个数组就自然有序了
递归调用的时机发生在处理整个数组之前发生在处理整个数组之后
数组切分方式一个数组被分为两半切分的位置取决于数组的内容

1.2快排基本算法

快速排序递归地将子数组a[lo..hi]排序,先用partition()方法将a[j]放到一个合适的位置,然后再用递归调用将其他位置的元素排序。

该方法的关键在于切分,这个过程使得数组满足下面三个条件:

  • 对于某个ja[j]已经排定
  • a[lo]a[j-1]中的所有元素都不大于a[j]
  • a[j+1]a[hi]中的所有元素都不小于a[j]

我们就是通过递归调用切分来排序的。

1.3实现

/**
 * 快速排序
 * @Author: AZhu
 * @Date: 2021/2/22 19:30
 */
public class Quick extends Sorting {

    /**
     * 对数组a进行快速排序
     * @param a
     */
    public static void sort(Comparable[] a) {
        StdRandom.shuffle(a);//消除对输入的依赖
        sort(a, 0, a.length - 1);
    }

    /**
     * 对a[lo..hi]进行排序
     * @param a
     * @param lo
     * @param hi
     */
    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]排序
    }

    /**
     * 将数组a[lo..hi]切分为a[lo..i-1] a[i] a[i+1..hi]
     * @param a
     * @param lo
     * @param hi
     * @return
     */
    private static int partition(Comparable[] a, int lo, int 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;//a[lo..j-1] <= a[j] <=a[j+1..hi]达成
    }

}

1.4分析

注意事项:

  • 原地切分

    辅助数组可以很容易实现切分,但是切分后的数组复制回去的开销会使我们得不偿失。

  • 别越界

    如果切分元素是数组中最小或最大的元素,我们就需要小心别让扫描指针跑出数组的边界。

  • 保持随机性

    打乱数组元素的顺序对于预测算法的运行时间很重要,

    保持随机性的另一个方法是在partition()中随机选取一个切分元素

  • 终止循环

    要格外小心保证循环结束。

性能:

内循环十分简洁是快速排序的一个优点。

一个速度上的优势在于它的比较次数很少。

时间复杂度 ~ NlgN

  • 将长度为N的无重复数组排序,平均需要 ~ 2NlgN次比较(以及1/6的比较)。
  • 快速排序最多需要约N^2/2次比较,但是随机打乱数组能够预防这种情况。

1.5算法改进

优化后的快速排序一般来说能将性能提升20%~30%

  1. 切换到插入排序

    对于小数组,插入排序比快速排序慢

    改动:

    if(hi <= lo) return;
    

    替换成:

    if(hi <= lo + M) {
        Insertion.sort(a,lo,hi);
        return;
    }
    

    参数M的最佳值是和系统相关,但是5~15之间的任意值在大多数情况下都能令人满意。

  2. 三取样切分

    使用子数组的一小部分元素的中位数来切分数组。

    取样大小为3并用大小居中的元素切分的效果最好。

  3. 熵最优的排序

    一个元素全部重复的子数组就不需要继续排序了,但是我们的算法还会继续将它切分为更小的数组。

    一个简单的方法是:将数组切分为三部分,分别对应于小于、等于、大于切分元素的数组元素。

2三向切分的快速排序

1.1基本算法

我们使用Comparable接口(而非less()接口)对a[i]进行三向比较来直接处理以下情况:

  • a[i]小于v,将a[lt]a[i]交换,将lti加一;
  • a[i]大于v,将a[gt]a[i]交换,将gt减一;
  • a[i]等于v,将i减一;

1.2实现

/**
 * 三向切分的快速排序
 * @Author: AZhu
 * @Date: 2021/2/22 19:54
 */
public class Quick3way extends Sorting {

    /**
     * 对数组a进行三向切分快速排序
     * @param a
     */
    public static void sort(Comparable[] a) {
        StdRandom.shuffle(a);//消除对输入的依赖
        sort(a, 0, a.length - 1);
    }

    /**
     * 对a[lo..hi]进行排序
     * @param a
     * @param lo
     * @param hi
     */
    private 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) {
            //实现:a[lo..lt-1] < v=a[lt..gt] < a[gt+1..hi]
            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);
    }

}

1.3分析

对于存在大量的重复元素的数组,这种方法比标准的快速排序的效率高得多。

对于只有若干不同主键的随机数组:

  • 归并排序的时间复杂度是线性对数
  • 三向切分快速排序的时间复杂度是线性

这种对重复元素的适应性使得三向切分的快速排序成为排序库函数的最佳算法选择。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值