引言
数据结构的期末复习,最怕的不是题目难,而是知识点散落在各处------讲义里有定义、PPT里有伪代码、作业里有坑题、复习资料里有复杂度表。你需要的不是更多的材料,而是一份把零散知识串成体系的完整地图。
本文正是为此而生。它不是简单的知识点罗列,而是按照**"概念 → 操作 → 复杂度 → 易错点 → 考题方向"** 的逻辑,把所有考试范围内的内容重新组织,配合高频题型和解题模板,帮你做到考前心中有数,考场上手不抖。
如果说平时学习是"种树",那么期末复习就是 "画地图"------把每棵树的位置、每棵树的特征、树与树之间的路径都标清楚,考试时无论从哪个方向出题,你都能快速定位到答案。
考试范围速览
| 章节 | 涵盖内容 |
|---|---|
| 树(上) | 树的基本概念、二叉树、二叉树遍历、二叉搜索树(BST) |
| 树(下) | 堆(优先队列)、并查集 |
| 图(上) | 图的定义与存储、DFS/BFS、拓扑排序 |
| 图(下) | 最短路径(无权BFS/Dijkstra/Floyd)、最小生成树(Prim/Kruskal) |
| 排序 | 插入、冒泡、希尔、堆排、归并、快排、表排序、复杂度与稳定性 |
| 散列表 | 散列函数、分离链接、开放定址法(线性探测/平方探测/双散列/Robin Hood/再散列) |
明确排除:AVL树、2-3树、红黑树、Huffman编码、最大流、alpha-beta剪枝、活动选择、回溯、NP-complete、Master定理、Precision/Recall、栈和队列综合题。
第一章:树
1.1 基本概念(必背)
树是由 n≥0n≥0 个结点组成的有限集合。当 n=0n=0 时为空树。非空树有唯一根结点,其余结点可划分为若干互不相交的子树。
核心术语速查表:
| 术语 | 含义 |
|---|---|
| 结点的度 | 结点的子树个数 |
| 树的度 | 所有结点度数的最大值 |
| 叶结点 | 度为0的结点 |
| 路径长度 | 路径上边的条数 |
| 树的深度 | 树中所有结点的最大层次(根为第1层) |
三条必背公式:
-
一棵有 NN 个结点的树有 N−1N−1 条边
-
总边数 = 所有结点度数之和
-
对度为0、1、2、3的树:n0=n2+2n3+1n0=n2+2n3+1(重要!高频)
1.2 二叉树
定义:每个结点最多有2个子树的树。
完全二叉树的性质(高频考点):
-
第 ii 层最多有 2i−12i−1 个结点(根为第1层)
-
前 hh 层最多有 2h−12h−1 个结点
-
数组存储时(下标从1开始):
-
父结点:
i/2 -
左孩子:
2*i -
右孩子:
2*i + 1
-
完全二叉树层数题秒杀公式:
题型:根为第1层,第5层有8个叶结点,问结点最多多少?
思路:为使总结点最多,让另外8个结点都有2个孩子。
-
前5层满:25−1=3125−1=31 个结点
-
第6层最多增加:8×2=168×2=16 个结点
-
最多结点数:31+16=4731+16=47
1.3 二叉树的四种遍历
| 遍历方式 | 访问顺序 | 递归写法 |
|---|---|---|
| 前序 | 根 → 左 → 右 | visit(T); preorder(T->L); preorder(T->R); |
| 中序 | 左 → 根 → 右 | inorder(T->L); visit(T); inorder(T->R); |
| 后序 | 左 → 右 → 根 | postorder(T->L); postorder(T->R); visit(T); |
| 层序 | 从上到下、从左到右 | 用队列BFS |
唯一还原定理(高频判断题):
-
前序 + 中序 → 唯一确定
-
后序 + 中序 → 唯一确定
-
前序 + 后序 → 通常不唯一
1.4 二叉搜索树(BST)
定义:
-
左子树所有键值 小于 根
-
右子树所有键值 大于 根
-
左右子树也都是BST
关键性质 :BST的中序遍历结果是递增序列(判断题高频考点)
核心操作复杂度:
| 操作 | 时间复杂度 |
|---|---|
| 查找 | O(h)O(h) |
| 插入 | O(h)O(h) |
| 删除 | O(h)O(h) |
| 查最大 | 一直往右,O(h)O(h) |
| 查最小 | 一直往左,O(h)O(h) |
其中 hh 是树高。平衡时 h=O(logN)h=O(logN),最坏(退化为链)时 h=O(N)h=O(N)。
BST删除三种情况:
-
删除叶结点:父结点指针置空
-
删除度为1的结点:父结点指向其唯一孩子
-
删除度为2的结点:用左子树最大值 或右子树最小值替换,再删除替身结点
易错点:
-
插入顺序不同,BST形态可能完全不同
-
递增序列插入普通BST会退化成链
-
BST ≠ 堆!BST保证左右子树整体大小关系,堆只保证父子大小关系
第二章:堆(优先队列)
2.1 堆的定义
堆 = 完全二叉树 + 堆序性
-
最大堆:父结点 ≥ 子结点
-
最小堆:父结点 ≤ 子结点
2.2 核心操作与复杂度
| 操作 | 做法 | 时间复杂度 |
|---|---|---|
| 插入 | 插到末尾,上滤 | O(logN)O(logN) |
| 删除堆顶 | 取根,末尾元素放根,下滤 | O(logN)O(logN) |
| 建堆 | 从最后一个非叶结点开始下滤 | O(N)O(N) |
高频判断点 (来自 ds_exam_prediction_manual.pdf):
-
线性建堆的复杂度是 O(N)O(N),不是 O(NlogN)O(NlogN)
-
逐个插入建堆才是 O(NlogN)O(NlogN)
2.3 下标关系(必背)
| 编号方式 | 左孩子 | 右孩子 | 父结点 |
|---|---|---|---|
| 从0开始 | 2*i + 1 |
2*i + 2 |
(i-1)/2 |
| 从1开始 | 2*i |
2*i + 1 |
i/2 |
2.4 建堆并DeleteMin题型(高频)
题型:用线性算法把 [16, 28, 14, 13, 7, 6] 建成最小堆,再 DeleteMin。
常见判断(来自预测手册):
-
7是根 → ✓
-
13和14是兄弟 → ✓
-
13是16的父结点 → ✓
-
28是13的左孩子 → ✗(是右孩子)
第三章:并查集
3.1 核心表示法
用数组 S\[\] 表示:
-
S[x] < 0:x 是根,-S[x]表示集合大小或树高 -
S[x] >= 0:S[x]是 x 的父结点
3.2 核心操作
cpp
// 查找(带路径压缩)
int Find(int S[], int x) {
if (S[x] < 0) return x;
return S[x] = Find(S, S[x]);
}
// 按规模归并
void Union(int S[], int r1, int r2) {
if (S[r1] < S[r2]) { // r1 的规模更大(负数更小)
S[r1] += S[r2];
S[r2] = r1;
} else {
S[r2] += S[r1];
S[r1] = r2;
}
}
3.3 并查集题型秒杀
题型:S ={1, -4, 1, 1, -3, 4, 4, 8, -2},合并包含6和8的两个集合。
分析:
-
Find(6) = 4,S4 = -3 → 规模3
-
Find(8) = 8,S8 = -2 → 规模2
-
按规模归并:小树(8)接到大树(4)上
-
新根 = 4,新根值 = -3 + (-2) = -5
答案:4 and -5
3.4 复杂度
| 优化方式 | 树高 | 复杂度 |
|---|---|---|
| 朴素并查集 | 可能 O(N)O(N) | 最坏 O(N)O(N) |
| 按秩归并 | O(logN)O(logN) | O(logN)O(logN) |
| 按秩归并 + 路径压缩 | 近似常数 | 均摊近似 O(1)O(1) |
易错点:
-
Union 前必须先 Find,合并的是根,不是原元素
-
路径压缩改变树形,但不改变集合划分
-
负数比较:-10 表示比 -3 更大的集合(10 > 3)
第四章:图
4.1 图的基本概念
| 术语 | 含义 |
|---|---|
| 有向图 | 边有方向 <v, w> |
| 无向图 | 边无方向 (v, w) |
| 网络 | 边带权值 |
| 连通图 | 无向图中任意两点连通 |
| 强连通图 | 有向图中任意两点双向可达 |
| 路径 | 顶点序列,相邻顶点之间有边 |
| 回路 | 起点等于终点的路径 |
| 连通分量 | 无向图的极大连通子图 |
| 强连通分量 | 有向图的极大强连通子图 |
4.2 图的存储(常考判断)
| 存储方式 | 空间 | 查边 | 遍历邻接点 | 适用场景 |
|---|---|---|---|---|
| 邻接矩阵 | O(V2)O(V2) | O(1)O(1) | O(V)O(V) | 稠密图 |
| 邻接表 | O(V+E)O(V+E) | 较慢 | O(deg)O(deg) | 稀疏图 |
高频判断点:
-
邻接表空间是否只与顶点数有关?------ 错,还与边数有关(O(V+E)O(V+E))
-
邻接矩阵适合稠密图,邻接表适合稀疏图
4.3 DFS / BFS
DFS:深度优先,类似树的前序遍历,递归或栈。一条路走到底再回溯。
BFS:广度优先,用队列。一层一层向外扩展。
复杂度:
| 存储方式 | DFS/BFS 复杂度 |
|---|---|
| 邻接表 | O(V+E)O(V+E) |
| 邻接矩阵 | O(V2)O(V2) |
连通分量:遍历所有顶点,遇到未访问顶点就启动一次 DFS/BFS,每启动一次得到一个连通分量。
BFS vs DFS 适用场景:
-
BFS 可求无权图最短路
-
DFS 不保证最短,但适合连通性判断和回溯搜索
4.4 拓扑排序
定义:如果从 v 到 w 有路径,则 v 一定排在 w 之前。满足此条件的顶点序列称为拓扑序。
适用条件 :图必须是 DAG(有向无环图)
队列版算法(常考程序填空):
text
1. 计算所有顶点入度
2. 所有入度为0的顶点入队
3. while (队列非空) {
v = 出队; 输出 v; count++;
for (v 的每个邻接点 w) {
indegree[w]--;
if (indegree[w] == 0) 入队(w);
}
}
4. if (count < V) 图中有回路
复杂度:O(V+E)O(V+E)
常考点:
-
拓扑排序能否检测有向图回路?------ 能
-
入度队列法如果使用堆栈而不是队列,得到的拓扑序可能不同,但都是合法的
第五章:最短路径
5.1 三种算法对比(必背)
| 问题 | 算法 | 适用条件 | 时间复杂度 |
|---|---|---|---|
| 无权单源 | BFS | 边权相同 | O(V+E)O(V+E) |
| 有权单源 | Dijkstra | 非负权 | 邻接矩阵 O(V2)O(V2),堆优化 O(ElogV)O(ElogV) |
| 多源 | Floyd | 任意两点 | O(V3)O(V3) |
5.2 Dijkstra 核心逻辑
text
while (1) {
V = 未收录顶点中 dist 最小者;
if (不存在) break;
collected[V] = true;
for (V 的每个邻接点 W) {
if (!collected[W]) {
if (dist[V] + G[V][W] < dist[W]) {
dist[W] = dist[V] + G[V][W];
path[W] = V;
}
}
}
}
程序填空关键代码:
cpp
if (!collected[w]) {
if (dist[v] + G[v][w] < dist[w]) {
dist[w] = dist[v] + G[v][w];
path[w] = v;
}
}
高频判断点:
-
Dijkstra 按距离递增顺序确定最短路 → ✓
-
Dijkstra 不能处理负边 → ✓
-
"负边会导致 Dijkstra 无限循环" → ✗(它只是结果不正确,不会无限循环)
-
所有边权相等时,BFS 可求最短路径 → ✓
5.3 Floyd 核心逻辑(程序填空必考)
cpp
// 初始化 D[i][j] = G[i][j]
for (k = 0; k < N; k++)
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
if (D[i][k] + D[k][j] < D[i][j])
D[i][j] = D[i][k] + D[k][j];
口诀:最外层是"允许经过的中间点"
另一种写法(如果题目变量顺序不同):
cpp
for (i = 0; i < N; i++)
for (k = 0; k < N; k++)
for (j = 0; j < N; j++)
if (D[k][i] + D[i][j] < D[k][j])
D[k][j] = D[k][i] + D[i][j];
第六章:最小生成树(MST)
6.1 MST 的性质(高频判断)
-
是一棵树:无回路,有 V-1 条边
-
是生成树:包含全部 V 个顶点
-
边权和最小
-
只对连通无向图存在
-
如果图不连通,MST 不存在 → 判断"MST always exists"是错的
6.2 Prim vs Kruskal
| 算法 | 思路 | 选边方式 | 判环 | 适用 |
|---|---|---|---|---|
| Prim | 一棵小树长大 | 树到外部的最小边 | 无显式判环 | 稠密图 |
| Kruskal | 森林合并 | 全图剩余最小边 | 并查集 | 稀疏图 |
Prim 程序填空要点:
-
与 Dijkstra 很像,但
dist[W]表示到当前树的最小边权,不是源点到 w 的路径长 -
初始化:
dist[s] = 0,其他为 INF
Kruskal 程序填空要点:
-
边按权重排序(或用最小堆)
-
用并查集判断是否成环
-
不成环就加入,成环就丢弃
-
直到收录 V-1 条边
复杂度:Kruskal O(ElogE)O(ElogE)
第七章:排序
7.1 七种排序复杂度总表(必背)
| 排序算法 | 最好 | 平均 | 最坏 | 稳定 | 额外空间 |
|---|---|---|---|---|---|
| 冒泡排序 | O(N)O(N) | O(N2)O(N2) | O(N2)O(N2) | ✓ | O(1)O(1) |
| 插入排序 | O(N)O(N) | O(N2)O(N2) | O(N2)O(N2) | ✓ | O(1)O(1) |
| 选择排序 | O(N2)O(N2) | O(N2)O(N2) | O(N2)O(N2) | ✗ | O(1)O(1) |
| 希尔排序 | 与增量有关 | 与增量有关 | 常见优于 O(N2)O(N2) | ✗ | O(1)O(1) |
| 堆排序 | O(NlogN)O(NlogN) | O(NlogN)O(NlogN) | O(NlogN)O(NlogN) | ✗ | O(1)O(1) |
| 归并排序 | O(NlogN)O(NlogN) | O(NlogN)O(NlogN) | O(NlogN)O(NlogN) | ✓ | O(N)O(N) |
| 快速排序 | O(NlogN)O(NlogN) | O(NlogN)O(NlogN) | O(N2)O(N2) | ✗ | O(logN)O(logN)(递归栈) |
7.2 稳定性判断口诀(来自 ds_final_review.pdf)
插入适合基本有序,堆排原地但不稳定;
归并稳定但费空间,快排平均快但最坏平方。
稳定性速记:冒泡、插入、归并稳定;选择、希尔、堆排、快排不稳定。
7.3 逆序对(必考概念)
-
如果 i<ji<j 且 Ai>AjAi>Aj,则 (i,j)(i,j) 是一对逆序对
-
交换相邻元素一次只能消去 1 个逆序对
-
插入排序复杂度可写成 O(N+I)O(N+I),其中 II 是逆序对数
-
任何仅交换相邻元素的排序,平均时间复杂度下界为 Ω(N2)Ω(N2)
7.4 快速排序(高频过程题)
三数取中:取头、中、尾三个元素的中位数作为主元(pivot)
题型 :输入 [21, 85, 32, 49, 67, 1, 12],三数取中,问第一次 partition 后序列。
分析:
-
头=21,中=49,尾=12 → 中位数是 21,主元为 21
-
按课程代码模拟交换后结果:
[12, 1, 21, 85, 67, 32, 49]
注意:这类题不要只找 pivot,要按课程代码模拟交换!
7.5 堆排序(下标注意)
0 下标堆:
-
左孩子:
2*i + 1 -
右孩子:
2*i + 2 -
父结点:
(i-1)/2
建堆和排序算法要点:
-
从最后一个非叶结点开始下滤建最大堆
-
每次将堆顶(最大值)与末尾元素交换
-
堆规模减1,从根下滤恢复堆序
-
重复直到堆空
第八章:散列表
8.1 基本概念
-
散列函数 :
h(key)将关键字映射到地址 -
装填因子:α=n/mα=n/m(n 为元素个数,m 为表空间大小)
-
冲突:不同关键字映射到同一地址
-
理想情况下查找/插入/删除为 O(1)O(1)
8.2 散列函数
整数 :除留余数法 h(x) = x % TableSize,TableSize 取素数效果较好
字符串:移位法
cpp
Index Hash(const char *x, int TableSize) {
unsigned int HashVal = 0;
while (*x != '\0')
HashVal = (HashVal << 5) + *x++;
return HashVal % TableSize;
}
8.3 冲突处理方式
| 方式 | 特点 | 聚集类型 |
|---|---|---|
| 分离链接法 | 同义词挂同一链表 | 无聚集 |
| 线性探测 | f(i) = i |
初级聚集 |
| 平方探测 | f(i) = i^2 |
二级聚集 |
| 双散列 | f(i) = i * h2(key) |
无明显聚集 |
| 再散列 | 扩表后重新插入 | - |
8.4 平方探测题型秒杀(高频)
题型:表长 13,h(c) = c % 13,平方探测,插入 [10, 23, 1, 36, 19, 5],问下标 6 放谁。
过程:
-
10 → 10
-
23 → 10 冲突,10+12=1110+12=11
-
1 → 1
-
36 → 10 冲突,11 冲突,10+22=14≡110+22=14≡1 冲突,10+32=19≡610+32=19≡6
-
19 → 6 冲突,6+12=76+12=7
-
5 → 5
答案:下标 6 是 36
重要性质:
-
当 TableSize 为 4k+34k+3 形式的素数时,平方探测可以探查到整个表空间
-
装填因子小于 0.5 时,平方探测总能找到插入空位
8.5 线性探测 vs 双散列判断点(高频)
-
线性探测等价于双散列中步长为 1 → ✓
-
线性探测会引起初级聚集 → ✓
-
平方探测会引起二级聚集 → ✓
-
再散列在装填因子过高时触发
第九章:考前最后背诵清单
9.1 必背公式与模板
树:
-
边数 = N - 1
-
n0=n2+2n3+1n0=n2+2n3+1
-
完全二叉树前 h 层结点数 = 2h−12h−1
堆:
- 建堆 O(N)O(N),插入/删除 O(logN)O(logN)
并查集:
-
Sx < 0 是根,负数表示规模
-
Union 先 Find
图:
-
邻接表 DFS/BFS = O(V+E)O(V+E)
-
邻接矩阵 DFS/BFS = O(V2)O(V2)
-
无权 BFS → Dijkstra → Floyd
MST:
-
Prim 稠密图,Kruskal 稀疏图
-
Kruskal 判环用并查集
排序:
-
冒泡/插入/归并稳定;选择/希尔/堆排/快排不稳定
-
快排最坏 O(N2)O(N2),三数取中优化
散列表:
-
装填因子 α=n/mα=n/m
-
平方探测表长取素数 4k+34k+3
9.2 考场优先检查
-
题目问的是"有向图"还是"无向图"
-
边是否有权,权值是否可能为负
-
图是稀疏还是稠密
-
排序是否要求稳定
-
数组下标是从 0 还是从 1 开始
-
散列表表长是否为素数
-
输出的是一种合法结果,还是要求唯一结果
总结
数据结构的学习,本质上是理解数据在计算机中的组织方式 以及在这些组织方式上的操作效率。树是分层关系,图是多对多关系,散列表是映射关系,排序是整理关系------每一种结构都有其诞生的原因和适用的场景。
期末复习不必追求"背下所有细节",而应该做到**"看到题目能快速定位到对应章节和对应公式"**。这份笔记已经把最核心的公式、复杂度、易错点和高频题型都整理好了,剩下的就是动手写几遍、画几遍、推几遍。