数据结构与算法详细复习笔记
第2章:线性表 (Linear List)
1. 基本概念
- 定义:由
个数据特性相同的元素构成的有限序列。 - 特性:
- 有序性:元素之间有逻辑上的顺序(第一个、最后一个、前驱、后继)。
- 有限性:元素个数有限。
- 特性:
- 抽象数据类型 (ADT):
- 数据对象:
- 数据关系:
- 基本操作:
InitList,DestroyList,ListInsert,ListDelete,GetElem,LocateElem等。
- 数据对象:
2. 顺序表 (Sequential List)
- 定义:用一组地址连续的存储单元依次存储线性表的数据元素。
- 特点:
- 随机存取:通过首地址和下标可在
时间内访问任意元素。公式: 。 - 存储密度高:无需为逻辑关系(指针)额外分配空间。
- 缺点:插入/删除需要移动大量元素(平均移动
),需预分配空间(静态分配易溢出,动态分配需扩容)。
- 随机存取:通过首地址和下标可在
3. 链表 (Linked List)
- 单链表 (Singly Linked List):
- 结构:结点 = 数据域 (data) + 指针域 (next)。
- 头结点:在首元结点前附设的一个结点,便于处理空表和统一插入/删除操作。
- 建立方法:
- 头插法:新结点插入头结点之后,读入顺序与链表顺序相反(逆序)。
- 尾插法:维护一个尾指针
r,新结点插在r之后,读入顺序与链表顺序相同。
- 双向链表 (Doubly Linked List):
- 结构:结点包含
prior(前驱) 和next(后继) 指针。 - 插入操作 (
s插在p之后):s->prior = p;s->next = p->next;p->next->prior = s;p->next = s;(注意顺序,防止断链)
- 删除操作 (删除
p的后继节点q):p->next = q->next;q->next->prior = p;free(q);
- 结构:结点包含
- 循环链表 (Circular Linked List):
- 特点:表中最后一个结点的指针指向头结点,形成环状。
- 判空/遍历结束:指针是否等于头指针 (or 尾指针)。
- 优势:从任意结点出发均可遍历全表;若设尾指针
rear,则查找头尾的时间均为(便于合并两个链表)。
- 静态链表:
- 利用数组实现,元素包含
data和cur(游标,即下个元素的数组下标)。 - 适用于不支持指针的语言(如 BASIC)或数据量固定且需频繁操作指针的场景。
- 利用数组实现,元素包含
- 块状链表 (Unrolled Linked List):
- 每个结点存储多个数据元素(即一个小的顺序表)。平衡了顺序表(空间紧凑)和链表(插入删除快)的优缺点。
4. 应用
- 一元多项式相加:利用链表按指数升序存储。遍历两个链表,比较指数大小:
- 指数相同:系数相加,若非零则保留。
- 指数不同:将指数小的项链入结果链表。
- 大整数处理:利用链表存储大整数的每一位(或每几位),便于进位处理。
第3章:栈与队列 (Stack & Queue)
1. 栈 (Stack)
- 定义:只允许在表尾(栈顶 Top)进行插入(Push)和删除(Pop)的线性表。LIFO (Last In First Out)。
- 实现:
- 顺序栈:
top指针指向栈顶元素(或栈顶上一个空位)。需判断StackOverflow(上溢) 和StackUnderflow(下溢,即空栈 pop)。 - 共享栈:两个栈共享一段空间,栈底在两端,栈顶向中间延伸,
top1 + 1 == top2时满。
- 顺序栈:
- 应用:
- 递归:系统栈保存每一层调用的局部变量、返回地址。
- 括号匹配:左括号压栈,右括号弹栈匹配。
- 表达式求值:
- 中缀转后缀 (RPN):遇操作数输出;遇运算符与栈顶比较优先级。
- 后缀计算:遇操作数压栈;遇运算符弹栈计算结果并压栈。
- 迷宫求解:DFS 深度优先搜索(回溯法)。
2. 队列 (Queue)
- 定义:只允许在表尾(Rear)插入,表头(Front)删除。FIFO (First In First Out)。
- 循环队列 (Circular Queue):
- 问题:顺序队列单纯
front++rear++会导致“假溢出”(即前方有空位但 rear 已达上限)。 - 解决:模运算。
(rear + 1) % MAXSIZE。 - 判空/判满:为了区分队空和队满,通常牺牲一个单元。
- 队空:
front == rear - 队满:
(rear + 1) % MAXSIZE == front - 队长:
(rear - front + MAXSIZE) % MAXSIZE
- 队空:
- 问题:顺序队列单纯
- 双端队列 (Deque):两端均可入队、出队。
- 变种:输入受限(一端入两端出)、输出受限(两端入一端出)。
第4章:字符串 (String)
1. 基本概念
- 串:零个或多个字符组成的有限序列。
- 子串:主串中任意连续字符组成的子序列。
- 存储:定长顺序存储(截断)、堆分配存储(动态指针)、块链存储。
2. 模式匹配算法
- 朴素模式匹配 (Brute-Force):
- 主串指针
i,模式串指针j。 - 失配时:
i回溯到i - j + 1,j回溯到0。 - 最坏时间复杂度:
。
- 主串指针
- KMP 算法 (Knuth-Morris-Pratt):
- 核心:主串指针
i不回溯,模式串指针j回退到next[j]。 - Next 数组:
next[j]表示模式串第j个字符前的子串中,最长相等前后缀的长度。- 当
S[i] != P[j]时,j移动到next[j]继续比较。
- 当
- NextVal 数组(优化):若
P[j] == P[next[j]],则递归优化,避免重复无效比较。 - 时间复杂度:
。
- 核心:主串指针
第5章:数组和广义表
1. 数组
- 存储:内存是一维的,多维数组需映射。
- 行优先 (Row-Major):C/C++。
LOC(a[i][j]) = LOC(0,0) + (i * n + j) * size。 - 列优先 (Column-Major):Fortran。
- 行优先 (Row-Major):C/C++。
2. 矩阵压缩存储
- 对称矩阵:
。只存上三角或下三角。按行优先存储下三角元素 的下标 。 - 稀疏矩阵 (Sparse Matrix):
- 三元组表:
(i, j, value)。空间优化,但丧失随机存取特性。 - 十字链表:每个非零元节点既在行链表上,又在列链表上。便于频繁插入删除。
- 三元组表:
3. 广义表 (Generalized List)
- 定义:
,其中 可以是原子或子表。 - 重要操作:
- GetHead(L):取表头(第一个元素,可能是原子或列表)。
- GetTail(L):取表尾(除第一个元素外剩余元素构成的表,结果一定是一个表)。
- 例:
,Head(L)= ,Tail(L)= 。
第6章:树与二叉树
1. 二叉树 (Binary Tree)
- 性质:
- 第
层至多 个结点。 - 深度为
至多 个结点。 - 核心性质:
(叶子结点数 = 度为2的结点数 + 1)。
- 第
- 完全二叉树 (Complete Binary Tree):
- 编号规则与满二叉树一一对应。
- 数组存储时,结点
的左孩子为 ,右孩子为 ,父节点 。
2. 遍历 (Traversal)
- 前序 (Pre):根 -> 左 -> 右
- 中序 (In):左 -> 根 -> 右
- 后序 (Post):左 -> 右 -> 根
- 层序:利用队列实现 BFS。
- 构造:
- 前序 + 中序 -> 唯一确定二叉树。
- 后序 + 中序 -> 唯一确定二叉树。
- 前序 + 后序 -> 不能唯一确定(无法区分左右子树)。
3. 应用
- 哈夫曼树 (Huffman Tree):
- 构造:每次选取权值最小的两棵树合并,新树权值为两者之和,直到只剩一棵。
- WPL (带权路径长度):
。 - 哈夫曼编码:前缀编码(任一字符编码都不是另一字符编码的前缀),用于数据压缩。
- 线索二叉树:
- 利用空闲指针域(
lchild若空指向前驱,rchild若空指向后继),通过ltag/rtag标记。 - 加快遍历速度,无需递归或栈。
- 利用空闲指针域(
第7章:图 (Graph)
1. 存储结构
- 邻接矩阵:
AdjMatrix[i][j] = 1或权值。- 优点:易判断两点是否相连,易算度(行和列和)。
- 缺点:稀疏图浪费空间
。
- 邻接表:顶点表 + 边表(链表)。
- 优点:节省空间
。 - 缺点:求入度麻烦(需遍历全表或使用逆邻接表)。
- 优点:节省空间
2. 遍历
- DFS (深度优先):类似树的先序遍历,使用递归/栈。
- BFS (广度优先):类似树的层序遍历,使用队列。最短路径特性(无权图)。
第8章:图的应用 (重难点)
1. 最小生成树 (MST)
- Prim 算法:
- 思想:归并点。从一个顶点开始,每次选一条连接“已选集合”和“未选集合”且权值最小的边。
- 复杂度:
,适合稠密图。
- Kruskal 算法:
- 思想:归并边。将边按权值排序,从小到大选边,若不构成回路(利用并查集判断)则加入。
- 复杂度:
,适合稀疏图。
2. 最短路径
- Dijkstra 算法:
- 单源最短路。贪心策略。每次从未标记节点中选距离源点最近的节点,松弛其邻接点。
- 限制:边权不能为负。
- 复杂度:
。
- Floyd 算法:
- 多源最短路。动态规划。
A[i][j] = min(A[i][j], A[i][k] + A[k][j])。 - 复杂度:
。
- 多源最短路。动态规划。
3. AOV 网与拓扑排序
- 定义:顶点表示活动,边表示优先关系。
- 算法:
- 选择入度为 0 的顶点输出。
- 删除该顶点及其出边。
- 重复直到图空或无入度为 0 的点(说明有环)。
4. AOE 网与关键路径
- 定义:边表示活动,边权为持续时间,顶点表示事件。
- 关键路径:从源点到汇点的最长路径(决定工程最短工期)。
- 计算:
- 事件最早发生时间
ve(从前向后取max)。 - 事件最晚发生时间
vl(从后向前取min)。 - 活动最早/最晚开始时间
e/l。 - 若
e == l,则为关键活动。
- 事件最早发生时间
第9章:查找 (Search)
1. 动态查找树
- 二叉排序树 (BST):左 < 根 < 右。中序遍历有序。插入/删除/查找平均
,最坏 (退化为链表)。 - 平衡二叉树 (AVL):
- 任一节点左右子树高度差(平衡因子 BF)绝对值
。 - 调整:
- LL型:右旋(Right Rotation)。
- RR型:左旋(Left Rotation)。
- LR型:先左旋后右旋。
- RL型:先右旋后左旋。
- 任一节点左右子树高度差(平衡因子 BF)绝对值
2. 散列 (Hashing)
- 散列函数:
H(key)。除留余数法H(key) = key % p(p 为不大于表长的最大质数)。 - 处理冲突:
- 开放定址法:
- 线性探测:
di = 1, 2, 3...(易产生聚集/堆积现象)。 - 二次探测:
di = 1^2, -1^2, 2^2, -2^2...。
- 线性探测:
- 链地址法:相同 Hash 值的元素存入同一链表。
- 开放定址法:
- ASL (平均查找长度):衡量效率指标,依赖于装填因子
。
第10章:排序 (Sorting)
1. 算法复杂度与稳定性对比
| 类别 | 排序算法 | 平均时间 | 最坏时间 | 空间 | 稳定性 | 备注 |
|---|---|---|---|---|---|---|
| 插入 | 直接插入 | 稳定 | 适合基本有序/n较小 | |||
| 希尔排序 | 不稳定 | 增量 | ||||
| 交换 | 冒泡排序 | 稳定 | 标志位优化 | |||
| 快速排序 | 不稳定 | 内排序最快,枢轴选择关键 | ||||
| 选择 | 简单选择 | 不稳定 | 移动次数少 | |||
| 堆排序 | 不稳定 | 适合 Top K 问题 | ||||
| 归并 | 归并排序 | 稳定 | 空间换时间,外排序基础 | |||
| 基数 | 基数排序 | 稳定 | 无需比较,分配+收集 |
2. 核心算法说明
- 快速排序 (Quick Sort):
- Partition:选取枢轴(pivot),将小于 pivot 的放左边,大于的放右边。
- 最坏情况:有序或逆序(退化为冒泡)。
- 堆排序 (Heap Sort):
- 建堆:从
处开始向下调整 (Heapify)。 - 排序:交换堆顶与末尾元素,堆大小减1,重新向下调整。
- 升序排序使用大顶堆。
- 建堆:从
- 归并排序:
- Divide (分):将数组对半切分。
- Conquer (治):递归排序子数组。
- Merge (合):合并两个有序数组。
附录:数据结构小测涉及公式与重点总结
通过分析《小测1 答案》、《小测2 答案》及《小测3_题目答案》,以下是针对考试重点涉及的公式、推导及核心结论的详细总结。
1. 栈与队列 (Stack & Queue)
卡特兰数 (Catalan Number)
- 应用场景:
个不同元素进栈,可能的出栈序列总数。 - 公式:
- 小测案例 (小测1 第6题):
时,出栈序列数为 种。
双端队列输出序列
- 排除法逻辑 (小测1 第8题):
- 对于输入受限或输出受限的双端队列,通常通过模拟和排除法来判断非法序列。
- 核心技巧:结合栈的后进先出特性与队列的先进先出特性进行模拟。
2. 数组与矩阵 (Arrays & Matrices)
三对角矩阵压缩存储
- 定义:元素仅分布在主对角线及上下两条对角线上。
- 下标映射公式 (小测1 第10题):
- 题目特定映射关系:
压缩至 。 - 给定公式推导结果:
。 - 案例验证:
对应 。 - 注意:通用公式通常为
或类似,考试时需严格根据题目给定的数组下标起始点(如从0还是从1开始)及排列方式进行推导。
- 题目特定映射关系:
KMP 算法 Next 数组计算
- 推导逻辑 (小测1 第13题):
- 若
(其中 ),则 。 - 若不等,则令
继续回溯比较。 - 案例:
(固定) ? (前缀比较), , 回溯...
- 若
3. 树与二叉树 (Trees)
二叉树节点性质公式
度数关系:
- 总节点数
- 总分支数
- 联立消去
,得 。
- 总节点数
- 小测陷阱 (小测2 第1题):已知总数
, ,求叶子数。 - 代入
。 必须为整数,故不存在这样的树。
完全二叉树 (Complete Binary Tree)
- 叶子结点数:$$n_0 = \lceil n/2 \rceil$$ (当
为奇数时 ,偶数时 )。 - 案例 (小测2 第3题):
(奇数), 。
- 叶子结点数:$$n_0 = \lceil n/2 \rceil$$ (当
哈夫曼树 (Huffman Tree)
- 特性:只有度为0和度为2的结点,不存在度为1的结点 (
)。 - 节点总数公式:
- 推导:
,且 。 - 小测结论 (小测2 第12题):若有
个叶子结点(题目用 表示 ),则总结点数为 。
- 特性:只有度为0和度为2的结点,不存在度为1的结点 (
森林与树的转换
- 连通分量/树的数量计算 (小测2 第9题):
- 若森林有
个结点, 条边,则森林中树的棵数 为: - 推导:每棵树 (连通分量) 的边数 = 节点数 - 1。设第
棵树有 个节点,则边数 。 - 总边数
。
- 若森林有
- 连通分量/树的数量计算 (小测2 第9题):
4. 图 (Graphs)
握手定理 (Handshaking Lemma)
- 公式:所有顶点的度数之和等于边数的2倍。
- 最少顶点数求解 (小测3 第2题):
- 已知:
(度数和32)。 - 已知点:3个度4,4个度3。已知度数和
。 - 剩余度数:
。 - 约束:其余顶点度数
(即最大为2)。 - 目标:顶点数最少
剩余顶点度数尽可能大 设其余点度数均为2。 - 剩余点数:
个。 - 总顶点数:
个。
- 已知:
最小生成树 (MST)
- 唯一性判定 (小测3 第8题):
- 当无向连通图的最小生成树不唯一时,Prim 和 Kruskal 算法生成的结果可能不同。
- 当最小生成树唯一时,两者结果必然相同。
关键路径 (Critical Path)
- 计算方法 (小测3 第11题):
- 路径长度 = 路径上所有活动(边)持续时间之和。
- 关键路径 = 源点到汇点的最长路径。
- 案例:路径
。 - 长度计算:
。
排序算法深度解析 (Sorting Algorithms Deep Dive)
本文档对常见的排序算法进行展开讲解,包含核心思想、算法步骤、过程演示、复杂度分析及代码逻辑。
1. 插入排序类 (Insertion Sorts)
1.1 直接插入排序 (Direct Insertion Sort)
核心思想: 将数组分为“已排序区间”和“未排序区间”。每次从未排序区间取出一个元素,在已排序区间中从后向前扫描,找到合适位置插入。类似于抓扑克牌的过程。
算法步骤:
- 默认第 1 个元素已排序。
- 取出第 2 个元素
temp,与第 1 个比较。若temp < arr[1],则arr[1]后移,temp插入开头。 - 重复直到所有元素归位。
过程演示 (排序 [5, 2, 4, 6, 1, 3]):
- 初始:
[5]|2, 4, 6, 1, 3 - 取 2:2 比 5 小,5 后移。 ->
[2, 5]|4, 6, 1, 3 - 取 4:4 比 5 小,5 后移;4 比 2 大,停。 ->
[2, 4, 5]|6, 1, 3 - ...以此类推。
核心代码逻辑 (C/C++):
void InsertSort(int A[], int n) {
int i, j, temp;
for (i = 1; i < n; i++) { // 从第2个元素开始
if (A[i] < A[i-1]) { // 若小于前驱,需插入
temp = A[i]; // 暂存当前元素
for (j = i-1; j >= 0 && A[j] > temp; --j) {
A[j+1] = A[j]; // 大于temp的元素后移
}
A[j+1] = temp; // 插入
}
}
}分析:
- 最好情况:
(已是有序,只需比较不移动)。 - 最坏情况:
(逆序)。 - 稳定性:稳定(因为
A[j] > temp才移动,相等时不移动)。
1.2 希尔排序 (Shell Sort)
核心思想: 又称“缩小增量排序”。是直接插入排序的优化版。通过将数组按增量 d 分组,对每组进行直接插入排序,使数组“基本有序”,最后 d=1 时再进行一次直接插入。
为什么比直接插入快? 直接插入在基本有序时效率极高。希尔排序前期移动步长长,能快速消除逆序对;后期步长短但数组已基本有序,移动次数少。
过程演示: 数组 [8, 9, 1, 7, 2, 3, 5, 4, 6, 0],
- d=5 (分5组:
8,3,9,5,1,4,7,6,2,0):- 组内排序后 ->
[3, 5, 1, 6, 0, 8, 9, 4, 7, 2]
- 组内排序后 ->
- d=2 (分2组:
3,1,0,9,7,5,6,8,4,2):- 组内排序后 ->
[0, 2, 1, 4, 3, 5, 7, 6, 9, 8]
- 组内排序后 ->
- d=1 (整体直接插入):
- 微调 ->
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
- 微调 ->
分析:
- 复杂度:依赖于增量序列,平均约
。 - 稳定性:不稳定(相同元素可能分在不同组,跳跃式移动导致相对位置改变)。
2. 交换排序类 (Exchange Sorts)
2.1 冒泡排序 (Bubble Sort)
核心思想: 两两比较相邻记录,如果反序则交换,直到没有反序的记录为止。每趟遍历将最大的元素“浮”到最后(或最小的“沉”到最前)。
优化点: 设置 flag 变量。若某一趟遍历中没有发生任何交换,说明已经有序,直接结束算法。
分析:
- 最好情况:
(有序,第一趟 flag未变直接退出)。 - 最坏情况:
。 - 稳定性:稳定。
2.2 快速排序 (Quick Sort) —— 最重要的排序算法
核心思想: 分治法 (Divide and Conquer)。
- 选基准 (Pivot):通常选第一个元素。
- 划分 (Partition):将比基准小的移到左边,比基准大的移到右边。基准归位。
- 递归:对左右两个子序列重复上述过程。
Partition 过程详解 (双指针法): 设基准 pivot = A[low],low 指向首,high 指向尾。
- High 向左找小:
while (low < high && A[high] >= pivot) high--;找到比 pivot 小的,移到low处 (A[low] = A[high])。 - Low 向右找大:
while (low < high && A[low] <= pivot) low++;找到比 pivot 大的,移到high处 (A[high] = A[low])。 - 重复直到
low == high,将pivot放入A[low]。
代码片段:
int Partition(int A[], int low, int high) {
int pivot = A[low]; // 选第一个为基准
while (low < high) {
while (low < high && A[high] >= pivot) high--;
A[low] = A[high];
while (low < high && A[low] <= pivot) low++;
A[high] = A[low];
}
A[low] = pivot;
return low; // 返回基准位置
}分析:
- 平均复杂度:
。 - 最坏复杂度:
。发生在有序或逆序时,基准每次都选到最大/最小,退化为冒泡。 - 空间复杂度:
(递归栈的高度)。 - 稳定性:不稳定(Partition 过程中的远距离交换)。
3. 选择排序类 (Selection Sorts)
3.1 简单选择排序 (Simple Selection Sort)
核心思想: 第
特点:
- 交换次数少:最多
次(优于冒泡)。 - 比较次数多:固定
次。 - 稳定性:不稳定。
- 例子:
[5, 8, 5, 2, 9]。第一趟,第一个5会和2交换,跑到第二个5后面。
- 例子:
3.2 堆排序 (Heap Sort)
核心思想: 利用堆(完全二叉树)这种数据结构。
- 大顶堆:
且 。用于升序排序。 - 小顶堆:用于降序排序。
算法步骤:
- 建堆:将无序数组构造成一个大顶堆。
- 从最后一个非叶子结点 (
) 开始,从右至左,从下至上进行“下沉”调整 ( HeapAdjust/sift_down)。
- 从最后一个非叶子结点 (
- 排序:
- 将堆顶元素(最大值)与堆底末尾元素交换。
- 将剩余
个元素重新调整为大顶堆。 - 重复直至堆大小为 1。
下沉 (sift_down) 逻辑: 判断节点 k,其左孩子 2k+1,右孩子 2k+2。 若孩子比父节点大,则将最大的孩子与父节点交换,并继续向下追踪被交换的孩子节点。
分析:
- 复杂度:
。建堆 ,调整 。 - 优势:在 Top K 问题(选出前 K 大)中表现极佳。
- 稳定性:不稳定。
4. 归并排序 (Merge Sort)
核心思想: 分治法。
- 分:将数组从中间切开,递归切分,直到长度为 1。
- 治 (Merge):将两个有序的子数组合并成一个有序数组。
Merge 过程: 需申请一个辅助数组 B[]。 比较左半区 A[i] 和右半区 A[j]:
- 若
A[i] <= A[j],将A[i]放入B,i++。 - 若
A[i] > A[j],将A[j]放入B,j++。 - 最后将剩余元素复制进
B,再把B拷回A。
分析:
- 复杂度:
。 - 空间复杂度:
(需要辅助数组,这是它最大的缺点)。 - 稳定性:稳定(Merge 时
A[i] <= A[j]优先取左边,保证相对位置不变)。
5. 基数排序 (Radix Sort)
核心思想: 不基于比较的排序。利用“分配”和“收集”。 通常针对非负整数。将整数按位数切割成不同的数字,按每个位数分别比较。
步骤 (LSD - Least Significant Digit):
- 按个位排序:准备 10 个桶 (0-9)。遍历数组,按个位数字放入对应桶。按顺序收集回来。
- 按十位排序:基于上一步结果,按十位放入桶,再收集。
- ...直至最高位。
分析:
- 复杂度:
。 :最大数字的位数(决定遍历几趟)。 :元素个数。 :基数(如十进制 r=10,决定桶的个数)。
- 稳定性:稳定(必须稳定,否则高位排序会打乱低位的顺序)。
6. 总结对比表
| 算法 | 平均时间 | 最好 | 最坏 | 空间 | 稳定性 | 关键词 |
|---|---|---|---|---|---|---|
| 直接插入 | 稳定 | 摸牌、越有序越快 | ||||
| 希尔 | - | 不稳定 | 增量分组 | |||
| 冒泡 | 稳定 | 交换、flag优化 | ||||
| 快速 | 不稳定 | Partition、分治 | ||||
| 简单选择 | 不稳定 | 选最小 | ||||
| 堆排序 | 不稳定 | 建堆、下沉 | ||||
| 归并 | 稳定 | 合并、空间换时间 |
