总览
索引
| 章 | 主题 | 英文 | 对应 Hot100 |
|---|---|---|---|
| 0 | 绪论与复杂度 | Intro & Complexity | 贯穿全部 |
| 1 | 线性表:数组与链表 | Array & Linked List | 数组、链表题 |
| 2 | 栈与队列 | Stack & Queue | 括号、单调栈、最小栈 |
| 3 | 哈希表 | Hash Table | 两数之和、异位词、连续序列 |
| 4 | 字符串基础技巧 | String Techniques | 子串、回文、前缀 |
| 5 | 树与二叉树 | Tree & Binary Tree | 遍历、路径、构造 |
| 6 | 二叉搜索树 | BST | 验证 BST、第 K 小 |
| 7 | AVL / 红黑树 / Splay | Balanced BSTs | 进阶理论 |
| 8 | 堆与优先队列 | Heap & PQ | TopK、合并 K 链表 |
| 9 | 多路搜索树与 Trie | B/B+ & Trie | 单词类扩展 |
| 10 | 图(含 DFS/BFS/Dijkstra 详解) | Graph | 岛屿、拓扑、BFS |
| 11 | 查找与排序概要 | Search & Sort | 二分、颜色分类 |
| 12 | 分治与主定理 | D&C & Master Theorem | 归并思想类 |
| 13 | 动态规划(重难点加强) | Dynamic Programming | 爬楼到编辑距离、背包 |
| 14 | 贪心与回溯 | Greedy & Backtracking | 跳跃、排列组合 |
| 15 | P 与 NP | Complexity Theory | 理论边界 |
| 16 | Hot100 知识点总表 | Hot100 Map | 全文检索 |
| 17 | 综合计算题 | Exercises | 考试向 |
| 18 | 专题补强 | Advanced Topics | 单调栈、最短路、选型 |
绪论
定义
数据结构(Data Structure):相互之间存在一种或多种特定关系的数据元素的集合,以及定义在该集合上的操作。
算法(Algorithm):求解一类问题的有限指令序列,须满足:有穷性、确定性、可行性、输入、输出。
抽象数据类型(ADT, Abstract Data Type):由数据对象、数据关系及一组基本操作构成的数学模型,强调“做什么”而非“怎么存”。
逻辑结构与存储结构
| 分类 | 英文 | 说明 |
|---|---|---|
| 集合结构 | Set | 元素仅同属一集合 |
| 线性结构 | Linear | 一对一:表、栈、队列 |
| 树形结构 | Tree | 一对多 |
| 图形结构 | Graph | 多对多 |
| 顺序存储 | Sequential | 连续内存,如数组 |
| 链式存储 | Linked | 指针/引用连接结点 |
| 索引存储 | Indexed | 附加索引表 |
| 散列存储 | Hashed | 由关键字直接定位 |
渐进记号
设 $f,g:\mathbb{N}\to\mathbb{R}^+$。
- $O$(上界):$f(n)=O(g(n))$ 当存在常数 $c>0,n_0$,使 $n\ge n_0\Rightarrow f(n)\le c\cdot g(n)$
- $\Omega$(下界):$f(n)=\Omega(g(n))$ 当 $g(n)=O(f(n))$
- $\Theta$(紧界):同时为 $O$ 与 $\Omega$
- $o$(严格上界):对任意 $c>0$,最终有 $f(n)<c\cdot g(n)$
- $\omega$(严格下界):对称定义
常见阶(由慢到快):$O(1)<O(\log n)<O(n)<O(n\log n)<O(n^2)<O(n^3)<O(2^n)<O(n!)$。
时间与空间复杂度
- 时间复杂度(Time Complexity):基本操作执行次数关于输入规模 $n$ 的量级,通常取最坏或平均情况。
- 空间复杂度(Space Complexity):算法额外占用的辅助空间量级(不含输入本身时称辅助空间)。
- 均摊分析(Amortized Analysis):对操作序列总代价求平均,常用聚合、记账、势能法(Splay、动态数组扩容)。
▸计算约定
比较、赋值、算术各计 $O(1)$;递归深度计入空间;哈希表平均 $O(1)$、最坏 $O(n)$(除非说明完美哈希或最坏保证结构)。
计算题:复杂度判定
▸例 0.1
下列代码的时间复杂度?
1
2
3
4for i = 1 to n: # 外层循环:i 取 1..n,共 n 轮
j = 1 # 每轮将 j 重置为 1
while j < n: # 内层:j 未达 n 则继续
j = j * 2 # j 每次翻倍 → 约 log2 n 次后退出
解: 外层 $n$ 次,内层 $j$ 每次翻倍,约 $\log_2 n$ 次,故 $\Theta(n\log n)$。
▸例 0.2
$T(n)=2T(n/2)+n$,$T(1)=1$。用展开法求闭式。
解: $T(n)=n+2\cdot(n/2)+4\cdot(n/4)+\cdots=n\log_2 n+T(1)\cdot n=\Theta(n\log n)$(亦可用主定理,见第 12 章)。
数组
定义
数组(Array):相同类型元素的连续存储序列,支持按下标 $O(1)$ 随机访问。
动态数组(Dynamic Array / Vector):可自动扩容的数组,均摊 $O(1)$ 尾部插入。
操作与复杂度
| 操作 | 平均 | 最坏 | 说明 |
|---|---|---|---|
| 按下标访问 | $O(1)$ | $O(1)$ | |
| 尾插(动态) | 均摊 $O(1)$ | $O(n)$ | 扩容时拷贝 |
| 任意位置插入/删除 | $O(n)$ | $O(n)$ | 需搬移元素 |
| 按值查找 | $O(n)$ | $O(n)$ | 无序 |
| 有序二分查找 | $O(\log n)$ | $O(\log n)$ |
代码:动态数组扩容思想
▸扩容思想
动态数组在元素个数达到容量时按倍数申请更大连续空间并拷贝旧数据;因扩容次数为对数级,连续尾插的均摊时间仍为 $O(1)$。
Python:
1 | class DynamicArray: |
C:
1 |
|
C++:
1 |
|
使用示例与 Hot100 映射
- 双指针(对撞/快慢):盛水容器、移动零、三数之和
- 前缀积/差分数组:除自身乘积
- 原地交换与环:旋转图像、下一个排列
- 二分边界:搜索旋转数组、查找首末位置
▸例 1.1 均摊分析
从空表连续 append $n$ 次,容量 $1,2,4,\ldots,2^{k}$($2^{k-1}<n\le 2^k$)。拷贝总次数 $<1+2+4+\cdots+2^{k-1}<n$,均摊每次 $O(1)$。
链表
定义
链表(Linked List):由结点通过指针串联的线性结构。结点含数据域与指针域。
- 单链表(Singly Linked List):仅后继指针
- 双向链表(Doubly Linked List):前驱+后继
- 循环链表(Circular Linked List):尾指向头
- 哨兵结点(Sentinel / Dummy):简化头尾边界处理
操作与复杂度
| 操作 | 单链表 | 说明 |
|---|---|---|
| 头插/头删 | $O(1)$ | |
| 按值/按下标查找 | $O(n)$ | |
| 已知结点后插入 | $O(1)$ | 需持有前驱才能删当前(单链表) |
| 删已知结点(双向) | $O(1)$ |
代码:单链表基本操作
▸链表操作要点
反转依赖「保存后继 → 改指前驱 → 三指针前移」;合并用哨兵简化空头;判环与找入口分别对应 Floyd 相遇与二次同速行走。
Python:
1 | class ListNode: |
C:
1 |
|
C++:
1 | struct ListNode { |
经典技巧(Hot100)
| 技巧 | 英文 | 题例 |
|---|---|---|
| 反转 | Reverse | 206 |
| 双指针合并 | Two-pointer merge | 21, 23 |
| 快慢指针判环 | Floyd cycle | 141, 142 |
| 双指针找交点 | Intersection | 160 |
| 前后指针删倒数第 N | Remove Nth | 19 |
| 模拟加法 | Digit add | 2 |
▸例 1.2 环入口证明要点
设头到环入口 $a$,入口到相遇点 $b$,环长 $c$。快慢相遇时慢走 $a+b$,快走 $a+b+kc$,且快走路程为慢的 2 倍:$a+b+kc=2(a+b)\Rightarrow a=kc-b$。故从头与从相遇点同速走,必在入口相遇。
栈
定义
栈(Stack):后进先出(LIFO, Last In First Out)的线性结构。仅在栈顶进行插入(push)与删除(pop)。
操作与复杂度
| 操作 | 时间 | 空间(整体) |
|---|---|---|
| push / pop / top | $O(1)$ | $O(n)$ 存 $n$ 个元素 |
| 判空 | $O(1)$ |
代码
▸栈与最小栈
普通栈仅维护一端;最小栈用同步辅助栈在每次 push/pop 时更新「当前栈内最小值」,使 getMin 为 $O(1)$。
Python:
1 | class Stack: |
C:
1 |
|
C++:
1 |
|
应用
- 括号匹配(Valid Parentheses)
- 表达式求值(中缀转后缀 + 后缀求值)
- 单调栈(Monotonic Stack):每日温度、柱状图最大矩形、接雨水
- 函数调用 / DFS 显式栈
- 浏览器前进后退、撤销
▸例 2.1 单调栈:下一个更大元素
数组 $[2,1,2,4,3]$,从右往左维护递减栈。对每个下标弹掉 $\le$ 当前值的栈顶,栈顶即为右侧第一个更大者;再压入当前。时间 $O(n)$,每个元素至多入出栈一次。
队列
定义
队列(Queue):先进先出(FIFO, First In First Out)。队尾入队(enqueue),队头出队(dequeue)。
双端队列(Deque):两端均可进出。
优先队列(Priority Queue):按优先级出队,常用堆实现(见第 8 章)。
循环队列(Circular Queue):数组实现时用模运算避免假溢出。
操作与复杂度
| 操作 | 时间 |
|---|---|
| enqueue / dequeue / front | $O(1)$(循环数组或链表) |
代码:循环队列
▸循环队列
用定长数组加 head/tail 与模运算复用槽位;size 区分「空」与「满」,避免仅靠头尾相等时的歧义。
Python:
1 | class CircularQueue: |
C:
1 | /* 循环队列控制块:缓冲由调用方分配并挂到 a */ |
C++:
1 |
|
Hot100 相关
- BFS 层序:二叉树层序、岛屿、单词接龙
- 双端队列优化:滑动窗口最值(扩展)
哈希表
定义
哈希表(Hash Table / Hash Map):通过哈希函数 $h(k)$ 将关键字映射到桶(bucket),实现接近 $O(1)$ 的查找、插入、删除。
哈希函数(Hash Function):将关键字映射到 ${0,1,\ldots,m-1}$。理想情况接近均匀随机。
装填因子(Load Factor) $\alpha=n/m$(元素数/桶数)。过大时需再散列(rehash)。
冲突处理
| 方法 | 英文 | 要点 |
|---|---|---|
| 拉链法 | Chaining | 每桶一条链表/树;平均链长 $\alpha$ |
| 线性探测 | Linear Probing | $(h+i)\bmod m$ |
| 二次探测 | Quadratic Probing | $(h+i^2)\bmod m$ |
| 双重哈希 | Double Hashing | $(h_1+i\cdot h_2)\bmod m$ |
成功查找平均(拉链,均匀假设):$1+\alpha/2$ 量级;开放定址依赖 $\alpha$,需 $\alpha<1$。
代码:拉链法示意
▸拉链法哈希
关键字经哈希映射到桶下标;冲突时在同一桶的链表(此处用列表)中顺序查找、更新或头插/尾插。
Python:
1 | class HashMap: |
C:
1 |
|
C++:
1 |
|
Hot100 映射
| 题 | 模式 |
|---|---|
| 1 两数之和 | 值→下标 |
| 49 字母异位词分组 | 排序键或计数元组→列表 |
| 3 无重复最长子串 | 滑动窗口 + 字符上次下标 |
| 128 最长连续序列 | 集合判起点 |
| 347 TopK 高频 | 计数 + 堆/桶 |
▸例 3.1
$m=5$,$h(k)=k\bmod 5$,插入 $12,22,7,32$。拉链后桶 $2$:$12\to 22\to 32$,桶 $2$ 另有 $7$($7\bmod 5=2$)。查找 $32$ 最多比较 3 次;$\alpha=4/5$。
字符串技巧
与 Hot100「字符串 / 字符串处理」对应的核心范式(结构仍依赖数组、哈希、双指针、DP)。
| 范式 | 英文 | 代表题 |
|---|---|---|
| 滑动窗口 | Sliding Window | 3 |
| 中心扩展 / DP | Palindrome | 5 |
| 排序或计数作键 | Anagram key | 49 |
| 栈 | Stack | 20 |
| 双指针反转 | Two pointers | 344, 151 |
| 纵向扫描 | LCP | 14 |
最长回文子串(中心扩展)复杂度: 中心 $2n-1$ 个,每次 $O(n)$,总 $O(n^2)$,空间 $O(1)$。
▸中心扩展
每个下标作为奇回文中心,每对相邻下标作为偶回文中心;向两侧扩展至字符不等,取所有扩展结果中最长者。
Python 中心扩展骨架:
1 | def longest_palindrome(s): |
树与二叉树
定义
树(Tree):有限结点集,满足:有且仅有一个根;其余结点分为互不相交的子树。
二叉树(Binary Tree):每个结点至多两个子结点,区分为左孩子与右孩子(有序)。
满二叉树(Full / Proper):每个结点 0 或 2 个孩子(教材定义不一,考研常指:深度为 $k$ 且有 $2^k-1$ 个结点的树为满二叉树)。
完全二叉树(Complete Binary Tree):深度为 $h$ 时,前 $h-1$ 层满,第 $h$ 层结点靠左连续排列。
平衡二叉树(Height-Balanced):任意结点左右子树高度差绝对值不超过某常数(AVL 为 1)。
基本性质
设二叉树结点数 $n$,边数 $e$,叶结点数 $n_0$,度为 2 结点数 $n_2$:
- $e=n-1$
- $n_0=n_2+1$(二叉树)
- 深度为 $h$ 的二叉树至多 $2^h-1$ 个结点(根深度计 1 时)
- 具有 $n$ 个结点的完全二叉树深度为 $\lfloor\log_2 n\rfloor+1$
- 完全二叉树下标(根为 1):左孩子 $2i$,右孩子 $2i+1$,父 $\lfloor i/2\rfloor$
存储
- 顺序存储:适合完全二叉树(堆)
- 二叉链表:
left/right(可加parent) - 线索二叉树(Threaded):空指针指向中序前驱/后继
遍历
| 遍历 | 英文 | 顺序 |
|---|---|---|
| 先序 | Preorder | 根-左-右 |
| 中序 | Inorder | 左-根-右 |
| 后序 | Postorder | 左-右-根 |
| 层序 | Level-order | BFS |
由先序+中序或后序+中序可唯一还原二叉树(Hot100-105)。
代码:结点与遍历
▸遍历骨架
递归版把“空树返回空序列”作为基准情形;迭代中序用栈模拟沿左链下探;层序用队列按层弹出,同层结点个数用当轮 len(q) 固定。对称判定把左右子树当作镜像对一起比较。
Python:
1 | from collections import deque # 双端队列:层序遍历需要 O(1) 队首弹出 |
C:
1 | /* 二叉链表结点:值 + 左右孩子指针(前向声明式自引用结构体) */ |
C++:
1 | // 与 LeetCode 风格一致的二叉树结点 |
Hot100 二叉树题型归类
| 类型 | 题号要点 |
|---|---|
| 遍历 | 94 中序,102 层序,103 锯齿,199 右视图,637 层均值,987 垂直 |
| 深度/直径 | 104 最大深,111 最小深,543 直径 |
| 路径 | 112/113 路径和,124 最大路径和,257 所有路径,563 坡度 |
| 结构判定 | 101 对称,993 堂兄弟,662 最大宽度 |
| BST | 98 验证,108 有序数组建树,230 第 K 小 |
| 构造/序列化 | 105 前中构树,297 序列化,116 next 指针 |
| LCA | 236 最近公共祖先 |
▸例 5.1
$n$ 个结点的二叉树,高度最低为多少?
解: 完全(或满)时最低,高度 $\Theta(\log n)$;最坏退化为链,高度 $\Theta(n)$。
二叉搜索树
定义
二叉搜索树(BST, Binary Search Tree):二叉树,对任意结点,左子树一切关键字 $<$ 该结点,右子树一切关键字 $>$ 该结点(或 $\le/\ge$ 依约定,通常禁止重复或规定一侧)。
中序遍历得递增序列。
操作与复杂度
| 操作 | 平均 | 最坏(退化链) |
|---|---|---|
| 查找/插入/删除 | $O(\log n)$ | $O(n)$ |
| 找 min/max | $O(h)$ | $O(n)$ |
| 建树(随机插入) | 期望高度 $O(\log n)$ |
代码
▸BST 不变量
对任意结点,左子树关键字均小于该结点、右子树均大于该结点。查找/插入沿比较链下降;删除有孩子时常用右子树最小结点(中序后继)顶替后再删后继。
Python:
1 | def bst_search(root, key): |
C:
1 | /* 递归插入:在空指针处 malloc 新结点并返回给父指针赋值 */ |
C++:
1 |
|
▸例 6.1
依次插入 $5,3,7,2,4,6,8$ 得较平衡形态;若插入顺序为 $1,2,3,4,5$ 则退化为右链,查找 $O(n)$。故需平衡 BST。
AVL 树
定义
AVL 树(Adelson-Velsky and Landis Tree):任一结点左右子树高度差的绝对值 $\le 1$。平衡因子 $bf=h_L-h_R\in{-1,0,1}$。
高度 $h$ 满足与 Fibonacci 相关下界,故 $h=O(\log n)$。
旋转
| 失衡类型 | 英文 | 操作 |
|---|---|---|
| LL | Left-Left | 右旋 |
| RR | Right-Right | 左旋 |
| LR | Left-Right | 先左旋再右旋 |
| RL | Right-Left | 先右旋再左旋 |
代码:旋转与插入
▸AVL 旋转
每个结点维护高度 h。插入后自下而上更新;若平衡因子绝对值大于 1,按 LL/RR 单旋或 LR/RL 双旋恢复。右旋以左孩子为新根,左旋以右孩子为新根;先更新原子树根高度,再更新新根。
Python:
1 | class AVLNode: |
C: 结构体增加 int h;,旋转逻辑与上相同(指针版)。
C++:
1 | // 标准库不提供现成 AVL;教学可手写旋转,工程有序表多用红黑树实现的 map/set |
复杂度
查找/插入/删除最坏均为 $O(\log n)$。
红黑树
定义
红黑树(Red-Black Tree):自平衡 BST,结点着红或黑,满足:
- 每个结点非红即黑
- 根为黑
- 每个叶(NIL)为黑
- 红结点两孩子必黑(无相邻红)
- 任一结点到其后代 NIL 的简单路径黑结点数相同(黑高)
结论:$h\le 2\log_2(n+1)$,操作 $O(\log n)$。
与 AVL 对比
| AVL | 红黑树 | |
|---|---|---|
| 平衡 | 更严 | 略松 |
| 插入 | 至多 2 次旋转 | 旋转+变色 |
| 删除 | 可能多次旋转 | 工程上常更优 |
| 应用 | 对查找极敏感场景 | std::map、TreeMap、CFS |
插入修复概要
新结点着红;若父红,视叔结点:叔红则变色上移;叔黑则按形态旋转改色。
代码
▸红黑结点
新插入结点通常先着红色以少破坏黑高;nil 哨兵代表空叶且为黑。完整插入/删除还需旋转与 fixup(变色),此处仅给出结点结构与 BST 式查找。
Python(结点与查找):
1 | BLACK, RED = 0, 1 # 颜色枚举:黑=0,红=1 |
C++:
1 |
|
C: 教学实现需完整旋转与 fixup;内核有现成 RB 实现可参考。
▸黑高
黑高 $bh$ 的子树至少 $2^{bh}-1$ 个内部结点;又 $bh\ge h/2$,故 $h=O(\log n)$。
▸例 7.1
依次插入 $1..n$ 不会退化成链:变色与旋转维持对数高度。
Splay 树
定义
伸展树(Splay Tree):经 Splay 将访问结点旋至根的自调整 BST,不存平衡因子,利用访问局部性。
伸展
| 情形 | 名称 | 操作 |
|---|---|---|
| 父为根 | Zig | 单旋 |
| 与父同侧 | Zig-Zig | 先祖后父(顺序异于 AVL) |
| 与父异侧 | Zig-Zag | 先父后祖 |
复杂度
单次最坏 $O(n)$;均摊 $O(\log n)$(势能 $\Phi=\sum\log size(v)$)。
代码骨架
▸Splay 单旋
伸展通过一系列旋转把访问结点抬到根。下列 rotate_right_splay 是带父指针的右旋:更新孩子、父、以及整棵树的 root 引用。Zig / Zig-Zig / Zig-Zag 由多次此类旋转组合而成。
Python:
1 | def rotate_right_splay(root, y): |
C / C++: 竞赛中常手写 splay 维护序列(分裂、合并、区间翻转)。
堆与优先队列
定义
堆(Heap):满足堆序性质的完全二叉树。
- 大根堆(Max-Heap):父 $\ge$ 子
- 小根堆(Min-Heap):父 $\le$ 子
优先队列(Priority Queue):每次取出优先级最高(或最低)元素的 ADT,常用二叉堆实现。
数组下标从 $0$ 起:左 $2i+1$,右 $2i+2$,父 $\lfloor(i-1)/2\rfloor$。
操作与复杂度
| 操作 | 时间 | 说明 |
|---|---|---|
| 找最值 | $O(1)$ | 堆顶 |
| 插入 push | $O(\log n)$ | 上浮 sift-up |
| 删除堆顶 pop | $O(\log n)$ | 下沉 sift-down |
| 建堆 heapify | $O(n)$ | Floyd:自底向上 |
| 堆排序 | $O(n\log n)$ | 原地,不稳定 |
代码:小根堆
▸二叉堆下标
数组下标从 0 起:左孩子 2i+1,右孩子 2i+2,父 (i-1)//2。上浮修复插入,下沉修复删除堆顶;Floyd 建堆从最后一个非叶向下沉,总复杂度 $O(n)$。
Python:
1 | import heapq # 标准库:默认小根堆;heappush / heappop / heapify |
C:
1 | /* 小根堆下沉:在长度为 n 的数组 a 上,从下标 i 开始恢复堆序 */ |
C++:
1 |
|
使用示例
1 | # 合并 K 个有序链表(Hot100-23): |
▸例 8.1 建堆为何 $O(n)$
高度为 $h$ 的层约 $n/2^{h+1}$ 个结点,每个下沉 $O(h)$。$\sum_{h=0}^{\log n} (n/2^{h+1})\cdot h = O(n)$。
▸例 8.2
数组 $[4,1,3,2,16,9,10,14,8,7]$ 建大根堆后堆顶为 $16$,再连续 pop 得降序,即堆排序。
Hot100
215 第 K 大;347 TopK 高频;23 合并 K 链表。
哈夫曼树与 Trie
哈夫曼树
哈夫曼树(Huffman Tree):带权路径长度 $WPL=\sum w_i l_i$ 最小的二叉树。构造:每次取最小两棵合并。用于前缀编码(无歧义)。
贪心正确性:最优前缀码中频率最低两字符必为最深兄弟叶。
复杂度:用堆维护,$O(n\log n)$。
Trie(字典树)
Trie / Prefix Tree:每边一个字符,从根到结点表示前缀。插入/查询 $O(L)$($L$ 为串长)。空间与字符集与结点总数相关。
▸Trie 路径
从根出发,每个字符对应一条边;结点的 end 标记“是否有单词在此结束”。search 要求走到终点且 end 为真;startsWith 只需路径存在。
Python:
1 | class TrieNode: |
C++: 子结点数组 TrieNode* next[26];C 同理用结构体数组。
B 树与 B+ 树
定义
B 树(B-Tree):多路平衡查找树,阶为 $m$ 时:根至少 2 子(除非叶);其余内部结点 $[ \lceil m/2\rceil, m]$ 个子;所有叶同层;结点内关键字有序。
B+ 树(B+ Tree):关键字全在叶层有序链表;内部结点仅作索引。磁盘/数据库聚簇索引主流结构。
查找 $O(\log_m n)$ 次 IO(树高低)。
图
定义
图(Graph) $G=(V,E)$:顶点集与边集。
| 术语 | 英文 |
|---|---|
| 有向/无向 | Directed / Undirected |
| 权 | Weight |
| 度/入度/出度 | Degree / In / Out |
| 路径/回路 | Path / Cycle |
| 连通/强连通 | Connected / Strongly connected |
| 稀疏/稠密 | Sparse / Dense |
| DAG | Directed Acyclic Graph |
存储
| 结构 | 空间 | 适场景 |
|---|---|---|
| 邻接矩阵 | $O(V^2)$ | 稠密、判边 $O(1)$ |
| 邻接表 | $O(V+E)$ | 稀疏(常用) |
遍历
- DFS(Depth-First Search):深度优先,$O(V+E)$
- BFS(Breadth-First Search):广度优先,最短路(无权),$O(V+E)$
代码:邻接表 DFS/BFS
▸图遍历与应用
graph 为邻接表(如 defaultdict(list))。DFS 递归标记连通可达集;BFS 用队列得层次序。岛屿题把网格当隐式图,淹没 '1' 以免重复计数。课程表用 Kahn 拓扑:入度为 0 入队,若最终取出数等于顶点数则无环。
Python:
1 | from collections import defaultdict, deque |
C:
1 |
|
C++:
1 |
|
DFS 详解
深度优先搜索(Depth-First Search, DFS):从起点沿一条路径尽量深入,直至无法前进再回溯;用递归栈或显式栈实现。
性质与复杂度
| 项目 | 说明 |
|---|---|
| 时间 | 邻接表 $\Theta(V+E)$;邻接矩阵 $\Theta(V^2)$ |
| 空间 | 递归/栈最坏 $O(V)$(链状);另加 vis 为 $O(V)$ |
| 输出 | 遍历序、连通分量、环、拓扑序、路径存在性等 |
| 无权最短路 | 不能直接当最短路(应用 BFS) |
时间戳:disc[u] 首次发现时刻,fin[u] 结束处理时刻。有向图边分类:树边、前向边、后向边(成环)、横向边。
逐步示例
▸DFS 走图
无向图:$0-1,\ 0-2,\ 1-3,\ 2-3$。从 $0$ 出发,邻接表按编号升序。
访问:$0\to 1\to 3\to 2$(由 3 到 2),回溯后 $0$ 的邻居 $2$ 已访问。遍历序示例:$0,1,3,2$。
栈深度峰值与路径长度相关;vis 保证每点入栈/递归一次。
递归与显式栈(三语言)
▸递归 DFS 与显式栈
递归版进入即标记并记录序;迭代版用栈,对邻接表逆序压栈可使弹出顺序贴近递归。无向判环需记录父结点以免把回边当成树边误判。有向拓扑用三色:灰表示递归栈上,再遇灰即为后向边成环;完结后追加,最后整体反转得拓扑序。
Python:
1 | from collections import defaultdict |
C:
1 |
|
C++:
1 |
|
应用对照
| 应用 | 做法 | Hot100 |
|---|---|---|
| 连通块 / 岛屿 | 每未访点开一次 DFS | 200 |
| 克隆图 | DFS/BFS + 哈希映射旧→新 | 133 |
| 判环 | 无向看父;有向三色 | 207(亦可用 Kahn) |
| 网格搜索 | 四向/八向递归 | 79 单词搜索(回溯式 DFS) |
BFS 详解
广度优先搜索(Breadth-First Search, BFS):按距起点的层数逐层扩展,队列实现。在边权均为 1(或无权)的图上,首次到达即为最短路。
性质与复杂度
| 项目 | 说明 |
|---|---|
| 时间 | 邻接表 $O(V+E)$;网格 $O(RC)$ |
| 空间 | 队列最坏 $O(V)$ |
dist[v] |
边数意义下的最短距离 |
parent[v] |
可回溯最短路径 |
逐步示例
▸BFS 分层
图:$0-1,\ 0-2,\ 1-3,\ 2-3,\ 3-4$。从 $0$ 出发。
- 第 0 层:$0$($dist=0$)
- 第 1 层:$1,2$($dist=1$)
- 第 2 层:$3$($dist=2$)
- 第 3 层:$4$($dist=3$)
$0\to 4$ 最短边数为 3。若用 DFS 可能先走出更长路径,故无权最短路必须用 BFS(或 0-1 BFS / Dijkstra)。
三语言完整实现(含最短路与网格)
▸无权最短路
边权均为 1 时,BFS 首次到达即为最少边数。dist 记距离,parent 可回溯路径。网格与单词接龙把状态空间建成隐式图,同样用队列扩展。
Python:
1 | from collections import deque |
C:
1 |
|
C++:
1 |
|
BFS vs DFS 选型
| 目标 | 选用 |
|---|---|
| 无权最短路 / 最少步数 | BFS |
| 连通性、任意路径、拓扑、强连通预备 | DFS 常更自然 |
| 层序、按距离分层处理 | BFS |
| 回溯构造(排列、棋盘) | DFS(搜索树) |
Dijkstra 详解
Dijkstra 算法:计算非负权图单源最短路。维护 $dist[v]$,反复取出当前 $dist$ 最小的未确定顶点 $u$,对邻边做松弛(relax):若 $dist[v] > dist[u]+w(u,v)$ 则更新。
正确性前提
边权 $w\ge 0$。若存在负权,小顶堆“已确定”性质失效,应改用 Bellman-Ford 或 SPFA(注意 SPFA 最坏较差)。
两种实现
| 实现 | 时间 | 适用 |
|---|---|---|
| 朴素:$V$ 次扫最小 | $O(V^2+E)$ | 稠密图 |
二叉堆 / priority_queue |
$O((V+E)\log V)$ | 稀疏图(常用) |
| Fibonacci 堆(理论) | $O(E+V\log V)$ | 少见实践 |
允许堆中同一顶点多条目,弹出时若 $d>dist[u]$ 则跳过(懒删除)。
逐步计算题
▸Dijkstra 手算
顶点 $0..4$,有向边($u\to v:w$):$0\to1:2,\ 0\to2:5,\ 1\to2:1,\ 1\to3:2,\ 2\to3:1,\ 2\to4:4,\ 3\to4:2$。源点 $0$。
- 初值:$dist=[0,\infty,\infty,\infty,\infty]$,取出 $0$
- 松弛:$dist[1]=2,\ dist[2]=5$
- 取 $1$($d=2$):$dist[2]=\min(5,2+1)=3$,$dist[3]=4$
- 取 $2$($d=3$):$dist[3]=\min(4,3+1)=4$,$dist[4]=7$
- 取 $3$($d=4$):$dist[4]=\min(7,4+2)=6$
- 取 $4$。最终 $dist=[0,2,3,4,6]$
路径例:$0\to1\to2\to3\to4$ 或 $0\to1\to3\to4$,长度均为 6。
三语言实现
Python(堆优化 + 路径还原):
1 | import heapq |
▸堆优化与稠密版对照
堆版用小顶堆反复取当前最近点,边松弛时压入新距离条目,过期条目靠 d > dist[u] 跳过。稠密版不用堆,每轮线性扫描未确定点中 dist 最小者,时间 $O(V^2)$,边多时往往更快。parent 仅堆版维护,沿前驱回溯可得一条最短路径。
C(稠密图 $O(V^2)$):
1 |
|
▸C 稠密 Dijkstra
全局数组存放矩阵与结果,调用前须将无边位置填为 INF。0x3f3f3f3f 约为 $10^9$,两数相加仍落在 32 位有符号范围内,适合竞赛写法。逻辑与 Python dijkstra_dense 一致。
C++(堆优化):
1 |
|
▸C++ `priority_queue` 写法
默认 priority_queue 是大顶堆,须以 greater<> 改为小顶堆。允许同一顶点多次入堆,用 d > dist[u] 过滤陈旧记录,避免在堆中做 decrease-key。与 Python heapq 版同一算法骨架。
与其它最短路对比
| 算法 | 负权 | 单源/全源 | 典型复杂度 |
|---|---|---|---|
| BFS | 权须为 1 | 单源 | $O(V+E)$ |
| Dijkstra | 不可负 | 单源 | $O((V+E)\log V)$ |
| Bellman-Ford | 可,能判负环 | 单源 | $O(VE)$ |
| Floyd | 可(无负环) | 全源 | $O(V^3)$ |
▸专题补强中的 Dijkstra
后文「专题补强」仍保留另一份堆优化代码,可与本节对照;本节含手算、稠密版与路径还原,更完整。
### 经典算法复杂度
| 算法 | 用途 | 时间 |
|---|---|---|
| Kahn / DFS 拓扑 | DAG 序、判环 | $O(V+E)$ |
| Kruskal | MST | $O(E\log E)$ + 并查集 |
| Prim | MST | 二叉堆 $O(E\log V)$ |
| Dijkstra | 非负权最短路 | 堆 $O(E\log V)$ |
| Bellman-Ford | 负权、判负环 | $O(VE)$ |
| Floyd-Warshall | 全源 | $O(V^3)$ |
并查集(Union-Find)
路径压缩 + 按秩合并,均摊近乎 $O(\alpha(n))$。
1 | class DSU: |
▸路径压缩与按秩合并
find 的路径压缩把查找路径打平;union 按秩把较矮树挂到较高树下。二者合用时,单次操作均摊复杂度接近 $O(\alpha(n))$(阿克曼反函数)。union 返回 False 表示两点原先已连通,Kruskal 判环即依赖此语义。
Hot100 图论
200 岛屿;207 课程表(拓扑);133 克隆图;127 单词接龙(BFS)。
查找与排序概要
查找
| 方法 | 平均 | 最坏 | 前提 |
|---|---|---|---|
| 顺序查找 | $O(n)$ | $O(n)$ | 无 |
| 二分查找 | $O(\log n)$ | $O(\log n)$ | 有序随机访问 |
| 分块查找 | $O(\sqrt{n})$ 量级 | 块有序 | |
| 哈希 | $O(1)$ | $O(n)$ | |
| BST/平衡树 | $O(\log n)$ | $O(\log n)$ 平衡时 |
二分模板(找左边界):
1 | def lower_bound(a, target): |
▸左边界二分语义
维护不变量:答案始终落在 [lo, hi)。a[mid] < target 时丢弃左半;否则丢弃严格大于 mid 的右半,并保留 mid 作为候选。循环结束时 lo 即第一个不小于 target 的下标,与 C++ std::lower_bound 语义一致。
Hot100:33 旋转数组搜索;34 首末位置;240 二维矩阵搜索。
排序
详见《排序算法详解》。比较排序下界 $\Omega(n\log n)$(决策树)。稳定排序:插入、归并、冒泡、计数等;不稳定:快排、堆排、希尔、选择。
Hot100:75 颜色分类(荷兰国旗三路划分,$O(n)$);88 合并有序数组。
分治与主定理
定义
分治(Divide and Conquer):将问题分成若干规模更小的同类子问题,递归求解,再合并结果。典型:归并排序、快速排序、二分、最大子数组(分治版)、Strassen 矩阵乘。
一般递推:
$$T(n)=a,T(n/b)+f(n),\quad a\ge 1,\ b>1$$
其中 $a$ 为子问题个数,$n/b$ 为子问题规模,$f(n)$ 为分治与合并代价。
主定理(Master Theorem)
令 $n^{\log_b a}$ 为临界指数。比较 $f(n)$ 与 $n^{\log_b a}$:
| 情形 | 条件 | 结论 |
|---|---|---|
| 1 | $f(n)=O(n^{\log_b a-\varepsilon})$,$\varepsilon>0$ | $T(n)=\Theta(n^{\log_b a})$ |
| 2 | $f(n)=\Theta(n^{\log_b a}\log^k n)$,$k\ge 0$ | $k=0$:$T=\Theta(n^{\log_b a}\log n)$;一般 $T=\Theta(n^{\log_b a}\log^{k+1}n)$ |
| 3 | $f(n)=\Omega(n^{\log_b a+\varepsilon})$ 且正则性 $a f(n/b)\le c f(n)$,$c<1$ | $T(n)=\Theta(f(n))$ |
(教材对情形 2 常写 $f=\Theta(n^{\log_b a})$ 则 $T=\Theta(n^{\log_b a}\log n)$。)
经典例子
| 算法 | 递推 | $\log_b a$ | 结果 |
|---|---|---|---|
| 二分查找 | $T=T(n/2)+O(1)$ | $0$ | $\Theta(\log n)$ |
| 归并排序 | $T=2T(n/2)+O(n)$ | $1$ | $\Theta(n\log n)$ |
| 朴素递归斐波那契(坏) | $T=T(n-1)+T(n-2)+O(1)$ | 非主定理形 | $\Theta(\phi^n)$ |
| 草莓矩阵乘 Strassen | $T=7T(n/2)+O(n^2)$ | $\log_2 7$ | $\Theta(n^{\log_2 7})$ |
代码:分治最大子数组
Python:
1 | def max_crossing(a, lo, mid, hi): |
▸分治最大子数组
最优子段要么全在左、要么全在右、要么跨越中点。跨越情形用两次线性扫描合并,单层 $O(n)$;递推符合主定理情形 2,总时间 $\Theta(n\log n)$。若只需最大和,Kadane 动态规划可达线性时间。
C:
1 |
|
▸C 版与 Python 对照
算法步骤一一对应:max_crossing 两次单向扫描,max_subarray 三路取最大。INT_MIN 对应 Python 的 float('-inf');全为负时仍能正确返回最大的单个元素(由边界与扫描共同保证)。
C++: 与 C 相同逻辑,可用 std::numeric_limits<int>::min() 与 std::max。
▸例 12.1
$T(n)=4T(n/2)+n^2$。$\log_2 4=2$,$f(n)=n^2=\Theta(n^{\log_b a})$,情形 2,$T=\Theta(n^2\log n)$。
▸例 12.2
$T(n)=2T(n/2)+n^2$。$\log_2 2=1$,$f=n^2=\Omega(n^{1+\varepsilon})$,且 $2(n/2)^2=n^2/2\le c n^2$,情形 3,$T=\Theta(n^2)$。
▸例 12.3
$T(n)=T(n/2)+1$ → $\Theta(\log n)$;$T(n)=9T(n/3)+n$ → $\log_3 9=2$,$f=O(n^{2-\varepsilon})$,情形 1,$T=\Theta(n^2)$。
动态规划
定义
动态规划(Dynamic Programming, DP):将问题划分为重叠子问题,通过填表(或记忆化)复用结果,并具有最优子结构。
步骤:刻画状态 → 转移方程 → 边界 → 计算顺序 → 复杂度。
与分治区别:分治子问题通常不重叠;DP 子问题高度重叠。
常见类型
| 类型 | 英文 | Hot100 代表 |
|---|---|---|
| 线性 DP | Linear | 70 爬楼,198 打家劫舍,53 最大子数组 |
| 背包/完全背包 | Knapsack | 322 零钱,279 完全平方 |
| 区间/序列 | LCS/LIS/Edit | 1143,300,72 |
| 股票类 | Stock | 121 |
| 字符串分割 | Word Break | 139 |
| 树形 DP | Tree DP | 124 最大路径和 |
经典方程与代码
爬楼梯 / 斐波那契型: $dp[i]=dp[i-1]+dp[i-2]$,$O(n)$ 时间 $O(1)$ 空间。
▸经典线性与双串 DP
下列实现把多道 Hot100 题压成滚动变量或二维表:爬楼与打家劫舍只保留常数个前驱状态;Kadane 用「以当前结尾」与全局最优双变量;零钱为完全背包最少件数;LIS 朴素为 $O(n^2)$;编辑距离与 LCS 同属双串填表模板。
Python:
1 | def climb(n): |
C:
1 | int climbStairs(int n) { |
C++:
1 | int coinChange(vector<int>& coins, int amount) { |
复杂度分析要点
状态数 $\times$ 单状态转移代价。编辑距离状态 $O(mn)$、转移 $O(1)$ → 时间 $O(mn)$、空间可压到 $O(\min(m,n))$。
▸例 13.1
背包容量 $W$,物品 $n$,0-1 背包二维 $O(nW)$;滚动数组压成一维时须逆序更新容量,防重复选用。
▸例 13.2
LIS:$O(n^2)$ 的 $dp[i]=$ 以 $i$ 结尾的最长;维护 tails 数组二分得 $O(n\log n)$。
适用条件与建模步骤
判定能否使用 DP 的常用清单:
- 最优子结构(Optimal Substructure):最优解包含子问题的最优解。
- 重叠子问题(Overlapping Subproblems):递归树中同一状态被反复求解。
- 能定义状态与转移,且无“后效性”(当前决策只依赖状态所概括的信息)。
建模固定五步:状态含义 → 转移方程 → 边界 → 计算顺序(拓扑)→ 答案位置/回溯方案。
记忆化 vs 递推
| 记忆化搜索(Top-down) | 递推填表(Bottom-up) | |
|---|---|---|
| 写法 | 递归 + @cache / 数组标记 |
循环按依赖填表 |
| 优点 | 只算到的状态、贴近公式 | 无栈溢出、易压空间 |
| 空间 | 递归栈 + 表 | 表(可滚动) |
Python 记忆化示例(爬楼):
▸记忆化搜索
@lru_cache(None) 把 (n,) 映射到返回值并缓存;同一 n 只真正计算一次。递归式直接对应转移 $f(n)=f(n-1)+f(n-2)$,与自底向上滚动变量等价,但依赖调用栈。
1 | from functools import lru_cache # 标准库:函数结果按参数哈希缓存 |
背包九讲要点(科班)
设容量 $W$,物品 $i$ 重量 $w_i$、价值 $v_i$。
0-1 背包
每件至多选一次:
$$dp[i][j]=\max(dp[i-1][j],\ dp[i-1][j-w_i]+v_i)\quad (j\ge w_i)$$
一维优化:for i; for j=W..w_i: dp[j]=max(dp[j], dp[j-w_i]+v_i)(逆序)。
完全背包
每件无限件:一维 正序 for i; for j=w_i..W,使 dp[j-w_i] 已含本物品。
多重背包
每件最多 $k_i$ 件:二进制拆分转 0-1,或单调队列优化。
Python(0-1 与完全):
▸一维背包循环方向
0-1 背包对容量逆序枚举,使 dp[j-w] 仍是「未选当前物品」的旧值,保证每件至多一次。完全背包正序枚举,使 dp[j-w] 已含当前物品,从而允许同件多次使用。
1 | def knapsack_01(W, w, v): |
C++:
1 | int knapsack01(int W, vector<int>& w, vector<int>& v) { |
C: 同理用一维数组 dp[W+1],注意循环边界。
▸背包填表
$W=5$,物品 $(w,v)=(2,3),(3,4),(4,5)$,0-1。
物品 1 后:$dp=[0,0,3,3,3,3]$
物品 2 后:$j=5\leftarrow \max(3,0+4)=4$;$j=3\leftarrow4$ → $[0,0,3,4,4,7]$
物品 3:$j=5\leftarrow\max(7,3+5)=8$?$5-4=1$ 处为 0,故 $\max(7,5)=7$;$j=4\leftarrow\max(4,5)=5$。
最终最优 7(物品 1+2)。若误用完全背包正序,可能重复选同一件,结果不同——务必分清模型。
Hot100 DP 逐题强化
下列与既有代码互补:补齐状态定义、边界与复杂度;未重复删除前文实现。
70 爬楼梯
- 状态:$dp[i]=$ 到达 $i$ 阶方法数
- 转移:$dp[i]=dp[i-1]+dp[i-2]$
- 边界:$dp[1]=1,dp[2]=2$
- 复杂度:$O(n)/O(1)$
121 买卖股票一次
- 维护历史最低价 $mn$,答案 $\max(price-mn)$
- 或 $dp[i][0]$ 持有、$dp[i][1]$ 未持有(通用股票 DP 框架的一维退化)
- $O(n)/O(1)$
53 最大子数组和(Kadane)
- $f[i]=\max(a[i],\ f[i-1]+a[i])$,答案 $\max f[i]$
- 全负时取最大元素;勿把 $f$ 初始化为 0
198 打家劫舍
- $dp[i]=\max(dp[i-1],\ dp[i-2]+nums[i])$
- 环形房屋(213)拆成「不偷第一」与「不偷最后」两次
279 完全平方数
- 完全背包:物品为 $1^2,2^2,\ldots$,求凑成 $n$ 的最少个数
- $dp[x]=\min_j dp[x-j^2]+1$,$O(n\sqrt{n})$
139 单词拆分
- $dp[i]=$ 前缀 $s[:i]$ 可否拆分
- $dp[0]=\mathrm{True}$;$dp[i]=\bigvee_{j<i}(dp[j]\land s[j:i]\in wordDict)$
- $O(n^2\cdot L)$ 量级(视字典实现)
Python:
▸单词拆分布尔 DP
dp[i] 表示前缀 s[:i] 能否由字典拼出。枚举分割点 j:若前缀已可拆且 s[j:i] 在字典中,则 dp[i] 为真并提前结束内层。字典转 set 使成员判定平均 $O(1)$。
1 | def word_break(s, wordDict): |
300 最长递增子序列 LIS
- $O(n^2)$:$dp[i]=1+\max{dp[j]:j<i,a[j]<a[i]}$
- $O(n\log n)$:
tails[k]=长度为 $k+1$ 的增子序列最小尾;二分更新
▸LIS 贪心加二分
tails 始终有序:对新元素 x,用 bisect_left 找第一个 $\ge x$ 的位置。若在末尾则扩展长度;否则用更小的 x 替换该位置尾,为后续更长序列留余地。len(tails) 即 LIS 长度(不必还原序列)。
1 | import bisect # 对有序序列做二分查找/插入位置 |
322 零钱兑换
- 完全背包最少件数;$dp[0]=0$,其余 $+\infty$;无解返回 $-1$
72 编辑距离
- $dp[i][j]=$ $a[:i]$ 与 $b[:j]$ 最小编辑
- 相等:$dp[i-1][j-1]$;否则 $1+\min($删,插,改$)$
- 空间可两行滚动
C++:
▸编辑距离二维表
边界:空串与另一串的距离等于长度(全删或全插)。字符相等时沿对角线继承;否则在删除、插入、替换三种代价中取最小并加一。min({...}) 为 C++11 初始化列表重载。
1 | int minDistance(string a, string b) { |
1143 最长公共子序列 LCS
- $a[i]=b[j]$:$dp[i-1][j-1]+1$;否则 $\max(dp[i-1][j],dp[i][j-1])$
- 与编辑距离同属「双串 DP」模板
124 树形 DP:最大路径和
- 对结点 $u$:向下贡献 $\max(0,\mathrm{gain}(left))+\max(0,\mathrm{gain}(right))+u.val$ 更新全局
- 向父返回:$u.val+\max(0,\mathrm{gain}(left),\mathrm{gain}(right))$
- 时间 $O(n)$
▸树形路径和
gain(u) 返回「从 u 出发向下单支」的最大贡献(负贡献截断为 0)。路径可以在 u 处「拐弯」同时吃左右,故用 u.val+L+R 刷新全局;返回父结点时只能选左右中较大的一支,否则路径无法成简单路径。
1 | def max_path_sum(root): |
区间 DP 与状态压缩(进阶)
区间 DP:枚举长度与左右端点,如矩阵链乘、戳气球、回文分割相关。
$$dp[l][r]=\min_{k} / \max_{k}, f(dp[l][k],dp[k][r],\ldots)$$
复杂度常 $O(n^3)$。
状压 DP:状态用比特表示子集,如旅行商 $dp[S][i]=$ 走过集合 $S$ 且位于 $i$ 的最短路,$O(n^2 2^n)$。
数位 DP:按数字位计数,记忆化 (pos, tight, ...),竞赛常用。
双串 / 网格 DP 模板(C)
▸C 语言 LCS 填表
使用静态二维数组避免大栈分配;!i || !j 对应空前缀边界。字符相等时对角线加一,否则取上方与左方较大者。返回 dp[m][n] 即为最长公共子序列长度。
1 | /* LCS 长度:与前文 Python LCS 同转移;a、b 为以 '\0' 结尾的 C 字符串 */ |
网格路径数(障碍): $dp[i][j]=dp[i-1][j]+dp[i][j-1]$(可走时),$O(RC)$。
常见陷阱
| 陷阱 | 说明 |
|---|---|
| 初始化错误 | 求 min 时非 0 位置应置 INF;Kadane 勿从 0 起若全负 |
| 0-1 / 完全循环方向 | 逆序防重复用;正序允许多次用 |
| 状态维数不够 | 股票「第几次交易」需加一维;环形房屋需拆分 |
| 后效性 | 状态未记录必要历史则转移错误 |
| 复杂度漏算转移 | LIS 朴素内层 $O(n)$ 不可省 |
DP 计算题
▸例 DP-1 填表:LCS
$a=\mathrm{ABCBDAB}$,$b=\mathrm{BDCABA}$。$dp[7][6]$ 为多少?
解: 经典结果为 $4$(如 BCBA / BDAB)。可按转移逐步填 $7\times 6$ 表验证。
▸例 DP-2 编辑距离
$a=\mathrm{kitten}$,$b=\mathrm{sitting}$。最小编辑?
解: $3$($k\to s$,$e\to i$,末尾 $+\mathrm{g}$)。
▸例 DP-3 完全背包
硬币 $[1,2,5]$,金额 $11$,最少枚数?
解: $5+5+1$ 共 3 枚;$dp[11]=3$。
▸例 DP-4 复杂度
区间 DP 枚举 $len=1..n$,$l=0..n-len$,$k=l..r$,总时间?
解: $\Theta(n^3)$。
▸例 DP-5 与分治对比
斐波那契递归无记忆:$T(n)=\Theta(\phi^n)$;DP/记忆化后?
解: 状态 $O(n)$、转移 $O(1)$,总 $\Theta(n)$。重叠子问题是关键。
贪心
定义
贪心(Greedy):每步选当前最优,期望全局最优。需证明:贪心选择性质 + 最优子结构(交换论证或成熟定理如拟阵)。
| 问题 | Hot100 | 要点 |
|---|---|---|
| 跳跃游戏 | 55/45 | 维护最远可达 |
| 合并区间 | 56/57 | 排序后线性合并 |
| 股票一次 | 121 | 维护历史最低 |
Python:
▸跳跃与区间合并
can_jump 维护最远可达下标,一旦当前下标越界则失败。jump 在「当前步覆盖区间」扫完时被迫迈下一步并刷新覆盖终点。merge_intervals 先按左端排序,再线性合并与末段相交的区间。
1 | def can_jump(nums): |
回溯
定义
回溯(Backtracking):在解空间树上 DFS,构造候选解,不合法则撤销(undo)。用于排列、组合、子集、棋盘搜索。
模板:path + 选择列表 + 结束条件;注意去重(排序后同层跳过)。
Python:
▸回溯三件套
共同模式:维护 path,递归前做选择、递归后撤销。全排列用 used 标记下标;子集用 start 保证元素相对次序且每层可跳过;组合总和允许重复选同一下标(dfs(i, ...)),候选先排序以便剪枝。
1 | def permute(nums): |
C / C++: 用数组 path[] 与 used[],递归参数传递 len;C++ 可用 vector 传引用并 pop_back。
Hot100:17 电话字母;22 括号生成;46 全排列;78/90 子集;39 组合总和;79 单词搜索;131 分割回文。
复杂度:排列 $O(n\cdot n!)$;子集 $O(n\cdot 2^n)$。
P 与 NP
定义
在判定问题(答案为是/否)框架下:
| 类 | 英文全称 | 含义 |
|---|---|---|
| P | Polynomial time | 存在确定性图灵机多项式时间算法 |
| NP | Nondeterministic Polynomial | “是”实例存在多项式长度证据且可在多项式时间验证 |
| NP-Hard | NP-困难 | 所有 NP 问题可多项式归约到该问题 |
| NP-Complete (NPC) | NP-完全 | 属于 NP 且为 NP-Hard |
关系:$P\subseteq NP$(是否 $P=NP$ 为公开问题)。若任一 NPC 属于 P,则 $P=NP$。
经典 NPC 问题
SAT / 3-SAT、哈密顿回路、团、顶点覆盖、图着色、旅行商判定版、子集和、背包判定版、精确覆盖等。
归约(Reduction)
若问题 $A$ 多项式时间归约到 $B$($A\le_p B$),则 $B$ 至少与 $A$ 一样难。证明 NPC:证在 NP + 某已知 NPC $\le_p$ 本问题。
应对策略
精确指数算法、近似算法、FPT、启发式、限制特殊图类。数据结构课程侧:理解“为何不用暴力搜索所有排列当 $n$ 大”,以及 DP/贪心适用边界。
▸例 15.1
最短路(非负)在 P(Dijkstra);最长简单路径为 NPC。二者仅“最长/最短”之别,复杂度天壤。
▸例 15.2
2-SAT $\in$ P(蕴含图强连通);3-SAT 为 NPC。
Hot100 知识点总表
按 《Leetcode Hot100 题解整理》 分类,映射到本文结构与核心知识点(检索用)。
数组与哈希
| 题 | 知识点 |
|---|---|
| 1 两数之和 | 哈希表一次遍历 |
| 15 三数之和 | 排序 + 双指针 + 去重 |
| 11 盛水容器 | 对撞双指针 |
| 3 无重复最长子串 | 滑动窗口 + 哈希下标 |
| 5 最长回文子串 | 中心扩展 / DP |
| 88 合并有序数组 | 双指针从后往前 |
| 283 移动零 | 快慢指针 |
| 448 消失的数字 | 原地哈希 / 下标标记 |
| 238 除自身乘积 | 前缀积 |
| 48 旋转图像 | 转置 + 翻转 |
| 31 下一个排列 | 邻项交换字典序 |
| 33 搜索旋转数组 | 二分 + 有序半区 |
| 34 查找首末位置 | 二分边界 |
| 75 颜色分类 | 三路划分 |
链表
206 反转;21 合并两表;141/142 环与入口;160 相交;19 删倒数 N;2 两数相加;23 合并 K 表(堆)。
字符串
20 括号(栈);49 异位词分组(哈希);14 LCP;344/151 反转。
动态规划
70,121,53,198,279,139,300,322,72,1143。
二叉树
94,104,101,102,108,98,236,124,105,103,199,297,230,113,116,257,111,112,543,637,993,563,662,987。
回溯
17,22,46,78,90,39,79,131。
贪心 / 矩阵
55,45,56,57,54,48,240,128。
图
200,207,133,127。
堆
215,347,23。
栈
20,155,739,84,42。
综合计算题
▸题 A 复杂度
1
2
3for i = 1..n: # 外层:i 从 1 到 n
for j = 1..i: # 中层:j 从 1 到 i,随 i 变长
for k = 1..100: work() # 内层固定 100 次常数工作
解: $100\cdot\sum_{i=1}^n i=\Theta(n^2)$。
▸题 B 主定理
$T(n)=3T(n/4)+n\log n$。$\log_4 3\approx 0.792$,$f(n)=n\log n=\Omega(n^{0.792+\varepsilon})$,需检验正则性;$a f(n/b)=3\cdot(n/4)\log(n/4)= (3n/4)\log(n/4)$,对大 $n$ 小于 $c n\log n$($c<1$),情形 3,$T=\Theta(n\log n)$。
▸题 C 二叉树性质
完全二叉树第 $i$ 结点(根为 1)的父、左、右?叶结点下标范围?
解: 父 $\lfloor i/2\rfloor$,左 $2i$,右 $2i+1$;叶从 $\lfloor n/2\rfloor+1$ 到 $n$。
▸题 D AVL
最少结点的高度为 3 的 AVL 树有多少结点?(根高计法:高度 0 单结点)
解: $n_h=1+n_{h-1}+n_{h-2}$,$n_0=1,n_1=2$,则 $n_2=4,n_3=7$。高度定义若不同,按教材递推一致即可。
▸题 E 红黑树
证明:红黑树中不存在两条连续红边(性质 4),并说明黑高相同如何限制高度。
解: 性质 4 直接禁止红-红;最长路径红黑交替故长度 $\le 2\times$ 最短路径黑结点数,得 $h=O(\log n)$。
▸题 F 堆
证明 Floyd 建堆 $O(n)$(见例 8.1)。另:在大根堆中插入比堆顶大的元素后,比较次数上界?
解: 上浮至多 $\lfloor\log_2 n\rfloor$ 次交换/比较量级 $O(\log n)$。
▸题 G 哈希
装填因子 $\alpha=0.75$,拉链法,均匀假设下成功查找平均链探查次数约为?
解: 约 $1+\alpha/2=1.375$(具体公式依教材:$1+\alpha/2$ 为常见成功查找期望)。
▸题 H 图
$|V|=n,|E|=m$ 的稀疏图,邻接表 BFS 复杂度?拓扑排序如何判有环?
解: $O(n+m)$;Kahn 算法结束后入队顶点数 $<n$ 则有环。
▸题 I DP
编辑距离 $m=n$,时间空间?如何压空间?
解: 时间 $O(n^2)$;空间两行滚动 $O(n)$。
▸题 J P/NP
说明“验证哈密顿回路证书”为何在多项式时间,而“寻找”难。
解: 证书为 $n$ 个顶点排列,检查相邻边存在与是否排列即可 $O(n+m)$;搜索空间 $n!$ 无已知多项式算法(NPC)。
复杂度速查
| 结构/算法 | 查找 | 插入 | 删除 | 备注 |
|---|---|---|---|---|
| 动态数组 | $O(n)$ 按值 | 尾均摊 $O(1)$ | $O(n)$ | 按下标 $O(1)$ |
| 单链表 | $O(n)$ | 头 $O(1)$ | $O(n)$ | |
| 栈/队列 | — | $O(1)$ | $O(1)$ | |
| 哈希表 | 均 $O(1)$ | 均 $O(1)$ | 均 $O(1)$ | 最坏 $O(n)$ |
| BST | 均 $O(\log n)$ | 同 | 同 | 最坏 $O(n)$ |
| AVL/RB | $O(\log n)$ | $O(\log n)$ | $O(\log n)$ | 最坏保证 |
| Splay | 均摊 $O(\log n)$ | 同 | 同 | 单次最坏 $O(n)$ |
| 二叉堆 | 最值 $O(1)$ | $O(\log n)$ | $O(\log n)$ | 建堆 $O(n)$ |
| Trie | $O(L)$ | $O(L)$ | $O(L)$ | |
| 图 BFS/DFS | — | — | — | $O(V+E)$ |
专题补强
表达式求值(栈)
中缀转后缀(Shunting-yard)后用栈求值;或双栈直接算中缀。
▸后缀(逆波兰)求值
自左向右扫描记号:遇操作数则压栈;遇二元运算符则弹出栈顶两个操作数,先弹出者为右操作数 $b$、后弹出者为左操作数 $a$,将 $a,\mathrm{op},b$ 的结果再压回。扫描结束时栈中仅余最终结果。时间 $O(n)$,辅助栈空间 $O(n)$。除法截断方向依题约定(常见为向零取整)。
Python(后缀求值):
1 | def eval_rpn(tokens): |
C:
1 |
|
C++: std::stack<long>,tokens 为 vector<string>,逻辑同 Python。
单调栈:接雨水与柱状图
接雨水(42) 按行/按列/单调栈/双指针。单调栈法:维护高度递增下标栈,凹槽面积累加。
▸单调栈接雨水与柱状图
接雨水: 栈中下标对应高度严格递增。当当前柱 $h[i]$ 高于栈顶柱时,栈顶可作为凹槽底;再取新栈顶为左壁、$i$ 为右壁,横宽为 $i-\mathrm{left}-1$,高为 $\min(h[\mathrm{left}],h[i])-\mathrm{bottom}$,累加到答案。每个下标至多入出栈一次,时间 $O(n)$。
柱状图最大矩形: 两端加高度 0 哨兵后,维护高度严格递增的下标栈。当 $h[i]$ 小于栈顶高度时,弹出柱 $j$ 作为矩形高,右边界为 $i$、左边界为新栈顶,宽为 $i-\mathrm{st.top}-1$,更新全局最大面积。
Python:
1 | def trap(height): |
C++:
1 | // 接雨水:逻辑同 Python 版 trap;stack 存下标 |
C: 用数组模拟栈即可,复杂度均为 $O(n)$ 时间、$O(n)$ 空间。
Dijkstra(非负权最短路)
▸堆优化 Dijkstra
维护 $dist[v]$ 为源点到 $v$ 的当前最短路上界。优先队列每次取出 $dist$ 最小的顶点 $u$ 做松弛:对邻边 $(u,v,w)$,若 $dist[v]>dist[u]+w$ 则更新并重新入堆。允许同一顶点多次入堆,弹出时若 $d>dist[u]$ 则丢弃陈旧记录。要求边权非负;时间 $O((V+E)\log V)$(二叉堆)。
Python:
1 | import heapq |
C++:
1 | // g:邻接表,g[u] 存 {v, w};返回各点最短路(不可达为 INF) |
C: 邻接表 + 手写二叉堆,或稠密图用 $O(V^2)$ 数组版 Dijkstra。
Kruskal MST
▸Kruskal 最小生成树
将边按权升序排列,依次尝试加入:若两端点尚不连通(并查集 union 成功),则纳入 MST,否则丢弃以免成环。至多取 $n-1$ 条边即可停止。正确性依赖贪心选择性质;时间主导项为排序 $O(E\log E)$,并查集近似 $O(E\alpha(n))$。
1 | def kruskal(n, edges): |
递归与时间空间
递归式除主定理外,可用展开法、树图法、Akra-Bazzi。空间复杂度含递归栈深度:平衡递归 $O(\log n)$,链状 $O(n)$。尾递归可优化为迭代(视语言与编译器)。
▸题 K 递归空间
归并排序辅助数组 $O(n)$,递归深度 $O(\log n)$,总空间 $O(n)$。快排期望栈深 $O(\log n)$,最坏 $O(n)$(可通过与较短边优先递归改善)。
稀疏矩阵
三元组顺序表 (row, col, val) 或行逻辑链接。转置、加法、乘法注意复杂度与稠密矩阵 $O(n^3)$ 乘法的对比;极度稀疏时链表/哈希存非零元。
线索二叉树要点
利用空指针域存放中序前驱/后继线索,加 ltag/rtag 区分孩子与线索。中序遍历可 $O(1)$ 辅助空间完成(相对显式栈)。
数据结构选型
| 需求 | 倾向 |
|---|---|
| 随机下标访问 | 数组 / vector |
| 任意位置频繁插入删除 | 链表(实际缓存差,常不如 vector) |
| 端点操作 | 栈 / 队列 / deque |
| 关键字查找 | 哈希(无序)/ 平衡树(有序) |
| 动态最值 / TopK | 堆 |
| 前缀匹配 | Trie |
| 区间最值(静态) | ST 表;动态 → 线段树 / 树状数组 |
| 连通分量合并 | 并查集 |
| 外存有序索引 | B+ 树 |
补充计算题
▸题 L Splay 均摊直觉
为何 Zig-Zig 先转祖父再转父,而非两次单旋到根?
解: 该顺序使势能下降更显著,保证均摊 $O(\log n)$;若只用 Zig 到根,均摊界会变坏。
▸题 M 哈希最坏
开放定址 $\alpha\to 1$ 时查找如何退化?拉链法如何缓解?
解: 探测序列变长,接近 $O(n)$;拉链可在长链改用 BST/RB(Java 8 HashMap),或再散列降低 $\alpha$。
▸题 N 0-1 背包
$n=3$,$W=5$,$(w,v)= (2,3),(3,4),(4,5)$。$dp$ 一维逆序更新最终最优值?
解: 最优为物品 1+2:$w=5,v=7$。
▸题 O 拓扑
课程数 4,先修 [[1,0],[2,0],[3,1],[3,2]]。一种合法修课顺序?
解: 如 $0,1,2,3$ 或 $0,2,1,3$(Kahn 多解)。
三语言对照备忘
| 结构 | Python | C | C++ |
|---|---|---|---|
| 动态数组 | list |
自写 / 可变长数组 | vector |
| 栈 | list append/pop |
数组+top | stack |
| 队列 | deque |
循环数组 | queue / deque |
| 哈希 | dict / set |
自写链地址 | unordered_map / set |
| 有序映射 | SortedDict 等 |
自写 BST/RB | map / set |
| 堆 | heapq |
自写 | priority_queue |
| 链表/树题 | 自定义 class | struct + 指针 | struct / 智能指针 |
刷题时以 Python 快速验证思路,用 C/C++ 练指针与复杂度常数;课程作业常要求 C 手写结构体实现。
复习路线
- 线性结构与复杂度记号 → 栈队列应用(括号、单调栈)
- 哈希 + 双指针 + 滑动窗口(Hot100 数组篇)
- 链表题型套路 → 二叉树遍历与递归树 DP
- BST → AVL/RB/Splay 性质题 → 堆与 TopK
- 图 BFS/DFS/拓扑 → 并查集
- 分治与主定理计算题 → DP 状态设计 → 贪心证明意识 → P/NP 概念
- 对照本文 Hot100 总表查漏,详解见 Leetcode100 专文
本文与《排序算法详解》《Leetcode Hot100 题解整理》互补:结构与理论以本文为准,排序细节与逐题题解见另两文。


