相关笔记

概览

本节从直觉层面分析快速排序在不同划分平衡度下的性能表现,涵盖最坏情况、最好情况、平衡划分和平均情况四种场景,揭示快速排序为何在实践中表现优异。此外还分析了快速排序的栈空间需求。

要点列表:

  • 快速排序的运行时间完全取决于划分的平衡程度,而平衡程度取决于主元的选择
  • 最坏情况:每次划分极度不平衡(),递推 ,解为
  • 最好情况:每次划分完美平衡,递推 ,由主定理得
  • 平衡划分:即使每次 9:1 的”不平衡”划分,运行时间仍为 ;任意常数比例划分都是
  • 平均情况:好坏划分交替出现时,坏划分的代价被好划分”吸收”,运行时间仍为
  • 栈空间最坏情况为 ,最好情况为

知识结构总览

flowchart TD
    A["7.2 快速排序的性能"] --> B["最坏情况<br/>Θ(n²)"]
    A --> C["最好情况<br/>Θ(n lg n)"]
    A --> D["平衡划分<br/>O(n lg n)"]
    A --> E["平均情况直觉<br/>O(n lg n)"]
    A --> F["栈空间分析<br/>最坏 Θ(n)"]

    B --> B1["每次划分: 0 vs n-1"]
    B --> B2["递推: T(n) = T(n-1) + Θ(n)"]
    B --> B3["等差数列求和 → Θ(n²)"]
    B --> B4["触发条件: 已排序输入"]

    C --> C1["每次划分: ≤ n/2 vs ≤ n/2"]
    C --> C2["递推: T(n) = 2T(n/2) + Θ(n)"]
    C --> C3["主定理情况2 → Θ(n lg n)"]

    D --> D1["9:1 划分 → O(n lg n)"]
    D --> D2["99:1 划分 → O(n lg n)"]
    D --> D3["任意常数比例 → O(n lg n)"]
    D --> D4["比例仅影响常数因子"]

    E --> E1["好坏划分交替"]
    E --> E2["坏划分代价被好划分吸收"]
    E --> E3["80%概率 ≥ 9:1 平衡"]

    F --> F1["递归深度 = 栈空间"]
    F --> F2["最坏: Θ(n)"]
    F --> F3["最好: Θ(lg n)"]
    F --> F4["尾递归优化 → Θ(lg n)"]

核心思想

核心洞察

快速排序的性能完全取决于一个关键因素:每次 PARTITION 产生的划分有多平衡。这与归并排序(始终均匀划分)形成鲜明对比。本节通过递推关系式和递归树两种工具,逐步揭示一个反直觉的结论:即使划分看起来很不平衡(如 9:1),只要比例是常数,运行时间仍然是

最坏情况分析

最坏情况

当每次划分都产生一个大小为 和一个大小为 的子问题时,递推关系为:

其中 是对空子数组的递归调用开销。

【递推展开(等差数列求和)】

求解过程: 逐层展开递推关系:

最后一步使用了等差数列求和公式

最坏情况触发条件: 输入数组已经完全排序(正序或逆序),此时每次选取的主元 都是当前子数组的最大或最小元素,导致极度不平衡的划分。

讽刺之处: 当输入已排序时,插入排序只需 时间,而快速排序反而需要 时间——快速排序”最怕”的就是已排序输入。

最好情况分析

最好情况

当每次划分都尽可能均匀,两个子问题大小都不超过 时(一个为 ,另一个为 ):

【主定理求解(情况2:f(n)=Θ(n^log_b a))】

根据主定理(Master Theorem,定理 4.1)情况 2:,即

因此解为:

这与归并排序的运行时间渐近相同。

平衡划分分析

平衡划分

假设每次划分都产生 9:1 的比例划分(看似很不平衡):

【递归树分析(每层代价求和)】

递归树分析(图 7.4):

  • 根节点代价为 (PARTITION 的代价)
  • 下一层两个子问题代价之和为
  • 每一层的总代价都是 (因为子问题大小之和等于父问题大小)
  • 最短路径(沿 分支):深度为
  • 最长路径(沿 分支):深度为
  • 递归树总代价 = 每层代价 层数 =

