news 2026/5/10 16:04:09

快速排序 - 原理、时空分析、优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
快速排序 - 原理、时空分析、优化

过程

快速排序分为三个过程:

  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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/28 8:56:49

Python设计模式:享元模式详解

享元模式的核心思想享元模式&#xff08;Flyweight Pattern&#xff09;通过共享对象减少内存占用&#xff0c;适用于存在大量重复对象的场景。其核心是将对象的内在状态&#xff08;可共享&#xff09;与外在状态&#xff08;不可共享&#xff09;分离&#xff0c;通过共享内在…

作者头像 李华
网站建设 2026/5/3 16:41:57

线性化注意力

原文&#xff1a;towardsdatascience.com/linearizing-attention-204d3b86cc1e?sourcecollection_archive---------3-----------------------#2024-12-26 打破二次方限制&#xff1a;softmax 注意力的现代替代方案 https://medium.com/shitanshu273?sourcepost_page---bylin…

作者头像 李华
网站建设 2026/5/6 3:52:54

LibGDX中的多边形绘制优化

在游戏开发中,绘制多边形是常见的任务之一。特别是当我们需要处理复杂的形状或大量的点时,性能和错误处理就显得尤为重要。本文将通过一个具体的实例,讨论如何在LibGDX中优化多边形的绘制,并避免常见的IndexOutOfBoundsException错误。 问题背景 当使用LibGDX的ShapeRend…

作者头像 李华
网站建设 2026/4/25 11:07:06

解决Gradle中NPM命令失效问题

在使用IntelliJ IDEA进行项目开发时,尤其是在处理前端资产(assets)构建的任务中,我们可能会遇到一些奇异的问题。今天我们来讨论一个常见但不易解决的错误:在Gradle脚本中调用npm命令时失败,报错信息为“Cannot run program ‘npm’… No such file or directory”。 问…

作者头像 李华
网站建设 2026/5/1 8:35:58

深入探讨Clang-Tidy与Bazel的整合

在现代软件开发中,代码质量和可维护性是至关重要的。Clang-Tidy作为一个强大的静态分析工具,可以帮助开发者发现并修复代码中的潜在问题。然而,当Clang-Tidy与构建工具Bazel结合使用时,可能会遇到一些有趣的挑战。本文将通过一个实例,探讨如何正确配置和使用Clang-Tidy来分…

作者头像 李华