首页/数据结构/07-sorting/快速排序的优化 🔗 在 Obsidian 中打开
数据结构 · 07-sorting

快速排序的优化

重要度 ★★ 数据结构/排序
速查
快排平均 $O(n\log n)$,最坏(已有序 + 首元素作枢轴)退化为 $O(n^2)$。三大优化:三数取中随机化枢轴小数组切换插入排序

速查表

项目
主题快速排序的优化
核心概念快速排序的平均时间复杂度为 $O(n\log n)$,但在最坏情况下(如数组已有序)会退化为 $O(n^2)$。常见优化策略有三种。
时间复杂度$O(n\log n)$,最坏 $O(n^2)$
难度⭐⭐⭐

核心概念

快速排序的平均时间复杂度为 $O(n\log n)$,但在最坏情况下(如数组已有序)会退化为 $O(n^2)$。退化的根源在于枢轴选得太偏——每次划分只能剥掉一个元素,递归树退化成一条链。常见优化策略有三种。

快速排序的两类瓶颈 → 三种优化 枢轴选取不均 已有序 → $O(n^2)$ 三数取中 / 随机化 递归调用开销 小子表函数栈过重 切换插入排序 组合使用 三者互补 工程标准做法
图:优化不改变快排的分治骨架,只解决"枢轴偏"与"递归重"两个痛点。

1. 三数取中法(Median-of-Three)

思想:选取数组首、中、末三个元素,取其中位数作为枢轴(pivot),避免极端数据导致的最坏情况。

做法

  • lowhigh、$mid = (low+high)/2$
  • 比较 a[low]a[mid]a[high],将中位数交换到 a[low] 作为 pivot
  • 然后正常执行 partition

效果:对已有序或逆序数组,pivot 接近中位值,避免 $O(n^2)$ 退化。

为什么有效已有序数组中,首元素必是最小值,是最糟的枢轴;而"首/中/末三者的中位数"至少不会是全局极值,划分不会完全失衡。

2. 随机化法(Randomized Pivot)

思想:随机选取一个元素作为枢轴,从概率上避免最坏情况。

做法

  • [low, high] 范围内随机生成一个下标 randIdx
  • a[randIdx]a[low] 交换
  • 然后正常执行 partition

效果:期望时间复杂度仍为 $O(n\log n)$,最坏情况概率极低。

关键区别随机化让"最坏输入"不再由数据决定,而由随机数决定——攻击者无法构造必然退化的输入,但理论最坏仍是 $O(n^2)$。

3. 小数组切换插入排序(Cutoff to Insertion Sort)

思想:当子数组规模较小时,递归开销反而大于排序本身,此时切换为插入排序更高效。

做法

  • 设定阈值 THRESHOLD(通常 10~20)
  • high - low + 1 <= THRESHOLD 时,调用插入排序
  • 否则继续递归快排

效果:减少递归深度和函数调用开销,实际运行速度提升明显。

底层原因小规模数据上,插入排序常数因子极小且近乎有序时接近 $O(n)$;而快排每层都要付出函数调用、栈帧、参数传递的固定成本。

三者对比

优化方法解决的问题实现难度效果
三数取中避免已有序退化简单★★★
随机化避免特定输入退化简单★★★
小数组切换减少递归开销简单★★☆
考点提示前两种优化改善的是渐进复杂度的稳健性(避免 $O(n^2)$),第三种优化改善的只是常数因子,不改变渐进阶。

手算示例

例 1:三数取中选取 pivot

数组:[10, 3, 15, 7, 8, 23, 9]

  • $low=0$,$high=6$,$mid=3$
  • a[low]=10a[mid]=7a[high]=9
  • 中位数为 9($7 < 9 < 10$)
  • a[high]=9a[low]=10 交换 → [9, 3, 15, 7, 8, 23, 10]
  • 以 9 为 pivot 进行 partition

partition 过程(Lomuto 方案):

i = low = 0, pivot = 9
j=1: a[1]=3  < 9  → i=1, swap a[1],a[1]  → 不变
j=2: a[2]=15 > 9  → 跳过
j=3: a[3]=7  < 9  → i=2, swap a[2],a[3]  → [9,3,7,15,8,23,10]
j=4: a[4]=8  < 9  → i=3, swap a[3],a[4]  → [9,3,7,8,15,23,10]
j=5: a[5]=23 > 9  → 跳过
j=6: a[6]=10 > 9  → 跳过
最后 swap a[low],a[i]  → [8,3,7,9,15,23,10]

pivot 最终位置 $i=3$,左右两侧各 3 个元素,划分完全均衡。

例 2:小数组切换

数组(子问题):[4, 2, 7, 1, 3],阈值 = 10

  • 此子数组长度为 $5 \leq 10$,直接调用插入排序
  • 第 1 趟:[2, 4, 7, 1, 3](插入 2)
  • 第 2 趟:[2, 4, 7, 1, 3](7 已有序)
  • 第 3 趟:[1, 2, 4, 7, 3](插入 1 到最前)
  • 第 4 趟:[1, 2, 3, 4, 7](插入 3)
  • 排序完成,无递归调用

记忆卡片

快排最坏情况与复杂度?
每次 pivot 都取到极值(如已有序取首元素),每趟只减少一个元素,递归深度 $n$,比较次数 $n+(n-1)+\cdots+1$,即 $O(n^2)$。
三数取中为什么能优化?
已有序/逆序时首元素极可能是极值。取首、中、末三者的中位数,保证 pivot 接近中位值,划分更均衡。
随机化快排的期望复杂度?
期望 $O(n\log n)$;最坏出现概率极低(约 $1/n!$ 量级),但理论最坏仍是 $O(n^2)$。
切换插入排序的阈值多少?
通常 10~20。小子表上递归调用开销大于插入排序的比较+移动开销。
三种优化能同时用吗?
能,且工程中常组合。前两者解决枢轴选取,后者解决递归开销,互补,组合效果最佳。

交互动画 · 枢轴选取策略对比

待划分子表 本趟划分结果(左 ≤ pivot / 右 > pivot) 左子表 0 右子表 0
选择一种枢轴策略,观察划分是否均衡
点击上方按钮开始
示意图:橙色格为选中的枢轴,浅橙为候选元素;绿色/黄色条长度表示划分后左右子表的规模——两条越接近等长,递归树越矮、性能越好。

相关知识点

quick-sort merge-sort heap-sort

↑ 以上为站内 HTML 相对链接(纯网页可浏览);本页右上「在 Obsidian 中打开」跳回源笔记。