《算法》系列——排序算法之快速排序算法(C++描述)

《算法》系列 知识整理(C++描述)

算法学习历程

  • 排序算法
  • 查找算法
  • 字符串问题
  • 智能算法

学习目录

  • 排序算法

    • 初级排序算法
    • 归并排序
    • 快速排序
    • 优先队列
    • 排序算法的应用

本文主要内容

本文主要针对快速排序算法的思想、实现、复杂度分析、算法改进几个方面展开。
各位看官可以根据需求,从任意一部分开始看起。

快速排序

算法思想

快速排序的基本思想为:将一个数组分成两个子数组 ,保证其中一个子数组的所有元素均小于另一个数组的最小元素。然后对两个数组独立地排序,当两个数组都有序时,整个数组便有序了。

我们在这里比较一下归并排序快速排序
1、归并排序将数组分成两个子数组,分别进行排序,再将有序的子数组归并得到有序的整个数组。
2、快速排序将数组以一种规定的方式切分为两个子数组,再分别进行排序,两个子数组均有序后,整个数组便有序。

我们看到:
1、两者均用到递归的思想,且“分别对两个数组进行排序 ”体现了递归的思想。
2、两个算法对于数组的操作,一个是“归并”,一个是“切分”,且前者发生在递归前,后者发生在递归后

接着来看快速排序的思想,在上面的比较中,我们知道了实现快速排序的关键点在于如何划分数组,在划分好数组后,只需要递归调用函数自身即可。

划分这一操作,使得数组有以下性质:
1、对于数组中的某个元素,其位置在划分完成后就定了下来;
2、对于该元素左侧的所有元素,均小于等于该元素;
3、对于该元素右侧的所有元素,均大于等于该元素。

也就是说,在进行一次这样的划分后,就会有一个元素的位置确定了下来,我们只需要递归调用这个函数,就可以完成所有元素的排序。

算法的实现

我们沿着刚刚提到的划分操作的性质,来逐步实现这样的划分操作。
第一条提到:对于数组中的某个元素,其位置在划分后就定了下来。
那么,我们要怎样来确定这个“某个元素”呢?

倘若我们直接去遍历一遍数组,想直接得到满足上述三条性质的元素,那是很难实现的。
那么我们不妨随机选取一个元素,将其作为将要被排定的元素,在经过对数组的一定操作后,使其满足条件。
那为了方便起见,也不妨直接选取数组首元素作为这个将要被排定的元素,我们记为a[lo]。

接下来只需要想办法,把小于a[lo]的元素都放在a[lo]的左侧,大于a[lo]的元素都放在a[lo]的右侧

现在来看如何实现:(非最终操作,只是作为引导开端,后面会进行一些优化)
1、设两个指针i、j,分别指向数组的两端a[lo]、a[hi]。
2、使指针i从左往右移动,寻找大于等于a[lo]的元素,当遇到第一个符合要求的元素时便停下来。
3、使指针j从右向左自动,寻找小于等于a[lo]的元素,当遇到第一个符合要求的元素时便停下来。
4、如果此时i在j的左侧,即数组还没有被扫描完毕,则将i、j处的元素交换,回到步骤2,继续扫描。
5、如果此时i与j已经相遇(i>=j),证明数组已经被扫描完毕。现在就要把a[lo]放到i与j相遇的地方来,那么我们是放到a[i]处还是a[j]处呢?
答:a[j]处。由于i处的元素是大于等于a[lo]的,当a[i]>a[lo],若我们交换a[i]和a[lo],那么就会有一个大于a[lo]的元素去了最左端,便不符合我们的目的了。而a[j]处是小于等于a[lo]的元素,交换后符合我们的目的。
将a[lo]与a[j]交换后,一轮划分完毕,此时a[j]处是已排定的元素,其左侧均小于它,右侧均大于它。

来看一下切分的轨迹,有助于理解:
记v=a[lo]=K
                a[ ]
            i    j   0   1   2   3   4   5   6   7   8   9   10   11   12   13   14   15
初始     0 16  K  R   A   T   E   L  E   P   U   I    M    Q    C    X     O    S
扫描     1 12  K  R i \frac{R}{i} iR  A   T   E   L  E   P   U   I    M    Q    C j \frac{C}{j} jC    X     O    S
交换     1 12  K  C i \frac{C}{i} iC  A   T   E   L  E   P   U   I    M    Q    R j \frac{R}{j} jR    X     O    S
扫描     3   9  K  C  A   T i \frac{T}{i } iT   E   L  E   P   U   I j \frac{I}{j} jI   M    Q    R     X     O    S
交换     3   9  K  C  A   I i \frac{I}{i} iI   E   L  E   P   U   T j \frac{T}{j} jT   M    Q    R    X     O    S
扫描     5   6  K  C  A   I   E   L i \frac{L}{i} iL   E j \frac{E}{j} jE   P   U   T    M    Q    R    X     O    S
交换     5   6  K  C  A   I   E   E i \frac{E}{i} iE   L j \frac{L}{j} jL   P   U   T    M    Q    R    X     O    S
扫描     6   5  K  C  A   I   E   E j \frac{E}{j} jE   L i \frac{L}{i} iL   P   U   T    M    Q    R    X     O    S
最后一次
交换     5   6  E  C  A   I   E   K j \frac{K}{j} jK   L i \frac{L}{i} iL   P   U   T    M    Q    R    X     O    S

