408考点强化-数据结构
前言
本文是 408 数据结构的一轮考点强化笔记,依托 BOK408 数据结构强化课(26 版)整理,按「时间复杂度 → 线性结构 → 非线性结构 → 查找 → 排序」五个板块推进。
与按教材章节从头捋一遍的复习不同,这里更强调考点的横向串联与易错点的集中对照:同一个知识点在选择题里怎么设干扰项、在大题里怎么出计算、哪些结论必须能现场推导而不能死记。每一部分都尽量落到「考试怎么考」这一层,便于冲刺阶段快速扫盲补漏。
笔记随课程进度持续补充,新增内容按板块顺序追加到对应章节末尾。
主要内容
本文按以下五个板块组织,各板块覆盖的核心范围如下:
- 时间复杂度:算法效率的度量方式,大 O / Ω / Θ 记号的差别与用法,常见代码段的复杂度推导,以及递归式的求解。
- 线性结构:顺序表与链表(单链表、双链表、循环链表、静态链表)的存储方式、基本操作与效率对比;栈、队列与数组(含特殊矩阵的压缩存储)及其典型应用。
- 非线性结构:树与二叉树(含二叉树的遍历、线索二叉树、树 / 森林的转换与存储),哈夫曼树与并查集;图的基本概念、存储结构、遍历(DFS / BFS)与经典应用(最小生成树、最短路径、拓扑排序、关键路径)。
- 查找:顺序查找、折半查找、分块查找,二叉排序树、平衡二叉树与红黑树,B 树与 B+ 树,散列表(散列函数、冲突处理与查找效率分析)。
- 排序:插入排序、希尔排序、交换排序(冒泡、快速)、选择排序(简单选择、堆排序)、归并排序、基数排序,以及各算法的时间 / 空间复杂度、稳定性对比与外部排序。
一、时间复杂度
单层循环是时间复杂度里最基础、也最常考的一类。分析时可以先固化成一个三步流程:
- 确定基本运算:找出循环体里被反复执行的那条语句;
- 找出变化规律:把与循环次数相关的变量列成表,看它随执行次数 $t$ 怎么变化;
- 联系跳出条件:用循环的结束条件把 $t$ 和问题规模 $n$ 联系起来,得到二者的数量级关系。
题型1-单层循环
例题:【2011】
设 $n$ 是描述问题规模的非负整数,下面程序片段的时间复杂度是
x = 2;
while(x < n/2)
x = 2 * x;
第 1 步,找出基本运算。 x = 2 * x,设其执行次数为 $t$。
第 2 步,列表找规律。 看 $t = 1, 2, 3, \dots$ 时 $x$ 的取值:
| 执行次数 $t$ | $x$ 的取值 |
|---|---|
| 1 | $2^2 = 4$ |
| 2 | $2^3 = 8$ |
| 3 | $2^4 = 16$ |
| ⋮ | ⋮ |
| $t$ | $2^{t+1}$ |
可见 $x$ 随 $t$ 指数增长,第 $t$ 次执行后 $x = 2^{t+1}$。
第 3 步,列式子。 循环的跳出条件是 $x \ge n/2$,于是在临界处有 $2^{t+1} < n/2$,解得 $t < \log_2 n - 2$——执行次数 $t$ 与 $\log_2 n$ 同数量级,故 $T(n) = O(\log_2 n)$。
注:$2^{t+1} < n/2$ 这个式子其实并不严谨,它假设的是“第 $t$ 轮算出来的 $x$ 仍然小于 $n/2$”。不过研究时间复杂度不需要这么严谨,只要能得到 $t$ 和 $n$ 的数量级关系就够了。
简单方法:联系归并排序——每次都把长为 $n$ 的表除以 2,一共 $k$ 层,直到长度都为 1($n / 2^k = 1$,即 $k = \log_2 n$)。本题就类似归并操作里的“合并”部分,循环次数自然是 $O(\log_2 n)$。
完整的方法流程要熟练使用:跳出条件往往是 $n$ 和 $x$ 的关系,而 $x$ 和 $t$ 之间的关系可以列表求得,两者一联立就得到 $t$ 和 $n$ 的关系,也就是执行次数与问题规模的关系。
练习:【2017】
下列函数的时间复杂度是
int func(int n){
int i = 0, sum = 0;
while(sum < n) sum += ++i;
return i;
}
这道题的变量不止一个,i 和 sum 都要在表格里列出来(这里 ++i 还是 i++ 都一样):
| 执行次数 $t$ | $i$ | $sum$ |
|---|---|---|
| 1 | 1 | $0 + 1 = 1$ |
| 2 | 2 | $1 + 2 = 3$ |
| 3 | 3 | $3 + 3 = 6$ |
| 4 | 4 | $6 + 4 = 10$ |
| ⋮ | ⋮ | ⋮ |
| $t$ | $t$ | $1 + 2 + \dots + t = \dfrac{t(t+1)}{2}$ |
循环的跳出条件是 $sum \ge n$,即 $\dfrac{t(t+1)}{2} \ge n$,也就是 $t \approx \sqrt{2n}$。所以 $T(n) = O(\sqrt n)$。
小结:单层循环怎么分析
单层循环的复杂度分析,本质上只有一句话——数清循环体到底执行了多少次。落到流程上:
- 锁定基本运算,设执行次数为 $t$;
- 把循环变量的取值随 $t$ 列成表(变量多就都列上),归纳出“第 $t$ 次时它等于什么”;
- 代入跳出条件,解出 $t$ 与 $n$ 的关系,取数量级即得时间复杂度。
难点基本都在第 2 步,按变量每次的变化方式分三类:
| 变量每次的变化 | 典型写法 | $t$ 与 $n$ 的关系 | 复杂度 |
|---|---|---|---|
| 乘 / 除一个常数 | x = 2 * x |
$t \approx \log_2 n$ | $O(\log n)$ |
| 加一个常数 | i++ |
$t \approx n$ | $O(n)$ |
| 累加自身 | sum += ++i |
$t \approx \sqrt{2n}$ | $O(\sqrt n)$ |
题型2-递归
递归类题目也是看两个量,乘起来就是总时间:
- 每次调用执行了多少次基本语句——通常是一个常数,即 $O(1)$;
- 一共调用了多少次——也就是递归的调用次数。
$$T(n) = \text{每次调用的基本语句次数} \times \text{调用次数}$$
例题:【2012】
求整数 $n$($n \ge 0$)阶乘的算法如下,其时间复杂度是
int fact(int n){
if(n <= 1) return 1;
return n * fact(n - 1); }
- 每次调用执行基本语句的次数:一次比较 + 一次乘法,即 $O(1)$;
- 调用次数:每次把 $n$ 减 1,从 $n$ 一路递归到 1,易知调用次数为 $n$;
- 相乘:$1 \times n = n$,所以 $T(n) = O(n)$。
通用方法:怎么数递归的调用次数
调用次数本身就是一个递归结构。设 $C(n)$ 表示“执行 $f(n)$ 一共会发生多少次调用(含 $f(n)$ 自己这一次)”,则
$$C(n) = 1 + \sum_i C(n_i)$$
其中 $n_i$ 是每次递归进去的子问题规模,边界取 $C(\text{基例}) = 1$。这个式子数出来的正好是递归调用树的结点总数——所以数调用次数就是数这棵树有多少个结点,关键是两个量:每层有几个结点(由分支数决定)和一共有几层(由规模衰减的速度决定)。
按结构分几类就很清楚了(例子统一取 $n = 8$):
| 递归结构 | 调用次数递推式 | 调用次数 | 典型例子(以 $n = 8$ 计) |
|---|---|---|---|
| 单分支,规模每次减 1 | $C(n) = C(n-1) + 1$ | $O(n)$ | 阶乘 fact(8) → 8 次 |
| 单分支,规模每次减半 | $C(n) = C(n/2) + 1$ | $O(\log n)$ | 折半查找 $8 \to 4 \to 2 \to 1$ → 4 次 |
| 双分支,规模减常数 | $C(n) = C(n-1) + C(n-2) + 1$ | $O(2^n)$ | 朴素斐波那契 fib(8) → 67 次 |
| 双分支,规模减半 | $C(n) = 2C(n/2) + 1$ | $O(n)$ | 归并排序 $n = 8$ → 15 次 |
两条要点:
- 单分支时“调用次数 = 递归深度”:规模每次减 1 → 深度 $n$;规模每次折半 → 深度 $\log_2 n$。
- 多分支时不能只看深度,还要乘上每层的结点数:斐波那契每减一层就多一个分叉,结点数指数膨胀;归并排序虽然也是两个分支,但规模同时减半,从根往下每层结点数翻倍、层数只有 $\log n$,累计仍是 $O(n)$。
更一般地,凡是能写成 $T(n) = aT(n/b) + f(n)$ 的递归(把“每次调用的基本语句次数”也并进 $f(n)$),都可以直接用主定理定出数量级——这也是大题里推导分治算法复杂度的标准工具。
题型3-for循环嵌套
层层无关
【2014】下列程序段的时间复杂度是
count = 0;
for(k = 1; k <= n; k *= 2)
for(j = 1; j <= n; j++)
count++;
直接两层的循环次数相乘。此处 for 的循环条件又可以联系到题型1——这里最外层的 for 就是题型1里那个「类归并合并」的算法,是 $O(\log_2 n)$;内层 for 从 1 累加到 $n$、每次加 1,是 $O(n)$。两层相乘即 $O(n \log n)$。