推广到任意常数比例: 设划分比例为 ):

  • 递归树深度为 (因为每层子问题大小至少缩小为 倍,
  • 每层总代价为
  • 总代价为

关键结论: 划分比例只影响 记号中隐藏的常数因子,不影响渐近增长率。即使是 99:1 的划分,运行时间仍然是

平均情况的直觉

平均情况 (直觉性分析)

在随机输入上,PARTITION 产生好坏划分的混合。关键概率分析结果:

  • 80% 的概率产生至少 9:1 平衡的划分(习题 7.2-6:对任意 ,至少 平衡的概率约为 ;取
  • 20% 的概率产生不如 9:1 平衡的划分

【代价吸收论证(好坏划分交替分析)】

关键直觉(图 7.5): 一个”坏划分”( vs )后紧跟一个”好划分”( vs ),其组合效果为:

  • 两层划分的总代价:
  • 产生的三个子数组大小:
  • 这与单独一层好划分(产生两个大小为 的子数组,代价 )相差不超过一个常数因子

直觉上,坏划分的 代价被好划分的 代价”吸收”了。因此,好坏划分交替出现时,运行时间仍为 ,只是常数因子稍大。严格的期望运行时间分析将在 7.4.2 节给出。

栈空间分析

栈空间 (最坏情况)

虽然快速排序是原地排序(不需要额外的数组存储空间),但递归调用需要运行时栈空间:

  • 每次递归调用需要 的栈空间(保存返回地址、局部变量等)
  • ==栈空间 = 最大递归深度==
  • 最坏情况(每次极度不平衡):递归深度为 ,栈空间
  • 最好情况(每次完美平衡):递归深度为 ,栈空间

尾递归优化: 通过先递归处理较小的子数组,将较大的子数组通过尾调用处理,可以将最坏栈深度从 降至 。这是因为每次递归调用后,较大的子问题通过循环而非递归来处理。


补充理解与拓展

快速排序的工程优化——从理论到实践的五项关键技术

纯快速排序在实际工程中很少直接使用,现代标准库中的快速排序都经过多种优化:

  1. Median-of-Three 主元选择:取首、中、末三个元素的中位数作为 pivot,有效避免已排序数组的最坏情况。研究表明这可将比较次数减少约 5%(Sedgewick, 1978)

  2. 三路划分(Dutch National Flag):Dijkstra 于 1976 年提出,将数组分为 三部分。对含大量重复元素的数组,可将时间从 降至 。Java 的 Arrays.sort() 在检测到大量重复元素时会自动切换到三路划分

  3. 小数组切换插入排序:当子数组大小 时切换到插入排序。原因:对很小的数组,递归调用的固定开销(函数调用、栈帧创建)超过了插入排序 的劣势。实验表明此优化可减少约 15-20% 的比较次数(Sedgewick & Wayne, Algorithms, 4th Edition)

  4. 尾递归优化:先递归处理较小的子数组,较大的子数组通过尾调用(或循环)处理,将最坏栈深度从 降至 。CLRS 习题 7.4-2 要求读者实现此优化

  5. Introsort(内省排序):Musser 于 1997 年提出,结合了快速排序、堆排序和插入排序——先用快速排序,当递归深度超过 时切换到堆排序防止退化,对小分区切换到插入排序。C++ STL 的 std::sort 采用此策略,保证了最坏情况

来源:Musser, D.R., “Introspective Sorting and Selection Algorithms”, Software: Practice and Experience, 27(8):983-993, 1997; Sedgewick, R., “Implementing Quicksort Programs”, Communications of the ACM, 21(10):847-857, 1978.

快速排序在现代语言标准库中的实现——没有"纯"快速排序

现代编程语言的标准库几乎都不使用”纯”快速排序,而是采用混合策略来兼顾平均性能和最坏情况保证:

语言/库排序算法关键特性
C++ std::sortIntrosort快速排序 + 堆排序 + 插入排序,最坏
Java Arrays.sort()(基本类型)Dual-Pivot QuicksortYaroslavskiy 2009 年提出,两个主元,比经典快速排序快约 10-20%
Java Arrays.sort()(对象类型)TimSort归并排序 + 插入排序混合,稳定排序,最坏
Python sorted() / list.sort()TimSort2002 年 Tim Peters 为 Python 设计,利用输入中的”自然有序段”
Go sort.Slice()pdqsortpattern-defeating quicksort,结合快速排序、堆排序和插入排序
Rust slice::sort()pdqsort同 Go,由 Orson Peters 于 2016 年实现

关键洞察: Java 在 2009 年将 Arrays.sort() 从经典快速排序切换到 Dual-Pivot Quicksort 后,基准测试显示对随机数据排序速度提升了约 10-20%,对已部分排序的数据提升更显著。这一改动被广泛认为是排序算法工程实践中的一个里程碑。

来源:Yaroslavskiy, V., “Dual-Pivot Quicksort”, 2009; Peters, O., “Pattern-Defeating Quicksort”, 2016.


易混淆点与辨析

误区:只有 50:50 的完美平衡才能保证

错误理解: “快速排序要达到 的运行时间,每次划分必须恰好将数组一分为二”

正确理解: 任意常数比例的划分都能保证 。9:1、99:1 甚至 999:1 的划分都是 。关键在于比例是常数(不随 变化),这保证了递归树深度为 ,每层代价为

真正导致 的不是”不平衡”,而是极度不平衡 vs ),此时递归树退化为链,深度为

划分比例递归树深度每层代价总运行时间
: (最坏)
:
:
: (最好)

误区:快速排序是原地排序,所以只使用 额外空间

错误理解: “快速排序是原地排序算法,所以它的额外空间复杂度是

正确理解: 快速排序的”原地性”指的是不需要额外的数组存储空间(不像归并排序需要 的辅助数组)。但递归调用需要运行时栈空间,栈空间取决于递归深度:

  • 最坏情况(极度不平衡):递归深度 ,栈空间
  • 最好情况(完美平衡):递归深度 ,栈空间

通过尾递归优化(先递归处理较小的子数组),可以将最坏栈深度降至 ,但标准 QUICKSORT 伪代码并未包含此优化。

对比: 归并排序需要 的堆/栈空间(辅助数组),快速排序需要 的栈空间。在空间效率上,快速排序通常优于归并排序。


习题精选

题号题目描述难度
7.2-1用替换法证明递推式 的解为 ⭐⭐
7.2-2当数组 中所有元素值相同时,QUICKSORT 的运行时间是多少?⭐⭐
7.2-3证明当数组 包含不同元素且按递减顺序排序时,QUICKSORT 的运行时间为 ⭐⭐
7.2-4解释为什么在对几乎已排序的输入排序时,插入排序可能优于快速排序⭐⭐⭐
7.2-5假设快速排序每层划分比例为常数 ),证明递归树最小叶深度约为 ,最大叶深度约为 ⭐⭐⭐
7.2-6对含不同元素且所有排列等可能的数组,证明对任意常数 ,PARTITION 产生至少 平衡划分的概率约为 ⭐⭐⭐⭐

