算法基础:快速排序

本文深入讲解了快速排序的原理和实现,包括分治思想、关键步骤解析、C++代码实现,以及复杂度分析。同时,探讨了快速排序在数据处理和查找特定元素问题中的应用。

快速排序及其思想的运用

序言:
快速排序采用了分治的思想,是对冒泡的改进,它的期望复杂度是Θ(nlg⁡n)\Theta(n\lg n)Θ(nlgn),而且其中隐含的常数因子非常小。本文将笔记其算法的核心思想及应用(参考《算法导论》第3版)。

1. 快速排序的描述
快排与归排同样,思想就是分治。即所谓,1分2解3合:

  • 1分:
    第一步,分解。我们用递归的形式将原数组划分成两部分,即A[p..r]→A[p..q−1] and A[q+1..r]A[p..r] \to A[p..q-1]\ {\rm and}\ A[q + 1..r]A[p..r]A[p..q1] and A[q+1..r],并且前一段元素均小于等于 A[q]A[q]A[q], 后一段反之;
  • 2解:
    第二步,解决。递归地调用快速排序,从而对子数组A[p..q−1] and A[q+1..r]A[p..q-1]\ {\rm and}\ A[q + 1..r]A[p..q1] and A[q+1..r] 进行原址排序;
  • 3合:
    第三步,合并。但是,由于快排的操作是原址的,因此不需要进行合并操作(归排则不同)。

其中,第一步是算法的关键。我们需要从子数组 A[p..r]A[p..r]A[p..r] 中选择一个数作为“主元”,并围绕它来划分子数组 A[p..r]A[p..r]A[p..r]。而我个人更习惯于称之为“隔板”。

  • C++实现
int partition(vector<int>& arr, int p, int r) {
    int x = arr[r]; //选择隔板,一般选择末尾元素
    int i = p; //i代表隔板左边的最大下标
    for (int j = p; j < r; ++j) {
        if (arr[j] <= arr[r]) swap(arr[j], arr[i++]);
        //若当前元素小于等于隔板, 作交换
    }
    swap(arr[i], arr[r]);
    return i;
}

解释:
首先,我们选定子数组的末尾元素作为“隔板”,并以 iii 标记“隔板”最终所在位置,初始化为 ppp
尔后,遍历开始,遍历下标为 jjj。如果当前遍历元素小于或等于“隔板”,说明找到了一个“隔板”左边的数字,那么我们需要交换 arr[i]arr[i]arr[i]arr[j]arr[j]arr[j],并且隔板下标 i=i+1i = i + 1i=i+1
最后,当遍历完成后, iii 指示的恰好是“隔板”应该处在的位置,因此需要最后一次交换,将“隔板”,即末尾元素交换到 iii 的位置。

详见算法导论第3版96页图7-1及相关细致描述

完成第一步后,第二步便是一个简单的递归过程。

  1. C++实现
void quickSort(vector<int>& arr, int p, int r) {
    if (p < r) {
        int q = partition(arr, p, r);
        quickSort(arr, p, q - 1);
        quickSort(arr, q + 1, r);
    }
}

2. 快速排序的复杂度分析

2.1 最差情况分析
如果,我们每次划分都极度不平衡,即有一个子数组为空的话,此时算法运行时间的递归式为
T(n)=T(n−1)+T(0)+cn=T(n−1)+cn T(n) = T(n - 1) + T(0) + cn = T(n - 1) + cn T(n)=T(n1)+T(0)+cn=T(n1)+cn
迭加,可知 T(n)=Θ(n2)T(n) = \Theta(n^2)T(n)=Θ(n2)

2.2 最佳情况分析
如果,我们每次划分都能将尽量平衡,即两个子数组的长度都不大于n/2n/2n/2的话,此时算法运行时间的递归式为
T(n)=2T(n/2)+cn=2(T(n/4)+cn/2)+cn=2T(0)+cnlg⁡n \begin{aligned} T(n) &amp;= 2T(n/2) + cn\\ &amp;=2(T(n / 4) + cn/ 2) + cn\\ &amp;=2T(0) + cn\lg n \end{aligned} T(n)=2T(n/2)+cn=2(T(n/4)+cn/2)+cn=2T(0)+cnlgn
因此最佳情况下的时间复杂度为Θ(nlg⁡n)\Theta(n\lg n)Θ(nlgn)

2.3 平均情况分析
平均情况下其实与最佳情况类似,只要我们的子数组长度是倍缩的(只有在最坏情况下,子数组长度是递减而不是倍缩),递归深度定是Θ(lg⁡n)\Theta(\lg n)Θ(lgn),最终的时间复杂度总是O(nlg⁡n)O(n\lg n)O(nlgn)

3. 快速排序思想的应用
快排的partition函数对于我们处理很多算法题时是有启发意义的,下面举例说明。

3.1 根据“某种”规则,将数组划分为二
例如,要求将数组以奇数在前,偶数在后的原则,将数组重排;又或者给定一个 targettargettarget,使得数组小于 targettargettarget 的在前,大于 targettargettarget 的在后。
对于这些类型的题目,我们都可以采用partition函数里的方法(快排的主元是内定的,但在实际应用中也可以是外定的),在O(n)O(n)O(n)的复杂度内解决问题。

3.2 找到数组中第 kkk 小(大) 的数
比如说,我们要找到数组中第 kkk 小的数。我们对数组执行一次partition函数,

  1. 如果,返回的主元下标 iii 满足 i+1=ki + 1 = ki+1=k (下标为0的是第1小的数,因此要+1),则 arr[i]arr[i]arr[i] 便是所求。
  2. 如果,返回的主元下标 iii 满足 i+1&lt;ki + 1 &lt; ki+1<k ,说明要找的数在主元的右边,因此对右半部分进行递归,左边不用继续处理。
  3. 最后一种,自然是对左半部分进行递归,右边不用继续处理。

该算法的复杂度为O(n)O(n)O(n),而不是O(nlg⁡n)O(n \lg n)O(nlgn),为何?因为其比标准的快排相比,只需处理一半的数据即可。方便起见,我们考虑最佳情况(一般情况同之)
T(n)=2T(n/2)+cn=2(T(n/4)+cn/2)+cn=2T(0)+c(n+n/2+n/4+...) \begin{aligned} T(n) &amp;= \sout{2}T(n/2) + cn\\ &amp;=\sout{2}(T(n / 4) + cn/ 2) + cn\\ &amp;=\sout{2}T(0) + c(n + n/2 + n/4 +...) \end{aligned} T(n)=2T(n/2)+cn=2(T(n/4)+cn/2)+cn=2T(0)+c(n+n/2+n/4+...)
注意到∑k=0+∞12k&lt;2\sum_{k=0}^{+\infty}{\frac{1}{2^k}} &lt; 2k=0+2k1<2, 因此 T(n)=O(n)T(n) = O(n)T(n)=O(n).

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值