【Algorithms公开课学习笔记6】 排序算法part3——快速排序

Quick Sort 快速排序

0.前言

前面的文章分析了归并排序这个性能极高的排序算法,本文将继续分析另一个性能相当的排序算法——快速排序。快速排序还被誉为20世纪十大最佳算法之一。在Java编程中Arrays.sort(o),如果括号里的o是java基本类型,那么该API就会调用快速排序算法。

1.快速排序

原理

快速排序运用了递归的思想:


input:数组s

sort(数组s):
    1.调用partition()将数组分成两部分
    2.对左右两部分(递归地)调用sort()

partition():将数组以j-位置为界限分成两部分,j左边全部小于j-值,j右边全部大于j-

代码实现


public class Quick{
    //将数组分成两部分
    private static int partition(Comparable[] a, int lo, int hi){
        int i = lo, j = hi+1;
        while (true){
            while (less(a[++i], a[lo]))
                if (i == hi) break;
            while (less(a[lo], a[--j]))
                if (j == lo) break;
            if (i >= j) break;
            exch(a, i, j);
        }
        exch(a, lo, j);
        return j;
    }
    //外部排序方法
    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);
        sort(a, j+1, hi);
    }
}


注意事项:

  • 算法并没有使用到辅助数组,全部都在原数组上操作的。
  • 在此算法中,j == lo是多余的判断,但i == hi确实必须的(考虑数组最大值在lo位置的情况)。
  • 将数组打乱是非常关键的操作,因为有序的数组(无论是正序还是逆序)都是影响此算法的性能。当然,shuffle数组也要保证其性能。
  • 如果待排序数组存在很多相同的值也会极大地影响此算法的性能,这一点将在下面详细分析。

时间性能

最好的情况best case:~NlgN
最坏的情况worst case:~1/2N^2
平均average case:~2NlgN,下图详细地分析了期数学过程。
(以上都是比较compare操作)


然而,有两种情况会极大地影响排序算法的性能:1)待排序数组已经有序,无论是正序还是逆序(所以排序前要打乱数组);2)待排序数组汇中存在很多相同的值。具体将在下文进行分析。

空间占用率

快速排序是在原数组上(in place)进行操作的,所以额外空间占用是常数级的。

稳定性

快速排序不是稳定的算法,因为值之间的交换跨度非常大。

优化

优化一:当数组被分割到很小的时候(临界值),改用插入排序算法来对其进行排序。这个临界值可以根据实际情况来确定。


private static void sort(Comparable[] a, int lo, int hi){
    //CUTOFF就是临界值
    if (hi <= lo + CUTOFF - 1){
        Insertion.sort(a, lo, hi);
        return;
    }
    int j = partition(a, lo, hi);
    sort(a, lo, j-1);
    sort(a, j+1, hi);
}

优化二:取中位数(非严格)进行patition的比较参考,即lo位置的应该是中位数。

private static void sort(Comparable[] a, int lo, int hi){
    if (hi <= lo) return;
    //这是简单地取lo、mid、hi位置三个数中的中位数
    int m = medianOf3(a, lo, lo + (hi - lo)/2, hi);
    swap(a, lo, m);
    int j = partition(a, lo, hi);
    sort(a, lo, j-1);
    sort(a, j+1, hi);
}

对于稍大的数组,我们也可以使用Tukey’s ninther方法来取中位数。Tukey’s ninther的原理:


1.按照间隔,从数组中取出9个值
2.3个值为一组,调用medianOf3()取中位数,取得3个中位数
3.再将3个中位数组成一组,再调用medianOf3()取中位数,得到结果

使用Tukey’s ninther的好处就是避免在排序开始前需要打乱(shuffle)数组,同时效率更高。

2.快速选择

在实际应用中,我们经常需要在一个N长的数组中取其第K大的值,此时,快速排序将是效率非常高的算法。

原理


1.与快速排序一样,使用patition()将数组a分成以j位置为界的两部分,左边的值全部小于a[j],右边全部大于a[j]2.如果j==k,那么就求得第K大的值;如果j<k,说明第k大的值在右边;如果j>k,说明第k大的值在左边。
3.根据第2步判断k的位置,在对应位置patition()4.loop:从第一步开始。

代码实现


public static Comparable select(Comparable[] a, int k){
    //同理,需要打乱数组,否则影响算法性能(实际是patition的性能)
    StdRandom.shuffle(a);
    int lo = 0, hi = a.length - 1;
    while (hi > lo){
        int j = partition(a, lo, hi);
        if (j < k) lo = j + 1;
        else if (j > k) hi = j - 1;
        else return a[k];
    }
    return a[k];
}

时间性能

在有序的情况下会出现最坏情况:~1/2N^2,因此需要打乱数组来保证时间性能
平均消耗了线性(linear)的时间:~N
分析:每一次都大概将数组对半分,所以存在N + N/2 + N/4 + … + 1 ~ 2N 比较(compares)。

空间占用率

快速选择是在原数组上(in place)进行操作的,所以额外空间占用是常数级的。

3. 3-way partitioning三分法

上文提到,如果数组中存在很多相等的值,将会影响快速排序的性能(但对归并排序并没有很大影响)。如果扫描过程中遇到相等的值时不停止扫描,将会消耗平方(quadratic)的时间。
所以,正确的做法是在扫描过程中遇到相等的值的时候就停止扫描。

存在一种更优的方法:如果在分区的时候,能够将相等的值分在同一个区,将会更好地优化性能问题,因此有人提出了3-way partition方法。

原理


1.用lo,lt,gt,hi将数组分成三部分,
2.lo-lt部分是小于参考值v的,lt-gt部分是等于v的,gt-hi是大于v的

代码实现


private static void sort(Comparable[] a, int lo, int hi){
    if (hi <= lo) return;
    int lt = lo, gt = hi;
    Comparable v = a[lo];
    int i = 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);
}

时间性能

实际上,在最坏的情况下,至少需要如下图所示的比较次数。
当所有数组所有值都相异时,取NlgN(与快速排序相同);
当数组值存在部分等值时,去N(linear线性)。

空间占有率

三分快速排序是在原数组上(in place)进行操作的,所以额外空间占用是常数级的。

4.应用

java

在java中由一个常用的排序API:Arrays.sort(a)。如果a是基本数据类型,则使用快速排序(plus插入排序);如果a是类对象,则使用归并排序(plus插入排序)。
这么做的原因在于:如果程序使用类对象,说明空间性能并不是很重要,只需要关注时间性能,所以使用归并排序保证了排序的时间性能(不管什么情况都是NlgN);如果程序使用了基本数据类型,说明对空间和时间性能均有要求,所以使用快速排序(额外空间占用率是常数级)。

指标

  • 稳定性 stability
  • 并行 parallel
  • 确定性 Deterministic
  • 异值/同值 Keys all distinct
  • 链表/数组 Linked list or arrays
  • 多种类型的值 Multiple key types
  • 大数 Large or small items
  • 是否有序 Is your array randomly ordered
  • 是否有性能保证 Need guaranteed performance

总结

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值