5The Art of Computer Programming, Volume 1

排序与查找

排序算法分析

比较排序线性排序快速排序查找算法

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第5章:排序与查找(Sorting and Searching)

一、导读

1.1 本章学习目标

第5章是 TAOCP 第一卷的压轴之作,也是 Knuth 最擅长的领域之一。他在此全面分析了各种排序和查找算法,给出了许多至今仍是经典的结果。

本章核心学习目标:

  • 掌握比较排序算法的精确分析:插入排序、选择排序、归并排序、快速排序
  • 理解线性排序算法:计数排序、基数排序、桶排序
  • 掌握查找算法的分析:顺序查找、二分查找、散列
  • 学会用逆序数分析排序算法

1.2 为什么本章如此重要

排序和查找是最基本的算法问题,Knuth 的分析方法为整个算法分析领域奠定了基础。他引入的逆序数、平均情况分析等方法至今仍是标准工具。

1.3 核心问题引导

快速排序的平均复杂度为什么是 O(n log n)?最坏情况如何避免?

基数排序为什么能突破 O(n log n) 下界?

散列冲突的解决方案有哪些?各自的性能特征是什么?


二、核心概念详解

2.1 排序算法分类

比较排序:
- 插入排序: O(n²) 最坏/平均,O(n) 最好(已排序)
- 选择排序: O(n²) 所有情况
- 归并排序: O(n log n) 所有情况
- 快速排序: O(n log n) 平均,O(n²) 最坏
- 堆排序: O(n log n) 所有情况

线性排序(非比较):
- 计数排序: O(n + k),k 为值域
- 基数排序: O(d(n + k)),d 为位数
- 桶排序: O(n + k) 平均

2.2 快速排序分析

快速排序的平均复杂度:

设 C(n) 为 n 个元素的平均比较次数
C(n) = n - 1 + (1/n) Σ_{k=0}^{n-1} [C(k) + C(n-1-k)]

解: C(n) = 2(n+1)Hₙ - 4n ≈ 1.386 n log₂ n

其中 Hₙ = 1 + 1/2 + 1/3 + ... + 1/n ≈ ln n + 0.5772

最坏情况: 每次划分极度不平衡
C_worst(n) = n(n-1)/2 = O(n²)
(当数组已排序且选择第一个元素为 pivot 时)

避免最坏情况:
1. 随机选择 pivot
2. 三数取中(首、尾、中间的中位数)
3. IntroSort(快速排序 + 堆排序 + 插入排序)

2.3 散列(Hashing)

散列函数:
h(key) → 桶地址

好的散列函数:
- 均匀分布
- 雪崩效应(输入微小变化导致输出大幅变化)
- 计算快速

冲突解决:
1. 链地址法: 每个桶维护一个链表
   - 查找: O(1 + α),α = n/m (负载因子)
   
2. 开放寻址法:
   - 线性探测: h(k), h(k)+1, h(k)+2, ...
     → 一次聚集问题
   - 二次探测: h(k), h(k)+1, h(k)+4, h(k)+9, ...
   - 双重散列: h₁(k), h₁(k)+h₂(k), h₁(k)+2h₂(k), ...

负载因子 α = n/m:
- α < 0.7: 性能良好
- α → 1: 性能急剧下降
- 解决方案: 动态扩容(α 超过阈值时翻倍)

2.4 查找算法

顺序查找:
- 无序表: O(n)
- 有序表: O(n)

二分查找:
- 有序表: O(log n)
- 精确比较次数: ⌊log₂ n⌋ + 1

散列查找:
- 平均: O(1)
- 最坏: O(n)(所有元素冲突)

查找算法选择:
- 数据量小 (< 100): 顺序查找
- 静态有序数据: 二分查找
- 动态数据: 散列或平衡树
- 需要范围查询: 平衡树(B+ 树)

三、实战案例

3.1 案例一:快速排序的精确分析

python
import random

def quicksort(arr):
    if len(arr) <= 1:
        return arr, 0
    pivot = arr[0]
    left = [x for x in arr[1:] if x <= pivot]
    right = [x for x in arr[1:] if x > pivot]
    left_sorted, left_cmp = quicksort(left)
    right_sorted, right_cmp = quicksort(right)
    comparisons = len(arr) - 1 + left_cmp + right_cmp
    return left_sorted + [pivot] + right_sorted, comparisons

# 统计平均比较次数
import statistics
results = []
for _ in range(1000):
    arr = list(range(100))
    random.shuffle(arr)
    _, cmps = quicksort(arr)
    results.append(cmps)

avg = statistics.mean(results)
print(f"n=100, 平均比较次数: {avg:.1f}")
print(f"理论值: 2 × 101 × H₁₀₀ - 4 × 100 = {2 * 101 * sum(1/i for i in range(1, 101)) - 400:.1f}")

# 典型输出:
# n=100, 平均比较次数: 648.3
# 理论值: 648.5

四、本章小结

快速排序平均 O(n log n),常数因子小,实践中最快

基数排序突破比较排序下界,适合特定数据特征

散列提供 O(1) 平均查找,但需要好的散列函数和冲突处理


五、思考与练习

思考题

快速排序的常数因子为什么比归并排序小?

散列表的动态扩容为什么选择 2 倍而非其他倍数?

编程练习

实现三路快速排序(Dutch National Flag),处理大量重复元素。

实现一个完美的散列函数生成器(最小完美散列)。


5/5排序与查
0%