常州市网站建设_网站建设公司_网站制作_seo优化
2026/1/8 1:59:30 网站建设 项目流程

过程

快速排序分为三个过程:

  1. 将数列根据划分值m mm划分为两部分;
  2. 递归到两个子序列中分别进行快速排序;
  3. 不用合并,因为此时数列已经完全有序。

具体来说,第一步要是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。对于最优情况,每一次选择的分界值都是序列的中位数。

快速排序归并排序虽然都是划分区间,但是归并排序直接就可以选取中间位置,但快速排序需要选择的是中间数值,中间位置你是可以直接确定的,而中间数值你是无法确定的。为了保证平均时间复杂度,一般是随机选择一个数m mm来当做两个子数列的分界。

其实,快速排序没有指定应如何具体实现第一步,不论是选择m mm的过程还是划分的过程,都有不止一种实现方法。

第三步中的序列已经分别有序且第一个序列中的数都小于第二个数,所以直接拼接起来就好了。

在实现时,只需维护小于等于m mm的边界l o w lowlow,大于m mm的部分自然就唯一确定了。

朴素快速排序

publicstaticvoidquickSort(intl,intr){if(l>=r)// 只有一个数,或范围不存在return;intx=arr[l];// 固定方式选取划分值intmid=partition(l,r,x);// mid 位置已经固定// 处理剩余区间quickSort(l,mid-1);quickSort(mid+1,r);}// 划分数组 ≤x 放左边,>x 放右边publicstaticintpartition(intl,intr,intx){intlow=l,xi=0;for(inti=l;i<=r;i++){if(arr[i]<=x){swap(low,i);if(arr[low]==x)xi=low;low++;}}swap(xi,low-1);// 确保 ≤x 区域的最后一个数字是 xreturnlow-1;}

这里再贴一个 c 风格的,双指针扫描写法的 partition,常见于数据结构教材中。

intpartition(intlow,inthigh,intx){while(low<high){while(low<high&&arr[high]>=x)high--;swap(low,high);while(low<high&&arr[low]<=x)low++;swap(low,high);}returnlow;}

时间复杂度

快速排序的最优时间复杂度和平均时间复杂度为O ( n log ⁡ n ) O(n\log n)O(nlogn),最坏时间复杂度为O ( n 2 ) O(n^2)O(n2)

对于最优情况,每一次选择的分界值都是序列的中位数,此时算法时间复杂度满足的递推式为T ( n ) = 2 T ( n 2 ) + Θ ( n ) T(n) = 2T\left( \frac{n}{2} \right)+Θ(n)T(n)=2T(2n)+Θ(n),由主定理,T ( n ) = Θ ( n log ⁡ n ) T(n)=Θ(n\log n)T(n)=Θ(nlogn)

对于最坏情况,每一次选择的分界值都是序列的最值,此时算法时间复杂度满足的递推式为T ( n ) = T ( n − 1 ) + Θ ( n ) T(n)=T(n-1)+Θ(n)T(n)=T(n1)+Θ(n),易得T ( n ) = Θ ( n 2 ) T(n)=Θ(n^2)T(n)=Θ(n2)

对于平均情况,每一次选择的分界值可以看作是等概率随机的,设划分值在k kk位置,那么:T ( n ) = 1 n ∑ k = 1 n ( T ( k − 1 ) + T ( n − k ) ) + n = 2 n ∑ k = 1 n T ( k ) + n T(n)=\frac{1}{n}\sum_{k=1}^n(T(k-1)+T(n-k))+n=\frac{2}{n}\sum_{k=1}^nT(k)+nT(n)=n1k=1n(T(k1)+T(nk))+n=n2k=1nT(k)+n
由数学归纳法可证明,其数量级为O ( n log ⁡ n ) O(n\log n)O(nlogn)

在实际中,几乎不可能达到最坏情况,而快速排序的内存访问遵循局部性原理(递归处理相邻子数组、原地分区,连续访问缓存行),所以多数情况下快速排序的表现大幅优于堆排序等其他复杂度为O ( n log ⁡ n ) O(n\log n)O(nlogn)的排序算法。

就空间复杂度而言,主要是递归造成的栈空间的使用,最好情况,递归树的深度为log ⁡ 2 n \log_{2}nlog2n,其空间复杂度也就为O ( log ⁡ n ) O(\log n)O(logn),最坏情况,需要进行n − 1 n-1n1递归调用,其空间复杂度为O ( n ) O(n)O(n),平均情况,空间复杂度也为O ( log ⁡ n ) O(\log n)O(logn)

优化

  • 当序列较短时,直接使用插入排序的效率更高;
  • 尾递归(迭代)可以缩减堆栈深度,提高整体性能。

我们还有两条主路径去优化:划分值m mm的选取划分的过程

划分值m mm

如果你用朴素方法中的固定方式选取划分值m mm,比如总选数列第一个或最后一个元素,毒瘤数据能够把朴素的快速排序卡成O ( n 2 ) O(n^2)O(n2),比如升序或降序数列。

  • 通过三数取中,即选取第一个、最后一个以及中间的元素中的中位数。

    • 对于非常大的待排序的数列,也有九数取中,不再详述。
  • 通过随机选取,即随机选一个[ l , r ] [l,r][l,r]上的下标对应的值。虽然随机的常数时间比较大,但只有这一下随机,才能在概率上把快速排序的时间复杂度收敛到O ( n log ⁡ n ) O(n\log n)O(nlogn)

划分过程

朴素方法中,每次划分的过程都只能确定一个位置的值,即p a r t i t i o n partitionpartition返回的位置。

三路快速排序

是快速排序和基数排序的混合。思想基于 荷兰国旗问题。

每次划分过程,将待排数列划分为三个部分:小于m mm、等于m mm以及大于m mm。在处理含有多个重复值的数组时,效率远高于朴素快速排序。其最佳时间复杂度为O ( n ) O(n)O(n)

在实现时,只需维护小于m mm的边界,和大于m mm的边界,等于的部分自然就唯一确定了。

荷兰国旗优化版随机快速排序

publicstaticintfirst,last;publicstaticvoidquickSort(intl,intr){if(l>=r)return;// [l, r+1) -> l + [0, r-l+1)intx=arr[l+(int)(Math.random()*(r-l+1))];partition(l,r,x);intleft=first;intright=last;// 记录一下 lastquickSort(l,left-1);// 经过左半边快排,last 可能被更改quickSort(right+1,r);}publicstaticvoidpartition(intl,intr,intx){first=l;last=r;inti=l;while(l<=last){if(arr[i]==x)i++;elseif(arr[i]<x)swap(first++,i++);elseswap(last--,i);}}
内省排序

是快速排序和堆排序的结合。保证了最差时间复杂度为O ( n log ⁡ n ) O(n\log n)O(nlogn)

内省排序将快速排序的最大递归深度限制为⌊ log ⁡ n ⌋ \lfloor \log n \rfloorlogn,超过限制时就转换为堆排序。这样既保留了快速排序内存访问的局部性,又可以防止快速排序在某些情况下性能退化为O ( n 2 ) O(n^2)O(n2)

SGI C++ STL 的stl_algo.hsort()函数的实现采用了内省排序算法。

习题

洛谷 P1177【模板】快速排序 模板
力扣 912. 排序数组 模板

#atom

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询