视频学习指南

资源主题链接说明
MIT 6.006 Lecture 5Linear-Time Selection & Quicksort Analysishttps://www.youtube.com/watch?v=0VqawH5U5xIErik Demaine 讲解快速排序最坏/最好情况与递归树分析
Abdul BariQuicksort Analysishttps://www.youtube.com/watch?v=COk73cpQbFQ递归树可视化,平衡 vs 不平衡划分的直觉解释
Michael SambolQuicksorthttps://www.youtube.com/watch?v=Hoixgm4-P4M2 分钟可视化,直观展示不同输入下的性能差异
HackerRankQuicksort 1 - Partitionhttps://www.youtube.com/watch?v=uVxjJwcjKSU交互式讲解 PARTITION 与性能分析
CppCon 2019The Sort of Thingshttps://www.youtube.com/watch?v=bY6eU4gI5xgC++ std::sort 的 Introsort 实现与工程优化

教材原文

CLRS 第4版 7.2节原文

The running time of quicksort depends on how balanced each partitioning is, which in turn depends on which elements are used as pivots. If the two sides of a partition are about the same size—the partitioning is balanced—then the algorithm runs asymptotically as fast as merge sort. If the partitioning is unbalanced, however, it can run asymptotically as slowly as insertion sort.

CLRS 第4版 7.2节原文(平衡划分)

Even a 99-to-1 split yields an running time. In fact, any split of constant proportionality yields a recursion tree of depth , where the cost at each level is . The running time is therefore whenever the split has constant proportionality. The ratio of the split affects only the constant hidden in the -notation.

CLRS 第4版 7.2节原文(平均情况直觉)

Intuitively, the cost of the bad split in Figure 7.5(a) can be absorbed into the cost of the good split, and the resulting split is good. Thus, the running time of quicksort, when levels alternate between good and bad splits, is like the running time for good splits alone: still , but with a slightly larger constant hidden by the -notation.


参见Wiki

  • 快速排序 — 快速排序的最坏/最好/平均情况分析

第07章-快速排序 快速排序的性能