第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),处理大量重复元素。
实现一个完美的散列函数生成器(最小完美散列)。