1、学习目标

本章先回答一个问题:为什么同样能得到结果的程序,仍然要比较“好坏”。数据结构后面每一章都会反复讨论时间、空间、最坏情况、平均情况和分摊意义,所以绪论不是开场白,而是整门课的计量工具。

模块 要掌握的内容
计算 算法是有限、确定、可执行的步骤序列
模型 RAM 模型、基本操作、输入规模
复杂度 大 O、Omega、Theta、常见增长阶
分析 级数估计、循环分析、递归分析
方法 迭代、递归、减而治之、分而治之、动态规划
下界 排序下界、比较决策树、问题本身的限制

2、资料范围

PDF 课件 内容
01.Introduction.A.Computation.pdf 计算、算法、正确性
01.Introduction.B.Computational_Models.pdf 计算模型、RAM、基本操作
01.Introduction.C.Big_o.pdf 大 O 记号和增长阶
01.Introduction.D.Algorithm_analysis.pdf 复杂度估计和常见级数
01.Introduction.E.Iteration_Recursion.pdf 迭代与递归
01.Introduction.F.Dynamic_Programming.pdf 动态规划入口
01.Introduction.X1.Limitation.pdf 算法和计算的限制
01.Introduction.X2.Sorting_Lower_Bound.pdf 排序问题下界

3、核心概念

3.1 算法

算法是一组有限步骤,通常要求:

性质 说明
有穷性 不能无限执行
确定性 每一步含义明确
可行性 每一步能机械执行
输入 有零个或多个输入
输出 至少产生一个结果

数据结构课程里,算法不是孤立存在的。一个操作的复杂度往往取决于底层结构:顺序表插入可能移动大量元素,链表插入可以改指针,但查找又没有随机访问。

3.2 计算模型

常用 RAM 模型把以下操作近似视为常数时间:

1
-*/%、赋值、比较、数组下标访问、指针读写、函数基本跳转

这个模型不是硬件真实细节,而是为了比较算法增长趋势。真正写项目时还要考虑缓存、分支预测、内存分配等工程因素;但在数据结构课里,先把输入规模 n 和基本操作次数说清楚。

4、复杂度记号

记号 含义 直觉
O(f(n)) 渐进上界 不会比 f(n) 增长得更快
Ω(f(n)) 渐进下界 至少有 f(n) 这么慢
Θ(f(n)) 紧确界 上下界同阶
o(f(n)) 严格低阶 相比 f(n) 可忽略

常见增长阶:

1
O(1) < O(log n) < O(n) < O(n log n) < O(n^2) < O(n^3) < O(2^n) < O(n!)

复盘时不要只背顺序,要能解释:

  1. 二分查找每次折半,所以是 O(log n)
  2. 双重循环若每层都和 n 同阶,通常是 O(n^2)
  3. 归并排序每层处理 n 个元素,共 log n 层,所以是 O(n log n)
  4. 枚举所有排列通常到 O(n!)

5、级数与循环估计

常见求和:

形式
1 + 1 + ... + 1 O(n)
1 + 2 + ... + n O(n^2)
1 + 2 + 4 + ... + n O(n)
n + n/2 + n/4 + ... O(n)
1 + 1/2 + ... + 1/n O(log n)
log 1 + log 2 + ... + log n O(n log n)

循环估计流程:

1
2
3
4
for (int i = 1; i <= n; i *= 2) {
work();
}
// i 每次翻倍,执行次数约 log2(n)
1
2
3
4
5
6
for (int i = 0; i < n; ++i) {
for (int j = i; j < n; ++j) {
work();
}
}
// 次数为 n + (n-1) + ... + 1 = O(n^2)

6、迭代、递归与递推式

递归分析要抓住两个东西:每层做多少事,问题规模如何缩小。

递推式 典型算法 复杂度
T(n)=T(n-1)+O(1) 线性递归 O(n)
T(n)=T(n/2)+O(1) 二分查找 O(log n)
T(n)=2T(n/2)+O(n) 归并排序 O(n log n)
T(n)=T(n-1)+T(n-2)+O(1) 朴素 Fibonacci 指数级

朴素 Fibonacci 的问题是重复求解子问题:

1
2
3
4
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}

改成迭代或记忆化后,可以把复杂度降到 O(n)

1
2
3
4
5
6
7
8
9
int fib(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
int c = a + b;
a = b;
b = c;
}
return a;
}

7、动态规划入口

动态规划适用于两类特征明显的问题:

特征 含义
最优子结构 大问题的最优解能由小问题最优解组合出来
重叠子问题 递归过程中反复求解同一批子问题

基本步骤:

  1. 定义状态。
  2. 写出状态转移。
  3. 确定初始状态。
  4. 确定计算顺序。
  5. 还原答案或路径。

数据结构课里,动态规划不是主线,但它帮助理解“算法设计不是只换结构,还要换求解方式”。

8、算法下界

下界讨论的是:不是某个算法不够聪明,而是问题本身至少要付出多少代价。

比较排序的经典结论:

1
任何基于比较的排序算法,最坏情况下需要 Ω(n log n) 次比较。

理由可以用决策树理解:

  1. n 个互异元素有 n! 种可能排列。
  2. 每次比较最多把可能性分成两类。
  3. 决策树至少要有 n! 个叶子。
  4. 高度至少是 log2(n!) = Ω(n log n)

所以归并排序、堆排序的 O(n log n) 在比较排序模型下已经达到最优阶。

9、易错点

易错点 修正
把常数写进大 O 渐进分析忽略常数和低阶项
只看循环层数 内层循环次数可能随外层变化
忽略输入规模 图算法常同时有 ne
混淆最坏和平均 散列表、快速排序尤其要分清
只会背结论 要能从递推式或操作次数推出复杂度

10、复盘清单

检查项 状态
能解释 RAM 模型和输入规模 待复盘
能区分 OΩΘ 待复盘
能估计常见循环复杂度 待复盘
能写出二分、归并、Fibonacci 的递推式 待复盘
能说明朴素递归为什么会重复计算 待复盘
能复述比较排序下界的决策树证明 待复盘