算法与数据结构

比较排序全景与 n log n 下界

05-排序与检索核心预计 25 分钟排序快排归并下界证明
学习状态:

一句话定义

基于「两两比较」的排序家族——插入、选择、冒泡、归并、快排、堆排序——全部无法突破 Θ(n log n) 的比较下界(决策树论证);其中归并稳定、快排缓存友好但需随机化防退化、堆排原地但不稳定,工程排序器几乎都是三者的混合体。

为什么重要

排序是算法工程的全息缩影:同一个目标下聚集了分治、递归式、堆、稳定性、原地性、下界证明几乎所有核心概念。标准库排序为什么长成现在这样(TimSort、introsort),只有理解每个原型的取舍才能读懂。O(n log n) 下界还是你遇到的第一个「不可能性证明」——知道什么时候该放弃优化、转换假设(去 kp-021)。

前置知识

kp-002(渐进记号)、kp-003(递归式)、kp-014(堆)。

核心概念

  • 插入排序:逐个把新元素插进左侧有序段;近有序数据 O(n + 逆序对数)——工程混合排序的「小数据/近有序」零件。
  • 归并排序:分治 + 线性合并;稳定、O(n log n) 有保证,代价 O(n) 辅助数组;链表排序的最优选择。
  • 快速排序:partition 分半 + 两侧递归;平均/期望 O(n log n)、最坏 O(n²);随机化 pivot 或三数取中防退化;缓存局部性最佳,实践中常数最小。
  • 堆排序:建堆 + n 次取顶;原地 O(n log n) 但不稳定且缓存不友好。
  • 三向切分:把与 pivot 相等的元素单独分区,重复元素多时接近 O(n)。
  • 稳定性:相等键保持原相对次序;多关键字排序(先次关键字再稳定排主关键字)依赖它。
  • 决策树下界:n! 种排列需被区分 ⇒ 比较树至少 n! 片叶 ⇒ 高度 ≥ log₂(n!) = Θ(n log n)。

原理与机制

下界证明的机制是「把排序过程建模为决策树」:每次比较把 n! 种可能的排列空间二分,树高至少 log₂(n!),此界对一切比较排序普适。快排的机制是「分区不变量」:扫描中始终保持「左段 < pivot ≤ 右段」,扫完 pivot 归位,递归即成立。工程混合排序的机制是扬长避短:快排主攻随机数据、递归过深转堆排保底(introsort)、小段转插入排序吃常数与局部性,近有序片段由归并识别保留(TimSort)。

直观类比

插入排序是整理扑克牌(左手牌永远有序);归并是两摞已理好的牌轮流取小合并;快排是「先选一张基准牌把牌堆分小于/大于两堆,再各自照办」;堆排序是「先把牌架成金字塔只取最顶」。决策树下界则说:每问一句「谁大」最多二选一,要区分 n! 种可能至少要问 log₂(n!) 句。

公式或模型

  • 下界:log₂(n!) ≥ log₂((n/2)^(n/2)) = Θ(n log n)(只取一半项),配合 Stirling 上界得紧确 Θ(n log n)。
  • 快排期望递归式:T(n) = O(n) + E[T(k) + T(n−1−k)],随机 pivot 下期望 T(n) = O(n log n)(期望深度 O(log n));最坏划分 0/n−1 时 T(n)=T(n−1)+Θ(n)=Θ(n²)。
  • 插入排序与逆序对:运行时间 Θ(n + I),I 为逆序对数——近有序时近似线性。

六算法对比表:

算法平均最坏空间稳定
插入O(n²)(近有序 O(n))O(n²)O(1)✓
归并O(n log n)O(n log n)O(n)✓
快排O(n log n) 期望O(n²)O(log n) 栈✗
堆排O(n log n)O(n log n)O(1)✗
选择O(n²)O(n²)O(1)✗
计数/基数(kp-021)O(n+k)/O(d(n+k))同左O(n+k)✓

图示

快排 partition(Lomuto,pivot=末元素):
 [3, 8, 2, 5, 1, |4|] → 扫描把 <4 的换到左侧
 [3, 2, 1, |4|, 8, 5] → 4 归位,递归左右两段
决策树下界(n=3 示意):
        a<b?
       /     \
     b<c?    a<c?
     / \      / \
   abc bac  acb cab…  叶子 = 一种排列,深度 = 比较次数

实例或案例

  • Python sorted/Java 对象排序:TimSort = 归并 + 插入,利用现实数据的「已有序片段」(run),检测到坏Case也安全。
  • C++ std::sort:introsort = 快排起步、深度超限转堆排、小段转插入排序——三种原型的混合封装。
  • 为什么数据库 ORDER BY 外排用归并思想:k 路归并可流式合并已排序 run(见 kp-023)。

常见误区

  • 「快排总是最快」:近有序/大量重复输入会退化,必须随机化或三向切分。
  • 「O(n log n) 是一切排序的下界」:只在比较模型成立,非比较排序(kp-021)可以 O(n)。
  • 忽视稳定性:多关键字排序、对象排序在非稳定排序下结果错乱且难以察觉。
  • 用排序解决本可 O(n) 的问题(求最值用排序是 n log n,直接扫是 n)——先问「真的需要全序吗」。

自测题

  1. 证明比较排序下界 ≥ log₂(n!),并说明为何它是 Θ(n log n)。

答案要点:n! 种排列必须各占一片叶子;高度 h 的二叉树最多 2^h 叶 ⇒ h ≥ log₂n!;Stirling n! ≈ (n/e)ⁿ ⇒ log₂n! = Θ(n log n)。

  1. 构造快排最坏输入(固定取首元素为 pivot)。

答案要点:已有序或逆序数组:每次划分一边为空,T(n)=T(n−1)+Θ(n)=Θ(n²)。

  1. 为什么链表排序首选归并而非快排?

答案要点:链表随机访问差,partition 的扫描交换不占优势;而归并只需顺序合并,O(n log n) 且稳定,辅助空间仅递归栈。

与其他知识点的关系

kp-003 主定理给出归并/快排的复杂度;kp-014 供给堆排序;kp-021 在放松「比较」假设后突破下界;kp-023 把归并思想推到外存;kp-024 的分治框架以它为头号案例。

延伸阅读

《算法导论》第 2、7、8 章;Bentley & McIlroy 1993(工业级排序函数的坑与设计);Sedgewick《算法(第 4 版)》第 2.3 节(三向切分)。