算法基础:快速排序

快速排序及其思想的运用

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

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

  • 1分:
    第一步,分解。我们用递归的形式将原数组划分成两部分,即 A [ p . . r ] → A [ p . . q − 1 ]   a n d   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 ]   a n d   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;
}

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

详见算法导论第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 ) + c n = T ( n − 1 ) + c n T(n) = T(n - 1) + T(0) + cn = T(n - 1) + cn T(n)=T(n1)+T(0)+cn=T(n1)+cn
迭加,可知 T ( n ) = Θ ( n 2 ) T(n) = \Theta(n^2) T(n)=Θ(n2)

2.2 最佳情况分析
如果,我们每次划分都能将尽量平衡,即两个子数组的长度都不大于 n / 2 n/2 n/2的话,此时算法运行时间的递归式为
T ( n ) = 2 T ( n / 2 ) + c n = 2 ( T ( n / 4 ) + c n / 2 ) + c n = 2 T ( 0 ) + c n lg ⁡ 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
因此最佳情况下的时间复杂度为 Θ ( n lg ⁡ n ) \Theta(n\lg n) Θ(nlgn)

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

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

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

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

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

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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值