快速排序及其思想的运用
序言:
快速排序采用了分治的思想,是对冒泡的改进,它的期望复杂度是Θ(nlgn)\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..q−1] 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..q−1] 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及相关细致描述
完成第一步后,第二步便是一个简单的递归过程。
- 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(n−1)+T(0)+cn=T(n−1)+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)+cnlgn
\begin{aligned}
T(n) &= 2T(n/2) + cn\\
&=2(T(n / 4) + cn/ 2) + cn\\
&=2T(0) + cn\lg n
\end{aligned}
T(n)=2T(n/2)+cn=2(T(n/4)+cn/2)+cn=2T(0)+cnlgn
因此最佳情况下的时间复杂度为Θ(nlgn)\Theta(n\lg n)Θ(nlgn)。
2.3 平均情况分析
平均情况下其实与最佳情况类似,只要我们的子数组长度是倍缩的(只有在最坏情况下,子数组长度是递减而不是倍缩),递归深度定是Θ(lgn)\Theta(\lg n)Θ(lgn),最终的时间复杂度总是O(nlgn)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函数,
- 如果,返回的主元下标 iii 满足 i+1=ki + 1 = ki+1=k (下标为0的是第1小的数,因此要+1),则 arr[i]arr[i]arr[i] 便是所求。
- 如果,返回的主元下标 iii 满足 i+1<ki + 1 < ki+1<k ,说明要找的数在主元的右边,因此对右半部分进行递归,左边不用继续处理。
- 最后一种,自然是对左半部分进行递归,右边不用继续处理。
该算法的复杂度为O(n)O(n)O(n),而不是O(nlgn)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) &= \sout{2}T(n/2) + cn\\
&=\sout{2}(T(n / 4) + cn/ 2) + cn\\
&=\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<2\sum_{k=0}^{+\infty}{\frac{1}{2^k}} < 2∑k=0+∞2k1<2, 因此 T(n)=O(n)T(n) = O(n)T(n)=O(n).
本文深入讲解了快速排序的原理和实现,包括分治思想、关键步骤解析、C++代码实现,以及复杂度分析。同时,探讨了快速排序在数据处理和查找特定元素问题中的应用。
1694

被折叠的 条评论
为什么被折叠?