不得不说 画轨迹图可真累。。。
代码先不贴,咱们先分析以下复杂度,然后对这个算法进行一番优化,最后再贴代码。

算法复杂度分析

来分析快速排序需要的比较次数。
老规矩,先说明结论,再分析:

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

记CN为将N个不同元素进行排序所需要的比较次数。
1、C0=C1=0;
2、当N>1时:
根据上面的分析,我们可以得治,快速排序分为三大部分:划分、左数组递归、右数组递归,计算比较次数时,分别计算后相加即可。
划分中的比较次数:在划分过程中,我们对数组遍历了一遍,遍历每个元素时,都需要和a[lo]进行一次比较,此时有N次比较,而当i与j相遇时,两个指针对同一个元素都进行了比较,因此总的比较次数为N+1
左、右数组递归:左右两个子数组的长度可能为0、1、2、……、N-1中的任何一个值(对应地,另一个数组为N-1、……、2、1、0),因此,我们考虑平均情况,左右两个数组地比较次数分别为:
(C0+C1+……+CN-2+CN-1)/N和(CN-1+CN-2+……+C0)/N
因此:
CN=N+1+(C0+C1+……+CN-2+CN-1)/N+(CN-1+CN-2+……+C0)/N
下面即是一些数学归纳:

等式两边同乘N,整理后得:
NCN=N(N+1)+2(C0+C1+……+CN-2+CN-1)……………………①
令N=N-1,得:
(N-1)CN-1=N(N-1)+2(C0+C1+……+CN-2)……………………②

①-②得:
NCN-(N-1)CN-1=2N+2CN-1

整理得:
NCN=(N+1)CN-1+2N

两边同除以N得:
CN= N + 1 N \frac{N+1}{N} NN+1CN-1+2
即得到了CN和CN-1之间得关系。

利用数学归纳法,可得:
CN= N + 1 N \frac{N+1}{N} NN+1CN-1+2
= N + 1 N \frac{N+1}{N} NN+1( N N − 1 \frac{N}{N-1} N1NCN-2+2)+2
= N + 1 N − 1 \frac{N+1}{N-1} N1N+1CN-2+2 N + 1 N \frac{N+1}{N} NN+1+2
=……
=2(N+1)( 1 3 \frac{1}{3} 31+ 1 4 \frac{1}{4} 41+……+ 1 N + 1 \frac{1}{N+1} N+11)
~2NlnN

这是我们讨论的平均情况,但在最坏得情况下,快速排序会回退到 N ² 2 \frac{N²}{2} 2N²次比较,但通过下面的一种改进方式——随机打乱数组,可以有效避免这种情况的出现。

算法改进

1、最简单直白的一点改进为:初始时,我们没有必要将a[i]与a[lo]进行比较,因此i可以从lo+1开始;
2、为了避免出现a[lo]的选择受所输入的数组的影响,可以考虑在输入数组后,将其随机打乱,再定a[lo],以便消除对输入的依赖。
3、当数组较小时,切换成插入排序;(当数组较小时,快速排序比插入排序要慢,因其在小数组中也要递归调用自己。)
4、三取样切分(对于有大量重复元素出现时,效率非常高)
我们设置三个指针,将整个数组划分为四部分(其中三部分是确定的,有一部分未确定,因此称为“三取样”),三个指针分别记:it、i、gt,使得:
  ①a[lo…it-1]均小于v
  ②a[it…i-1]均等于v
  ③a[i…gt]未确定
  ④a[gt+1…hi]均大于v

采取以下三个操作来实现上述的划分:
  ①若a[i]<v,交换a[it]和a[i],再it++,i++;
  ②若a[i]>v,交换a[gt]和a[i],再gt–;
  ③若a[i]==v,执行i++;

最后,我们贴上改进后的普通快速排序和三向切分快速排序的C++代码:

首先是普通快速排序:

void exch(int *a,int i,int j)
{
        int t=a[i];
        a[i]=a[j];
        a[j]=t;
}

int Partition(int *a,int low,int high)
{
        int i=low,j=high+1;
        int v=a[low];
        while(true)
        {
        //j=high+1、+=i和--j这样的设计避免了a[i]与a[lo]的冗余比较
            while(a[++i]<v);
            while(a[--j]>v);
            if(i>=j)
                break;
            exch(a,i,j);
        }
        exch(a,low,j);
        return j;
}
        
void Quick_sort(int *a,int low,int high)
{
        if(high<=low)
            return;
        int j=Partition(a,low,high);
        Quick_sort(a,low,j-1);
        Quick_sort(a,j+1,high);
}

接下来是三向切分快速排序的代码:

void exch(int *a,int i,int j)
{
        int t=a[i];
        a[i]=a[j];
        a[j]=t;
}

void Quick3way_sort(int *a,int low,int high)
{
        if(high<=low)
            return;
        int lt=low,i=low+1,gt=high;
        int v=a[low];
        while(i<=gt)
        {
            if(a[i]<v)
                exch(a,lt++,i++);
            else if(a[i]>v)
                exch(a,i,gt--);
            else i++;
        }
        Quick3way_sort(a,low,lt-1);
        Quick3way_sort(a,gt+1,high);
}

有关快速排序的内容到这里就结束了,初次整理,避免不了有错误,欢迎指正。:)
如果喜欢,还可以观看本系列其他文章呐~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值