百日算法修炼 · Day 18
主题:图的 BFS、DFS、拓扑排序、Kruskal、Prim、Dijkstra
语言:Java | 建议用时:120~180 分钟
阅读方式
本文继续采用"先练习、后答案"的方式:
- 上半篇是独立练习区:只给题目、示例、思考问题和可折叠提示;
- 下半篇是参考答案区:包含代码诊断、优化代码、过程图解、复杂度分析与易错点;
- 推荐先独立写完 6 题,再通过"查看参考答案"链接逐题核对;
- 图算法很容易因为"重复访问、环、方向、权重"等细节出错,建议每题都先说清楚数据结构的语义再写代码。
今天的核心不是记住 6 段代码,而是建立一套"图上的信息推进"思维:
- BFS / DFS:决定"下一个访问谁";
- 拓扑排序 :不断消除入度为
0的节点;- 最小生成树:不断选择"安全的最小边";
- Dijkstra:不断确定"当前最短距离已经不会再变"的节点。
今日任务地图
#mermaid-svg-zHR2xMw91BWeid12{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zHR2xMw91BWeid12 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zHR2xMw91BWeid12 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zHR2xMw91BWeid12 .error-icon{fill:#552222;}#mermaid-svg-zHR2xMw91BWeid12 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zHR2xMw91BWeid12 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zHR2xMw91BWeid12 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zHR2xMw91BWeid12 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zHR2xMw91BWeid12 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zHR2xMw91BWeid12 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zHR2xMw91BWeid12 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zHR2xMw91BWeid12 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zHR2xMw91BWeid12 .marker.cross{stroke:#333333;}#mermaid-svg-zHR2xMw91BWeid12 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zHR2xMw91BWeid12 p{margin:0;}#mermaid-svg-zHR2xMw91BWeid12 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zHR2xMw91BWeid12 .cluster-label text{fill:#333;}#mermaid-svg-zHR2xMw91BWeid12 .cluster-label span{color:#333;}#mermaid-svg-zHR2xMw91BWeid12 .cluster-label span p{background-color:transparent;}#mermaid-svg-zHR2xMw91BWeid12 .label text,#mermaid-svg-zHR2xMw91BWeid12 span{fill:#333;color:#333;}#mermaid-svg-zHR2xMw91BWeid12 .node rect,#mermaid-svg-zHR2xMw91BWeid12 .node circle,#mermaid-svg-zHR2xMw91BWeid12 .node ellipse,#mermaid-svg-zHR2xMw91BWeid12 .node polygon,#mermaid-svg-zHR2xMw91BWeid12 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zHR2xMw91BWeid12 .rough-node .label text,#mermaid-svg-zHR2xMw91BWeid12 .node .label text,#mermaid-svg-zHR2xMw91BWeid12 .image-shape .label,#mermaid-svg-zHR2xMw91BWeid12 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zHR2xMw91BWeid12 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zHR2xMw91BWeid12 .rough-node .label,#mermaid-svg-zHR2xMw91BWeid12 .node .label,#mermaid-svg-zHR2xMw91BWeid12 .image-shape .label,#mermaid-svg-zHR2xMw91BWeid12 .icon-shape .label{text-align:center;}#mermaid-svg-zHR2xMw91BWeid12 .node.clickable{cursor:pointer;}#mermaid-svg-zHR2xMw91BWeid12 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zHR2xMw91BWeid12 .arrowheadPath{fill:#333333;}#mermaid-svg-zHR2xMw91BWeid12 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zHR2xMw91BWeid12 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zHR2xMw91BWeid12 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zHR2xMw91BWeid12 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zHR2xMw91BWeid12 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zHR2xMw91BWeid12 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zHR2xMw91BWeid12 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zHR2xMw91BWeid12 .cluster text{fill:#333;}#mermaid-svg-zHR2xMw91BWeid12 .cluster span{color:#333;}#mermaid-svg-zHR2xMw91BWeid12 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zHR2xMw91BWeid12 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zHR2xMw91BWeid12 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zHR2xMw91BWeid12 .icon-shape,#mermaid-svg-zHR2xMw91BWeid12 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zHR2xMw91BWeid12 .icon-shape p,#mermaid-svg-zHR2xMw91BWeid12 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zHR2xMw91BWeid12 .icon-shape .label rect,#mermaid-svg-zHR2xMw91BWeid12 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zHR2xMw91BWeid12 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zHR2xMw91BWeid12 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zHR2xMw91BWeid12 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始 Day 18
BFS 宽度优先
DFS 深度优先
拓扑排序 Kahn
Kruskal 最小生成树
Prim 最小生成树
Dijkstra 单源最短路
统一理解 图上的贪心与搜索
| 题号 | 主题 | 核心能力 |
|---|---|---|
| 1 | 图的 BFS | 队列、去重、按层扩散 |
| 2 | 图的 DFS | 栈、去重、深入再回退 |
| 3 | 拓扑排序 | 入度表、零入度队列、环检测 |
| 4 | Kruskal | 边排序、并查集、避免成环 |
| 5 | Prim | 点解锁边、小根堆、切分思想 |
| 6 | Dijkstra | 距离松弛、确定节点、改写堆 |
第一部分:独立练习区
先不要向下翻到答案区。做图算法时,建议先回答三个问题:
我用什么结构保存"下一批候选"?如何避免重复处理?什么时候一个状态可以被最终确定?
统一图结构说明
今天默认使用如下抽象:
text
Node
├── value 节点编号
├── in 入度
├── out 出度
├── nexts 从当前点可直接到达的邻接点
└── edges 从当前点出发的边
Edge
├── from
├── to
└── weight
Graph
├── nodes
└── edges
对 BFS、DFS、拓扑排序与 Dijkstra,可以直接理解为有向图。
对 Kruskal、Prim,本节讨论的是无向带权图 ;如果代码结构用有向
Edge表示无向边,通常需要为一条无向边建立两个方向的边。
练习 1:图的宽度优先遍历 BFS
给定图中的起点 root,按照宽度优先的方式访问所有从 root 可达的节点,并保证:
- 每个节点只处理一次;
- 即使图中存在环,也不能死循环;
- 使用迭代方式实现。
例如:
text
1 -> 2 -> 4
| |
v v
3 -> 5
并且存在一条边:5 -> 1
若邻接表顺序为:
text
1: [2, 3]
2: [4, 5]
3: [5]
5: [1]
一种 BFS 输出为:
text
1 2 3 4 5
思考问题
- 为什么树的 BFS 可以不写
visited,而一般图的 BFS 几乎一定需要? visited应该在"入队时"标记,还是"出队时"标记?两者有什么差别?- 为什么队列适合表达"距离起点更近的节点优先处理"?
- 如果一个节点在发现时输出一次、出队时又输出一次,会发生什么?
提示 1:先占坑,再排队
一个邻居第一次被发现时就立即放入 visited,再入队。这样其他节点即使也指向它,也不会再次把它加入队列。
提示 2:只选一个明确的"处理时机"
建议把"节点出队"定义为真正处理节点的时刻,输出也只发生在这里。
写完后再看:[查看练习 1 参考答案](#查看练习 1 参考答案)
练习 2:图的深度优先遍历 DFS
仍从 root 出发,使用显式栈完成 DFS。
要求:
- 每个可达节点只处理一次;
- 图中有环时仍能正确结束;
- 尽量让访问顺序与
nexts中的邻接顺序一致。
例如:
text
1 -> 2 -> 4
| |
v v
3 -> 5
若:
text
1: [2, 3]
2: [4, 5]
希望优先沿 1 -> 2 -> 4 深入,而不是一开始就先处理 3。
思考问题
- DFS 为什么用栈而不是队列?
- 如果按
nexts正序压栈,真正弹出的顺序会怎样? - 为了让
nexts中靠前的邻居先被处理,为什么常常需要倒序压栈? visited在入栈时标记有什么好处?
提示:注意 LIFO 会反转顺序
栈是后进先出。如果希望 nexts 中第 0 个邻居先被弹出,就可以从 nexts 的末尾向前压栈。
写完后再看:[查看练习 2 参考答案](#查看练习 2 参考答案)
练习 3:拓扑排序
给定一个有向图 graph,如果它是 DAG(Directed Acyclic Graph,有向无环图),请返回一种合法的拓扑序。
例如:
text
A ---> C ---> D
\ ^
\-> B -----|
约束关系可以理解为:
text
A 必须在 B、C 之前
B、C 必须在 D 之前
合法拓扑序之一:
text
A B C D
也可能是:
text
A C B D
思考问题
- 为什么"入度为 0"意味着一个节点当前没有前置依赖?
- 从结果中"删除"一个节点,应该如何体现在其他节点上?
- 为什么不能直接修改原图里的
node.in? - 如果最终输出节点数小于图中节点数,说明了什么?
提示 1:Kahn 算法
先把所有入度为 0 的节点放进队列。每弹出一个节点,就把它指向节点的剩余入度减 1;谁减到 0,谁进入队列。
提示 2:环不会产生新的零入度节点
如果剩余节点互相形成有向环,它们的入度无法通过删除环外节点降到 0,因此最终结果会缺节点。
写完后再看:[查看练习 3 参考答案](#查看练习 3 参考答案)
练习 4:Kruskal 最小生成树
给定一个无向连通带权图,求它的一棵最小生成树(MST)。
例如:
text
2
A ----- B
| \ |
4 | \ 1 | 5
| \ |
C ----- D
3
你可以把 Kruskal 理解为:
text
把所有边按权重从小到大检查
但是,不能为了便宜就把所有小边都拿走,因为生成树不能有环。
思考问题
- 每次拿到当前最小边时,怎样判断加入它会不会形成环?
- 为什么并查集特别适合回答"两个点是否已经连通"?
- 路径压缩与按大小合并分别优化了什么?
- 如果原图不连通,Kruskal 最终得到的是什么?
提示 1:连通性就是"集合"
如果边的两个端点已经属于同一个并查集,那么它们之间本来就有路径,再加这条边一定形成环。
提示 2:最小生成森林
对不连通图,同样的过程会在每个连通分量中得到一棵最小生成树,整体称为最小生成森林。
写完后再看:[查看练习 4 参考答案](#查看练习 4 参考答案)
练习 5:Prim 最小生成树
仍然求无向带权图的最小生成树,但这一次从"点"出发思考。
核心过程:
text
选一个起点
-> 解锁它的边
-> 取最小候选边
-> 如果这条边能带来新节点,就收下
-> 新节点继续解锁新的边
思考问题
- Prim 为什么需要一个"已进入生成树的节点集合"?
- 小根堆里保存的是节点还是边?
- 为什么弹出的最小边也可能被丢弃?
- Kruskal 与 Prim 都是贪心,它们的"候选集合"有什么本质区别?
- 如何让 Prim 支持不连通图?
提示 1:边便宜还不够
如果一条最小边的目标节点已经在生成树中,那么加入它只会形成环,因此应该跳过。
提示 2:森林需要重新开局
遍历所有节点;如果某个节点还没访问过,就把它当作新连通分量的起点重新启动 Prim。
写完后再看:[查看练习 5 参考答案](#查看练习 5 参考答案)
练习 6:Dijkstra 单源最短路径
给定起点 from,求它到所有可达节点的最短距离。
例如:
text
1 --2--> 2 --1--> 3 --1--> 4
\ |
|4
v v
3 4
从 1 出发:
text
dist(1) = 0
dist(2) = 2
dist(3) = 3
dist(4) = 4
今天建议写两版:
- 朴素版:每轮扫描所有候选距离,找出最小且未确定的节点;
- 堆优化版 :用支持
add / update / ignore的小根堆维护候选节点。
思考问题
- Dijkstra 为什么要求边权不能为负数?
- 什么叫"松弛一条边"?
- 一个节点什么时候可以认为最短距离已经最终确定?
- 第一次发现邻居
to时,新距离应该是edge.weight,还是dist(current) + edge.weight? - 改写堆为什么需要同时保存"节点位置"和"节点当前距离"?
提示 1:松弛公式
经过 current 到达 next 的候选距离是 distcurrent + weight(current, next)。如果它比 next 已知距离更小,就更新 next。
提示 2:三种堆状态
一个节点可能从未进堆、仍在堆中、已经弹出。三种状态分别对应 add、update 或 ignore。
重要前提
Dijkstra 不适用于包含负权边的图。负权场景需要考虑 Bellman-Ford 等其他算法。
写完后再看:[查看练习 6 参考答案](#查看练习 6 参考答案)
第二部分:参考答案与图解
到这里再开始核对。每题先看"题目回顾"和"核心思路",再看"原代码诊断"和优化代码。
重点比较的是:容器的语义、状态何时确定、去重发生在哪里。
答案 1:图的宽度优先遍历 BFS
[返回练习 1](#返回练习 1)
题目回顾
从起点出发,按"离起点更近的节点优先"的方式扩散,并保证每个节点只处理一次。
核心思路:队列 + visited
#mermaid-svg-G3KcwHjUL5pr8AgU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G3KcwHjUL5pr8AgU .error-icon{fill:#552222;}#mermaid-svg-G3KcwHjUL5pr8AgU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G3KcwHjUL5pr8AgU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G3KcwHjUL5pr8AgU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G3KcwHjUL5pr8AgU .marker.cross{stroke:#333333;}#mermaid-svg-G3KcwHjUL5pr8AgU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G3KcwHjUL5pr8AgU p{margin:0;}#mermaid-svg-G3KcwHjUL5pr8AgU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster-label text{fill:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster-label span{color:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster-label span p{background-color:transparent;}#mermaid-svg-G3KcwHjUL5pr8AgU .label text,#mermaid-svg-G3KcwHjUL5pr8AgU span{fill:#333;color:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .node rect,#mermaid-svg-G3KcwHjUL5pr8AgU .node circle,#mermaid-svg-G3KcwHjUL5pr8AgU .node ellipse,#mermaid-svg-G3KcwHjUL5pr8AgU .node polygon,#mermaid-svg-G3KcwHjUL5pr8AgU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-G3KcwHjUL5pr8AgU .rough-node .label text,#mermaid-svg-G3KcwHjUL5pr8AgU .node .label text,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape .label,#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape .label{text-anchor:middle;}#mermaid-svg-G3KcwHjUL5pr8AgU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-G3KcwHjUL5pr8AgU .rough-node .label,#mermaid-svg-G3KcwHjUL5pr8AgU .node .label,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape .label,#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape .label{text-align:center;}#mermaid-svg-G3KcwHjUL5pr8AgU .node.clickable{cursor:pointer;}#mermaid-svg-G3KcwHjUL5pr8AgU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-G3KcwHjUL5pr8AgU .arrowheadPath{fill:#333333;}#mermaid-svg-G3KcwHjUL5pr8AgU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-G3KcwHjUL5pr8AgU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-G3KcwHjUL5pr8AgU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G3KcwHjUL5pr8AgU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-G3KcwHjUL5pr8AgU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G3KcwHjUL5pr8AgU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster text{fill:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU .cluster span{color:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-G3KcwHjUL5pr8AgU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-G3KcwHjUL5pr8AgU rect.text{fill:none;stroke-width:0;}#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape p,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-G3KcwHjUL5pr8AgU .icon-shape .label rect,#mermaid-svg-G3KcwHjUL5pr8AgU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G3KcwHjUL5pr8AgU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-G3KcwHjUL5pr8AgU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-G3KcwHjUL5pr8AgU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
起点入队并标记
队首出队
处理当前节点
扫描所有邻居
第一次访问?
标记并入队
跳过
队列为空?
结束
完整代码
java
package class10;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;
/**
* 图的宽度优先遍历(BFS)。
*
* 核心结构:
* 1. 队列控制"先进先出";
* 2. visited 防止有环图中的节点被重复处理。
*/
public class Code01_BFS {
public static void bfs(Node root) {
if (root == null) {
return;
}
Queue<Node> queue = new ArrayDeque<>();
Set<Node> visited = new HashSet<>();
queue.offer(root);
visited.add(root);
while (!queue.isEmpty()) {
Node current = queue.poll();
// 一个节点只在真正出队处理时输出一次。
System.out.println(current.value);
for (Node next : current.nexts) {
// add 返回 true,说明 next 是第一次被访问。
if (visited.add(next)) {
queue.offer(next);
}
}
}
}
}
过程图解
假设:
text
1: [2, 3]
2: [4, 5]
3: [5]
5: [1]
| 步骤 | 出队 | 新发现 | 队列 | visited |
|---|---|---|---|---|
| 1 | 1 | 2, 3 | [2, 3] |
(1, 2, 3) |
| 2 | 2 | 4, 5 | [3, 4, 5] |
(1, 2, 3, 4, 5) |
| 3 | 3 | 无 | [4, 5] |
不变 |
| 4 | 4 | 无 | [5] |
不变 |
| 5 | 5 | 1 已访问 |
[] |
不变 |
复杂度
设可达点数为 V,可达边数为 E:
- 时间复杂度:O(V + E);
- 额外空间:O(V) ,来自队列与
visited。
易错点
- 忘记
visited,有环图可能无限循环; - 一个节点重复入队,导致大量重复工作;
- 同时在"发现"和"出队"两个阶段输出;
- 把 BFS 和"按层返回列表"混为一谈:图 BFS 的核心是搜索顺序,不一定需要分层收集结果。
答案 2:图的深度优先遍历 DFS
[返回练习 2](#返回练习 2)
题目回顾
使用显式栈从起点不断深入,无法继续时再回到仍有候选分支的位置。
核心思路:栈 + visited
#mermaid-svg-kEQxCklfJtMQbDXa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kEQxCklfJtMQbDXa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kEQxCklfJtMQbDXa .error-icon{fill:#552222;}#mermaid-svg-kEQxCklfJtMQbDXa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kEQxCklfJtMQbDXa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kEQxCklfJtMQbDXa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kEQxCklfJtMQbDXa .marker.cross{stroke:#333333;}#mermaid-svg-kEQxCklfJtMQbDXa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kEQxCklfJtMQbDXa p{margin:0;}#mermaid-svg-kEQxCklfJtMQbDXa .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster-label text{fill:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster-label span{color:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster-label span p{background-color:transparent;}#mermaid-svg-kEQxCklfJtMQbDXa .label text,#mermaid-svg-kEQxCklfJtMQbDXa span{fill:#333;color:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .node rect,#mermaid-svg-kEQxCklfJtMQbDXa .node circle,#mermaid-svg-kEQxCklfJtMQbDXa .node ellipse,#mermaid-svg-kEQxCklfJtMQbDXa .node polygon,#mermaid-svg-kEQxCklfJtMQbDXa .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kEQxCklfJtMQbDXa .rough-node .label text,#mermaid-svg-kEQxCklfJtMQbDXa .node .label text,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape .label,#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape .label{text-anchor:middle;}#mermaid-svg-kEQxCklfJtMQbDXa .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kEQxCklfJtMQbDXa .rough-node .label,#mermaid-svg-kEQxCklfJtMQbDXa .node .label,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape .label,#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape .label{text-align:center;}#mermaid-svg-kEQxCklfJtMQbDXa .node.clickable{cursor:pointer;}#mermaid-svg-kEQxCklfJtMQbDXa .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kEQxCklfJtMQbDXa .arrowheadPath{fill:#333333;}#mermaid-svg-kEQxCklfJtMQbDXa .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kEQxCklfJtMQbDXa .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kEQxCklfJtMQbDXa .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kEQxCklfJtMQbDXa .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kEQxCklfJtMQbDXa .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kEQxCklfJtMQbDXa .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kEQxCklfJtMQbDXa .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster text{fill:#333;}#mermaid-svg-kEQxCklfJtMQbDXa .cluster span{color:#333;}#mermaid-svg-kEQxCklfJtMQbDXa div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kEQxCklfJtMQbDXa .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kEQxCklfJtMQbDXa rect.text{fill:none;stroke-width:0;}#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape p,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kEQxCklfJtMQbDXa .icon-shape .label rect,#mermaid-svg-kEQxCklfJtMQbDXa .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kEQxCklfJtMQbDXa .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kEQxCklfJtMQbDXa .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kEQxCklfJtMQbDXa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
起点压栈并标记
栈为空?
弹出栈顶节点
处理当前节点
倒序扫描邻居
邻居未访问?
标记并压栈
结束
为什么要倒序压栈?
假设:
text
nexts = [2, 3, 4]
如果正序压栈:
text
push 2
push 3
push 4
栈顶是 4,所以真实弹出顺序会变成:
text
4 -> 3 -> 2
如果希望访问顺序更接近邻接表本身的 2 -> 3 -> 4,就倒序压栈。
完整代码
java
package class10;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 图的深度优先遍历(DFS),使用显式栈实现。
*/
public class Code02_DFS {
public static void dfs(Node root) {
if (root == null) {
return;
}
Deque<Node> stack = new ArrayDeque<>();
Set<Node> visited = new HashSet<>();
stack.push(root);
visited.add(root);
while (!stack.isEmpty()) {
Node current = stack.pop();
System.out.println(current.value);
List<Node> nexts = current.nexts;
// 栈是后进先出。倒序压栈后,弹出顺序与 nexts 原顺序一致。
for (int i = nexts.size() - 1; i >= 0; i--) {
Node next = nexts.get(i);
if (visited.add(next)) {
stack.push(next);
}
}
}
}
}
复杂度
- 时间复杂度:O(V + E);
- 额外空间:O(V)。
易错点
- 没有去重导致环上死循环;
- 正序压栈却误以为会正序访问;
- 把
visited放得太晚,导致同一个节点被多个前驱重复压栈。
答案 3:拓扑排序
[返回练习 3](#返回练习 3)
题目回顾
在 DAG 中找出一个线性顺序,使每条有向边:
text
u -> v
都满足:
text
u 出现在 v 之前
核心思路:不断消除零入度节点
#mermaid-svg-rXNsmqAPhl5xwFpC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rXNsmqAPhl5xwFpC .error-icon{fill:#552222;}#mermaid-svg-rXNsmqAPhl5xwFpC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rXNsmqAPhl5xwFpC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rXNsmqAPhl5xwFpC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rXNsmqAPhl5xwFpC .marker.cross{stroke:#333333;}#mermaid-svg-rXNsmqAPhl5xwFpC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rXNsmqAPhl5xwFpC p{margin:0;}#mermaid-svg-rXNsmqAPhl5xwFpC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster-label text{fill:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster-label span{color:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster-label span p{background-color:transparent;}#mermaid-svg-rXNsmqAPhl5xwFpC .label text,#mermaid-svg-rXNsmqAPhl5xwFpC span{fill:#333;color:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .node rect,#mermaid-svg-rXNsmqAPhl5xwFpC .node circle,#mermaid-svg-rXNsmqAPhl5xwFpC .node ellipse,#mermaid-svg-rXNsmqAPhl5xwFpC .node polygon,#mermaid-svg-rXNsmqAPhl5xwFpC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rXNsmqAPhl5xwFpC .rough-node .label text,#mermaid-svg-rXNsmqAPhl5xwFpC .node .label text,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape .label,#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape .label{text-anchor:middle;}#mermaid-svg-rXNsmqAPhl5xwFpC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rXNsmqAPhl5xwFpC .rough-node .label,#mermaid-svg-rXNsmqAPhl5xwFpC .node .label,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape .label,#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape .label{text-align:center;}#mermaid-svg-rXNsmqAPhl5xwFpC .node.clickable{cursor:pointer;}#mermaid-svg-rXNsmqAPhl5xwFpC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rXNsmqAPhl5xwFpC .arrowheadPath{fill:#333333;}#mermaid-svg-rXNsmqAPhl5xwFpC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rXNsmqAPhl5xwFpC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rXNsmqAPhl5xwFpC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rXNsmqAPhl5xwFpC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rXNsmqAPhl5xwFpC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rXNsmqAPhl5xwFpC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster text{fill:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC .cluster span{color:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rXNsmqAPhl5xwFpC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rXNsmqAPhl5xwFpC rect.text{fill:none;stroke-width:0;}#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape p,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rXNsmqAPhl5xwFpC .icon-shape .label rect,#mermaid-svg-rXNsmqAPhl5xwFpC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rXNsmqAPhl5xwFpC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rXNsmqAPhl5xwFpC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rXNsmqAPhl5xwFpC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
是
否
复制所有节点入度
零入度节点入队
队列为空?
弹出一个零入度节点
加入拓扑序
所有后继剩余入度减 1
某后继变为 0?
该后继入队
结果数等于节点数?
得到拓扑序
存在有向环
完整代码
java
package class10;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
/**
* Kahn 算法实现拓扑排序。
*
* 只适用于有向无环图(DAG)。
*/
public class Code03_TopologySort {
public static List<Node> topologicalSort(Graph graph) {
List<Node> result = new ArrayList<>();
if (graph == null || graph.nodes.isEmpty()) {
return result;
}
// 使用副本记录"剩余入度",避免修改原图中的 node.in。
Map<Node, Integer> inDegree = new HashMap<>();
Queue<Node> zeroInQueue = new ArrayDeque<>();
for (Node node : graph.nodes.values()) {
inDegree.put(node, node.in);
if (node.in == 0) {
zeroInQueue.offer(node);
}
}
while (!zeroInQueue.isEmpty()) {
Node current = zeroInQueue.poll();
result.add(current);
// 删除 current 等价于:它指向的所有节点入度减 1。
for (Node next : current.nexts) {
int nextIn = inDegree.get(next) - 1;
inDegree.put(next, nextIn);
if (nextIn == 0) {
zeroInQueue.offer(next);
}
}
}
// 若仍有节点无法被"消掉",说明存在有向环。
if (result.size() != graph.nodes.size()) {
throw new IllegalArgumentException(
"Graph contains a cycle; topological order does not exist.");
}
return result;
}
}
为什么不修改 node.in?
因为拓扑排序只是一次计算。
如果直接对原图做:
text
node.in--
下一次再调用拓扑排序时,图的入度信息已经被污染。
因此使用:
java
Map<Node, Integer> inDegree
保存副本更安全。
如何检测环?
如果:
text
result.size() < graph.nodes.size()
说明还有节点从未进入零入度队列。
最常见原因就是这些剩余节点处在有向环中。
复杂度
- 时间复杂度:O(V + E);
- 额外空间:O(V)。
易错点
- 拓扑排序只针对有向图;
- 有合法拓扑序的前提是无环;
- 拓扑序通常不唯一;
- 忘记维护入度副本,污染原图。
答案 4:Kruskal 最小生成树
[返回练习 4](#返回练习 4)
题目回顾
在无向带权图中选择若干条边,使所有节点连通、无环,并让总权重最小。
核心思路:边从小到大 + 并查集判断成环
#mermaid-svg-eSJKKoLuueEXJv2I{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eSJKKoLuueEXJv2I .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eSJKKoLuueEXJv2I .error-icon{fill:#552222;}#mermaid-svg-eSJKKoLuueEXJv2I .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eSJKKoLuueEXJv2I .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eSJKKoLuueEXJv2I .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eSJKKoLuueEXJv2I .marker.cross{stroke:#333333;}#mermaid-svg-eSJKKoLuueEXJv2I svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eSJKKoLuueEXJv2I p{margin:0;}#mermaid-svg-eSJKKoLuueEXJv2I .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster-label text{fill:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster-label span{color:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster-label span p{background-color:transparent;}#mermaid-svg-eSJKKoLuueEXJv2I .label text,#mermaid-svg-eSJKKoLuueEXJv2I span{fill:#333;color:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .node rect,#mermaid-svg-eSJKKoLuueEXJv2I .node circle,#mermaid-svg-eSJKKoLuueEXJv2I .node ellipse,#mermaid-svg-eSJKKoLuueEXJv2I .node polygon,#mermaid-svg-eSJKKoLuueEXJv2I .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eSJKKoLuueEXJv2I .rough-node .label text,#mermaid-svg-eSJKKoLuueEXJv2I .node .label text,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape .label,#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape .label{text-anchor:middle;}#mermaid-svg-eSJKKoLuueEXJv2I .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eSJKKoLuueEXJv2I .rough-node .label,#mermaid-svg-eSJKKoLuueEXJv2I .node .label,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape .label,#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape .label{text-align:center;}#mermaid-svg-eSJKKoLuueEXJv2I .node.clickable{cursor:pointer;}#mermaid-svg-eSJKKoLuueEXJv2I .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eSJKKoLuueEXJv2I .arrowheadPath{fill:#333333;}#mermaid-svg-eSJKKoLuueEXJv2I .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eSJKKoLuueEXJv2I .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eSJKKoLuueEXJv2I .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eSJKKoLuueEXJv2I .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eSJKKoLuueEXJv2I .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eSJKKoLuueEXJv2I .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eSJKKoLuueEXJv2I .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster text{fill:#333;}#mermaid-svg-eSJKKoLuueEXJv2I .cluster span{color:#333;}#mermaid-svg-eSJKKoLuueEXJv2I div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eSJKKoLuueEXJv2I .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eSJKKoLuueEXJv2I rect.text{fill:none;stroke-width:0;}#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape p,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eSJKKoLuueEXJv2I .icon-shape .label rect,#mermaid-svg-eSJKKoLuueEXJv2I .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eSJKKoLuueEXJv2I .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eSJKKoLuueEXJv2I .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eSJKKoLuueEXJv2I :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
所有边进入小根堆
取当前最小边
两端已连通?
跳过 会成环
选择这条边
并查集合并两端集合
还有候选边?
结束
为什么"两个端点已连通"就一定会成环?
假设当前已经存在一条路径:
text
A -> ... -> B
此时再加入边:
text
A -- B
就会构成一个闭环。
所以 Kruskal 的关键问题不是:
text
"这条边够不够小?"
而是:
text
"这条边够小,并且安全吗?"
并查集图解
#mermaid-svg-WkSaLXUb2eWxIfCj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WkSaLXUb2eWxIfCj .error-icon{fill:#552222;}#mermaid-svg-WkSaLXUb2eWxIfCj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WkSaLXUb2eWxIfCj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WkSaLXUb2eWxIfCj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WkSaLXUb2eWxIfCj .marker.cross{stroke:#333333;}#mermaid-svg-WkSaLXUb2eWxIfCj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WkSaLXUb2eWxIfCj p{margin:0;}#mermaid-svg-WkSaLXUb2eWxIfCj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster-label text{fill:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster-label span{color:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster-label span p{background-color:transparent;}#mermaid-svg-WkSaLXUb2eWxIfCj .label text,#mermaid-svg-WkSaLXUb2eWxIfCj span{fill:#333;color:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .node rect,#mermaid-svg-WkSaLXUb2eWxIfCj .node circle,#mermaid-svg-WkSaLXUb2eWxIfCj .node ellipse,#mermaid-svg-WkSaLXUb2eWxIfCj .node polygon,#mermaid-svg-WkSaLXUb2eWxIfCj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WkSaLXUb2eWxIfCj .rough-node .label text,#mermaid-svg-WkSaLXUb2eWxIfCj .node .label text,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape .label,#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape .label{text-anchor:middle;}#mermaid-svg-WkSaLXUb2eWxIfCj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WkSaLXUb2eWxIfCj .rough-node .label,#mermaid-svg-WkSaLXUb2eWxIfCj .node .label,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape .label,#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape .label{text-align:center;}#mermaid-svg-WkSaLXUb2eWxIfCj .node.clickable{cursor:pointer;}#mermaid-svg-WkSaLXUb2eWxIfCj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WkSaLXUb2eWxIfCj .arrowheadPath{fill:#333333;}#mermaid-svg-WkSaLXUb2eWxIfCj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WkSaLXUb2eWxIfCj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WkSaLXUb2eWxIfCj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WkSaLXUb2eWxIfCj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WkSaLXUb2eWxIfCj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WkSaLXUb2eWxIfCj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster text{fill:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj .cluster span{color:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WkSaLXUb2eWxIfCj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WkSaLXUb2eWxIfCj rect.text{fill:none;stroke-width:0;}#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape p,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WkSaLXUb2eWxIfCj .icon-shape .label rect,#mermaid-svg-WkSaLXUb2eWxIfCj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WkSaLXUb2eWxIfCj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WkSaLXUb2eWxIfCj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WkSaLXUb2eWxIfCj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
节点各自一个集合
find 查询代表节点
代表节点相同?
已经连通
union 合并
小集合挂到大集合
find 时路径压缩
完整代码
java
package class10;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
/**
* Kruskal 最小生成树算法。
*
* 思路:按边权从小到大尝试加入边;
* 若一条边的两个端点已经连通,则跳过它以避免形成环。
*
* 对不连通图,返回最小生成森林。
*/
public class Code04_Kruskal {
public static List<Edge> kruskal(Graph graph) {
List<Edge> result = new ArrayList<>();
if (graph == null || graph.nodes.isEmpty()) {
return result;
}
PriorityQueue<Edge> minEdgeHeap =
new PriorityQueue<>(Comparator.comparingInt(edge -> edge.weight));
minEdgeHeap.addAll(graph.edges);
UnionFind<Node> unionFind = new UnionFind<>(graph.nodes.values());
while (!minEdgeHeap.isEmpty()) {
Edge edge = minEdgeHeap.poll();
if (!unionFind.isSameSet(edge.from, edge.to)) {
unionFind.union(edge.from, edge.to);
result.add(edge);
}
}
return result;
}
/**
* 并查集:路径压缩 + 按集合大小合并。
*/
public static class UnionFind<T> {
private final Map<T, T> parent = new HashMap<>();
private final Map<T, Integer> size = new HashMap<>();
public UnionFind(Iterable<T> values) {
for (T value : values) {
parent.put(value, value);
size.put(value, 1);
}
}
/**
* 找到代表节点,并把沿途节点直接挂到代表节点上。
*/
public T find(T value) {
if (!parent.containsKey(value)) {
return null;
}
Deque<T> path = new ArrayDeque<>();
T current = value;
while (parent.get(current) != current) {
path.push(current);
current = parent.get(current);
}
while (!path.isEmpty()) {
parent.put(path.pop(), current);
}
return current;
}
public boolean isSameSet(T a, T b) {
T rootA = find(a);
T rootB = find(b);
return rootA != null && rootA == rootB;
}
public void union(T a, T b) {
T rootA = find(a);
T rootB = find(b);
if (rootA == null || rootB == null || rootA == rootB) {
return;
}
T big = size.get(rootA) >= size.get(rootB) ? rootA : rootB;
T small = big == rootA ? rootB : rootA;
parent.put(small, big);
size.put(big, size.get(big) + size.get(small));
size.remove(small);
}
public int getSetCount() {
return size.size();
}
}
}
复杂度
若使用小根堆装入所有边:
- 建堆 / 取边整体可近似理解为 O(E log E);
- 并查集操作摊还接近常数;
- 总体常写为 O(E log E)。
易错点
- MST 是无向图概念;
- 只按权重选边、不检查连通性会形成环;
- 对不连通图不存在"一棵覆盖全部节点的生成树",只有生成森林;
- 如果无向边用两个方向的
Edge表示,Kruskal 会看到重复方向,但并查集仍会阻止重复成环。
答案 5:Prim 最小生成树
[返回练习 5](#返回练习 5)
题目回顾
从某个点出发,让"已经进入生成树的节点集合"逐步扩大;每次选择一条最便宜、且能带来新节点的边。
核心思路:点解锁边
#mermaid-svg-4SAmdqjYZd2pDGvw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4SAmdqjYZd2pDGvw .error-icon{fill:#552222;}#mermaid-svg-4SAmdqjYZd2pDGvw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4SAmdqjYZd2pDGvw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4SAmdqjYZd2pDGvw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4SAmdqjYZd2pDGvw .marker.cross{stroke:#333333;}#mermaid-svg-4SAmdqjYZd2pDGvw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4SAmdqjYZd2pDGvw p{margin:0;}#mermaid-svg-4SAmdqjYZd2pDGvw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster-label text{fill:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster-label span{color:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster-label span p{background-color:transparent;}#mermaid-svg-4SAmdqjYZd2pDGvw .label text,#mermaid-svg-4SAmdqjYZd2pDGvw span{fill:#333;color:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .node rect,#mermaid-svg-4SAmdqjYZd2pDGvw .node circle,#mermaid-svg-4SAmdqjYZd2pDGvw .node ellipse,#mermaid-svg-4SAmdqjYZd2pDGvw .node polygon,#mermaid-svg-4SAmdqjYZd2pDGvw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4SAmdqjYZd2pDGvw .rough-node .label text,#mermaid-svg-4SAmdqjYZd2pDGvw .node .label text,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape .label,#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape .label{text-anchor:middle;}#mermaid-svg-4SAmdqjYZd2pDGvw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4SAmdqjYZd2pDGvw .rough-node .label,#mermaid-svg-4SAmdqjYZd2pDGvw .node .label,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape .label,#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape .label{text-align:center;}#mermaid-svg-4SAmdqjYZd2pDGvw .node.clickable{cursor:pointer;}#mermaid-svg-4SAmdqjYZd2pDGvw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4SAmdqjYZd2pDGvw .arrowheadPath{fill:#333333;}#mermaid-svg-4SAmdqjYZd2pDGvw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4SAmdqjYZd2pDGvw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4SAmdqjYZd2pDGvw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4SAmdqjYZd2pDGvw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4SAmdqjYZd2pDGvw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4SAmdqjYZd2pDGvw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster text{fill:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw .cluster span{color:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4SAmdqjYZd2pDGvw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4SAmdqjYZd2pDGvw rect.text{fill:none;stroke-width:0;}#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape p,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4SAmdqjYZd2pDGvw .icon-shape .label rect,#mermaid-svg-4SAmdqjYZd2pDGvw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4SAmdqjYZd2pDGvw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4SAmdqjYZd2pDGvw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4SAmdqjYZd2pDGvw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
是
否
选一个未访问起点
起点加入 visited
起点的边进入小根堆
堆为空?
弹出最小边
目标节点已访问?
选择该边
新节点加入 visited
新节点的边全部解锁
还有未访问节点?
结束
Kruskal 与 Prim 的区别
| 对比 | Kruskal | Prim |
|---|---|---|
| 贪心对象 | 全图最小边 | 当前生成树边界上的最小边 |
| 防环方式 | 并查集 | visited 节点集合 |
| 核心容器 | 边小根堆 + 并查集 | 边小根堆 + visited |
| 视角 | 从边出发 | 从点扩张 |
| 不连通图 | 最小生成森林 | 逐分量启动后得到森林 |
完整代码
java
package class10;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.PriorityQueue;
import java.util.Set;
/**
* Prim 最小生成树算法。
*
* 每次从"已访问节点集合"向外选择权重最小、且能引入新节点的边。
*
* 对不连通图,外层循环会从每个未访问连通分量重新启动,
* 因而返回最小生成森林。
*/
public class Code05_Prim {
public static Set<Edge> prim(Graph graph) {
Set<Edge> result = new LinkedHashSet<>();
if (graph == null || graph.nodes.isEmpty()) {
return result;
}
Set<Node> visited = new HashSet<>();
PriorityQueue<Edge> minEdgeHeap =
new PriorityQueue<>(Comparator.comparingInt(edge -> edge.weight));
// 支持森林:每个尚未访问的节点都可以作为新连通分量的起点。
for (Node start : graph.nodes.values()) {
if (!visited.add(start)) {
continue;
}
minEdgeHeap.addAll(start.edges);
while (!minEdgeHeap.isEmpty()) {
Edge edge = minEdgeHeap.poll();
Node next = edge.to;
// edge.to 已在树中,则这条边会形成环,直接跳过。
if (!visited.add(next)) {
continue;
}
result.add(edge);
// 新节点进入生成树后,解锁它能够提供的所有候选边。
minEdgeHeap.addAll(next.edges);
}
}
return result;
}
}
关于无向图的表示
这份 Node / Edge 结构中:
text
edge.from -> edge.to
本身带有方向。
因此如果要用它表达无向边:
text
A -- B
常见做法是同时建立:
text
A -> B
B -> A
Prim 才能从任意一端"解锁"到另一端。
复杂度
使用二叉堆时:
- 每条候选边最多进入堆;
- 时间复杂度通常写作 O(E log E);
- 也常根据具体实现表述为 O(E log V);
- 额外空间:O(V + E)。
易错点
- 只看最小边,不看它能否引入新节点;
- 忘记支持不连通图时重新选择起点;
- 图结构是有向边表示,却直接把它当无向图使用;
- 误以为 Prim 需要并查集:标准 Prim 用 visited 就足够防环。
答案 6:Dijkstra 单源最短路径
[返回练习 6](#返回练习 6)
题目回顾
从起点出发,计算到所有可达节点的最短距离。
前提:
text
所有边权 >= 0
一句话理解 Dijkstra
每一轮从"还没最终确定"的节点中,选当前距离最小者。
因为没有负权边,所以以后再绕路也不可能把它变得更短。
核心流程
#mermaid-svg-5UMzPJgXjRkiGjCt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5UMzPJgXjRkiGjCt .error-icon{fill:#552222;}#mermaid-svg-5UMzPJgXjRkiGjCt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5UMzPJgXjRkiGjCt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5UMzPJgXjRkiGjCt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5UMzPJgXjRkiGjCt .marker.cross{stroke:#333333;}#mermaid-svg-5UMzPJgXjRkiGjCt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5UMzPJgXjRkiGjCt p{margin:0;}#mermaid-svg-5UMzPJgXjRkiGjCt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster-label text{fill:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster-label span{color:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster-label span p{background-color:transparent;}#mermaid-svg-5UMzPJgXjRkiGjCt .label text,#mermaid-svg-5UMzPJgXjRkiGjCt span{fill:#333;color:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .node rect,#mermaid-svg-5UMzPJgXjRkiGjCt .node circle,#mermaid-svg-5UMzPJgXjRkiGjCt .node ellipse,#mermaid-svg-5UMzPJgXjRkiGjCt .node polygon,#mermaid-svg-5UMzPJgXjRkiGjCt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5UMzPJgXjRkiGjCt .rough-node .label text,#mermaid-svg-5UMzPJgXjRkiGjCt .node .label text,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape .label,#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape .label{text-anchor:middle;}#mermaid-svg-5UMzPJgXjRkiGjCt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5UMzPJgXjRkiGjCt .rough-node .label,#mermaid-svg-5UMzPJgXjRkiGjCt .node .label,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape .label,#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape .label{text-align:center;}#mermaid-svg-5UMzPJgXjRkiGjCt .node.clickable{cursor:pointer;}#mermaid-svg-5UMzPJgXjRkiGjCt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5UMzPJgXjRkiGjCt .arrowheadPath{fill:#333333;}#mermaid-svg-5UMzPJgXjRkiGjCt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5UMzPJgXjRkiGjCt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5UMzPJgXjRkiGjCt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5UMzPJgXjRkiGjCt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5UMzPJgXjRkiGjCt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5UMzPJgXjRkiGjCt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster text{fill:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt .cluster span{color:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5UMzPJgXjRkiGjCt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5UMzPJgXjRkiGjCt rect.text{fill:none;stroke-width:0;}#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape p,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5UMzPJgXjRkiGjCt .icon-shape .label rect,#mermaid-svg-5UMzPJgXjRkiGjCt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5UMzPJgXjRkiGjCt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5UMzPJgXjRkiGjCt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5UMzPJgXjRkiGjCt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
起点距离设为 0
选未确定节点中距离最小者
当前节点距离最终确定
遍历当前节点所有出边
计算 新距离 = 当前距离 + 边权
新距离更短?
更新邻居距离
保持原距离
还有未确定可达节点?
结束
什么叫"松弛"?
对边:
text
u --w--> v
如果当前已经知道:
text
dist[u]
那么经过 u 到 v 的候选距离是:
text
candidate = dist[u] + w
若:
text
candidate < dist[v]
就更新:
text
dist[v] = candidate
这一步叫做松弛(relaxation)。
朴素版为什么是 O(V² + E)?
每确定一个节点,都要扫描 distanceMap:
text
从未确定节点中找到当前距离最小者
这个查找最坏要看 O(V) 个节点,一共做 O(V) 轮,因此产生 O(V²)。
边的松弛总共 O(E)。
改写堆的三个状态
#mermaid-svg-YwYs6odnd5XQHDP4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YwYs6odnd5XQHDP4 .error-icon{fill:#552222;}#mermaid-svg-YwYs6odnd5XQHDP4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YwYs6odnd5XQHDP4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YwYs6odnd5XQHDP4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YwYs6odnd5XQHDP4 .marker.cross{stroke:#333333;}#mermaid-svg-YwYs6odnd5XQHDP4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YwYs6odnd5XQHDP4 p{margin:0;}#mermaid-svg-YwYs6odnd5XQHDP4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster-label text{fill:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster-label span{color:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster-label span p{background-color:transparent;}#mermaid-svg-YwYs6odnd5XQHDP4 .label text,#mermaid-svg-YwYs6odnd5XQHDP4 span{fill:#333;color:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .node rect,#mermaid-svg-YwYs6odnd5XQHDP4 .node circle,#mermaid-svg-YwYs6odnd5XQHDP4 .node ellipse,#mermaid-svg-YwYs6odnd5XQHDP4 .node polygon,#mermaid-svg-YwYs6odnd5XQHDP4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YwYs6odnd5XQHDP4 .rough-node .label text,#mermaid-svg-YwYs6odnd5XQHDP4 .node .label text,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape .label,#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-YwYs6odnd5XQHDP4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YwYs6odnd5XQHDP4 .rough-node .label,#mermaid-svg-YwYs6odnd5XQHDP4 .node .label,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape .label,#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape .label{text-align:center;}#mermaid-svg-YwYs6odnd5XQHDP4 .node.clickable{cursor:pointer;}#mermaid-svg-YwYs6odnd5XQHDP4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YwYs6odnd5XQHDP4 .arrowheadPath{fill:#333333;}#mermaid-svg-YwYs6odnd5XQHDP4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YwYs6odnd5XQHDP4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YwYs6odnd5XQHDP4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwYs6odnd5XQHDP4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YwYs6odnd5XQHDP4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwYs6odnd5XQHDP4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster text{fill:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 .cluster span{color:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YwYs6odnd5XQHDP4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YwYs6odnd5XQHDP4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape p,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YwYs6odnd5XQHDP4 .icon-shape .label rect,#mermaid-svg-YwYs6odnd5XQHDP4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YwYs6odnd5XQHDP4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YwYs6odnd5XQHDP4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YwYs6odnd5XQHDP4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} add
更短距离 update
pop
再次遇到 ignore
节点从未进堆
节点在堆中
最短距离已确定
为了支持 update,普通 PriorityQueue 不方便直接修改内部某个节点的优先级。
因此改写堆维护两张表:
text
indexMap:
节点 -> 当前在堆数组中的位置
-1 表示已经弹出
distanceMap:
节点 -> 当前候选最短距离
距离变小时:
text
更新 distanceMap
-> 从 indexMap 找到位置
-> heapInsert 向上调整
完整代码
java
package class10;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Dijkstra 单源最短路径。
*
* 前提:所有可达边的权重都必须 >= 0。
*/
public class Code06_Dijkstra {
/**
* 朴素版:每轮在线性扫描中找到尚未确定的最短节点。
*
* 时间复杂度:O(V^2 + E)
*/
public static HashMap<Node, Integer> dijkstra1(Node from) {
HashMap<Node, Integer> distanceMap = new HashMap<>();
if (from == null) {
return distanceMap;
}
Set<Node> selected = new HashSet<>();
distanceMap.put(from, 0);
Node minNode =
getMinDistanceNodeWithoutSelected(distanceMap, selected);
while (minNode != null) {
int baseDistance = distanceMap.get(minNode);
for (Edge edge : minNode.edges) {
checkNonNegative(edge);
Node to = edge.to;
int newDistance = safeAdd(baseDistance, edge.weight);
// 第一次发现:写入"起点到 minNode 的距离 + 当前边权";
// 再次发现:只保留更小的距离。
distanceMap.merge(to, newDistance, Math::min);
}
// minNode 的最短距离从此确定,不再被更新。
selected.add(minNode);
minNode =
getMinDistanceNodeWithoutSelected(distanceMap, selected);
}
return distanceMap;
}
private static Node getMinDistanceNodeWithoutSelected(
Map<Node, Integer> distanceMap,
Set<Node> selected) {
Node minNode = null;
int minDistance = Integer.MAX_VALUE;
for (Map.Entry<Node, Integer> entry : distanceMap.entrySet()) {
if (!selected.contains(entry.getKey())
&& entry.getValue() < minDistance) {
minNode = entry.getKey();
minDistance = entry.getValue();
}
}
return minNode;
}
/**
* 加强版:使用支持 decrease-key 的小根堆。
*
* 时间复杂度:O((V + E) log V)
*
* @param from 起点
* @param initialCapacity 初始堆容量;容量不足时会自动扩容
*/
public static HashMap<Node, Integer> dijkstra2(
Node from,
int initialCapacity) {
HashMap<Node, Integer> result = new HashMap<>();
if (from == null) {
return result;
}
NodeHeap heap = new NodeHeap(initialCapacity);
heap.addOrUpdateOrIgnore(from, 0);
while (!heap.isEmpty()) {
Record record = heap.pop();
// 从堆中弹出的节点,其最短距离已经确定。
result.put(record.node, record.distance);
for (Edge edge : record.node.edges) {
checkNonNegative(edge);
int newDistance =
safeAdd(record.distance, edge.weight);
heap.addOrUpdateOrIgnore(edge.to, newDistance);
}
}
return result;
}
private static void checkNonNegative(Edge edge) {
if (edge.weight < 0) {
throw new IllegalArgumentException(
"Dijkstra does not allow negative edge weights.");
}
}
private static int safeAdd(int a, int b) {
long sum = (long) a + b;
if (sum > Integer.MAX_VALUE) {
throw new ArithmeticException(
"Shortest-path distance exceeds Integer.MAX_VALUE.");
}
return (int) sum;
}
/**
* 支持三种操作的改写小根堆:
* 1. add:从未入堆 -> 加入;
* 2. update:仍在堆中且距离变小 -> 更新并上浮;
* 3. ignore:已经弹出,或新距离不更优 -> 忽略。
*/
public static class NodeHeap {
private Node[] nodes;
private final Map<Node, Integer> indexMap;
private final Map<Node, Integer> distanceMap;
private int size;
public NodeHeap(int initialCapacity) {
nodes = new Node[Math.max(1, initialCapacity)];
indexMap = new HashMap<>();
distanceMap = new HashMap<>();
}
public void addOrUpdateOrIgnore(Node node, int distance) {
if (!hasEntered(node)) {
ensureCapacity(size + 1);
nodes[size] = node;
indexMap.put(node, size);
distanceMap.put(node, distance);
heapInsert(size);
size++;
return;
}
if (inHeap(node)
&& distance < distanceMap.get(node)) {
distanceMap.put(node, distance);
heapInsert(indexMap.get(node));
}
// 已经弹出,或新距离没有更短:忽略。
}
public Record pop() {
if (isEmpty()) {
throw new IllegalStateException("Heap is empty.");
}
Node minNode = nodes[0];
int minDistance = distanceMap.get(minNode);
swap(0, size - 1);
Node removed = nodes[size - 1];
indexMap.put(removed, -1);
distanceMap.remove(removed);
nodes[size - 1] = null;
size--;
if (size > 0) {
heapify(0);
}
return new Record(minNode, minDistance);
}
private void heapInsert(int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (distanceMap.get(nodes[index])
>= distanceMap.get(nodes[parent])) {
break;
}
swap(index, parent);
index = parent;
}
}
private void heapify(int index) {
int left = index * 2 + 1;
while (left < size) {
int right = left + 1;
int smallest =
right < size
&& distanceMap.get(nodes[right])
< distanceMap.get(nodes[left])
? right
: left;
if (distanceMap.get(nodes[index])
<= distanceMap.get(nodes[smallest])) {
break;
}
swap(index, smallest);
index = smallest;
left = index * 2 + 1;
}
}
private void swap(int i, int j) {
if (i == j) {
return;
}
Node tmp = nodes[i];
nodes[i] = nodes[j];
nodes[j] = tmp;
indexMap.put(nodes[i], i);
indexMap.put(nodes[j], j);
}
private boolean hasEntered(Node node) {
return indexMap.containsKey(node);
}
private boolean inHeap(Node node) {
return hasEntered(node) && indexMap.get(node) >= 0;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity(int required) {
if (required <= nodes.length) {
return;
}
int newCapacity =
Math.max(required, nodes.length * 2);
nodes = Arrays.copyOf(nodes, newCapacity);
}
}
public static class Record {
public final Node node;
public final int distance;
public Record(Node node, int distance) {
this.node = node;
this.distance = distance;
}
}
}
为什么 Dijkstra 不能有负权边?
假设某个节点 A 已经以距离 5 被确定。
Dijkstra 的逻辑依赖:
text
未来从更远节点绕回来,
只会让距离更大,不会让 5 变小。
如果允许负权边:
text
更远节点 -> A 的边权 = -10
就可能在 A 已经"确定"以后,突然出现更短路径。
这会破坏贪心正确性的基础。
复杂度
朴素版
- 时间:O(V² + E);
- 空间:O(V)。
改写堆版
- 每个节点入堆 / 出堆;
- 每次有效距离下降触发上浮;
- 时间复杂度:O((V + E) log V);
- 额外空间:O(V)。
易错点
- 忘记加上
dist[current],只记录当前边权; - 在负权图上使用 Dijkstra;
- 把"被发现"误认为"最短距离已经确定";
- 自定义堆更新距离后忘记上浮;
- 节点弹出后没有标记为"已确定",导致再次进入堆;
int距离相加可能溢出,因此代码中用long做中间计算并主动检查。
第三部分:把 6 道题串起来理解
1. "候选集合"是图算法的核心
| 算法 | 候选集合 | 每次取谁 | 如何防止错误 |
|---|---|---|---|
| BFS | 队列 | 最早发现的节点 | visited |
| DFS | 栈 | 最近发现的节点 | visited |
| 拓扑排序 | 零入度队列 | 当前无依赖节点 | 剩余入度 |
| Kruskal | 全图候选边 | 全局最小边 | 并查集防环 |
| Prim | 当前边界候选边 | 边界最小边 | visited 防环 |
| Dijkstra | 候选距离 | 当前距离最小节点 | selected / 出堆确定 |
#mermaid-svg-yg6aFkjXOaqPGPHS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-yg6aFkjXOaqPGPHS .error-icon{fill:#552222;}#mermaid-svg-yg6aFkjXOaqPGPHS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-yg6aFkjXOaqPGPHS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-yg6aFkjXOaqPGPHS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-yg6aFkjXOaqPGPHS .marker.cross{stroke:#333333;}#mermaid-svg-yg6aFkjXOaqPGPHS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-yg6aFkjXOaqPGPHS p{margin:0;}#mermaid-svg-yg6aFkjXOaqPGPHS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster-label text{fill:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster-label span{color:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster-label span p{background-color:transparent;}#mermaid-svg-yg6aFkjXOaqPGPHS .label text,#mermaid-svg-yg6aFkjXOaqPGPHS span{fill:#333;color:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .node rect,#mermaid-svg-yg6aFkjXOaqPGPHS .node circle,#mermaid-svg-yg6aFkjXOaqPGPHS .node ellipse,#mermaid-svg-yg6aFkjXOaqPGPHS .node polygon,#mermaid-svg-yg6aFkjXOaqPGPHS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-yg6aFkjXOaqPGPHS .rough-node .label text,#mermaid-svg-yg6aFkjXOaqPGPHS .node .label text,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape .label,#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape .label{text-anchor:middle;}#mermaid-svg-yg6aFkjXOaqPGPHS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-yg6aFkjXOaqPGPHS .rough-node .label,#mermaid-svg-yg6aFkjXOaqPGPHS .node .label,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape .label,#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape .label{text-align:center;}#mermaid-svg-yg6aFkjXOaqPGPHS .node.clickable{cursor:pointer;}#mermaid-svg-yg6aFkjXOaqPGPHS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-yg6aFkjXOaqPGPHS .arrowheadPath{fill:#333333;}#mermaid-svg-yg6aFkjXOaqPGPHS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-yg6aFkjXOaqPGPHS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-yg6aFkjXOaqPGPHS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yg6aFkjXOaqPGPHS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-yg6aFkjXOaqPGPHS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yg6aFkjXOaqPGPHS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster text{fill:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS .cluster span{color:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-yg6aFkjXOaqPGPHS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-yg6aFkjXOaqPGPHS rect.text{fill:none;stroke-width:0;}#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape p,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-yg6aFkjXOaqPGPHS .icon-shape .label rect,#mermaid-svg-yg6aFkjXOaqPGPHS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-yg6aFkjXOaqPGPHS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-yg6aFkjXOaqPGPHS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-yg6aFkjXOaqPGPHS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} BFS DFS
控制访问顺序
拓扑排序
控制依赖消除顺序
Kruskal Prim
控制安全最小边选择
Dijkstra
控制最短距离确定顺序
2. visited 的语义并不总一样
BFS / DFS
text
visited = 已经发现,不要重复加入容器
Prim
text
visited = 已经进入生成树
Dijkstra
朴素版中的 selected:
text
selected = 最短距离已经最终确定
所以看到一个 Set<Node> 时,不要机械理解成"访问过"。
先问:
这个集合到底承诺了什么不变量?
3. 三种贪心为什么都能"选当前最小"?
Kruskal、Prim、Dijkstra 都在选某种"当前最小",但对象不同:
text
Kruskal:全局剩余安全边里的最小边
Prim:当前生成树切分边界上的最小边
Dijkstra:当前未确定节点中的最短距离
它们看起来相似,但正确性依据不同。
不要把模板记成:
text
"小根堆 = 贪心"
而应该记成:
text
先证明"当前最小可以安全地被确定"
再决定用什么数据结构高效找到它
第四部分:统一图结构
今天使用的三个基础类可以整理为:
Node.java
java
package class10;
import java.util.ArrayList;
import java.util.List;
/**
* 图中的点。
*/
public class Node {
public int value; // 节点标识
public int in; // 入度
public int out; // 出度
public List<Node> nexts; // 从当前点可直接到达的节点
public List<Edge> edges; // 从当前点出发的边
public Node(int value) {
this.value = value;
this.in = 0;
this.out = 0;
this.nexts = new ArrayList<>();
this.edges = new ArrayList<>();
}
}
Edge.java
java
package class10;
/**
* 图中的边。
*/
public class Edge {
public Node from;
public Node to;
public int weight;
public Edge(Node from, Node to, int weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
}
Graph.java
java
package class10;
import java.util.HashMap;
import java.util.HashSet;
/**
* 图结构。
*/
public class Graph {
public HashMap<Integer, Node> nodes; // 点集
public HashSet<Edge> edges; // 边集
public Graph() {
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
结构关系图
#mermaid-svg-96QQALv0qutFQtA8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-96QQALv0qutFQtA8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-96QQALv0qutFQtA8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-96QQALv0qutFQtA8 .error-icon{fill:#552222;}#mermaid-svg-96QQALv0qutFQtA8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-96QQALv0qutFQtA8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-96QQALv0qutFQtA8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-96QQALv0qutFQtA8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-96QQALv0qutFQtA8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-96QQALv0qutFQtA8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-96QQALv0qutFQtA8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-96QQALv0qutFQtA8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-96QQALv0qutFQtA8 .marker.cross{stroke:#333333;}#mermaid-svg-96QQALv0qutFQtA8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-96QQALv0qutFQtA8 p{margin:0;}#mermaid-svg-96QQALv0qutFQtA8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-96QQALv0qutFQtA8 .cluster-label text{fill:#333;}#mermaid-svg-96QQALv0qutFQtA8 .cluster-label span{color:#333;}#mermaid-svg-96QQALv0qutFQtA8 .cluster-label span p{background-color:transparent;}#mermaid-svg-96QQALv0qutFQtA8 .label text,#mermaid-svg-96QQALv0qutFQtA8 span{fill:#333;color:#333;}#mermaid-svg-96QQALv0qutFQtA8 .node rect,#mermaid-svg-96QQALv0qutFQtA8 .node circle,#mermaid-svg-96QQALv0qutFQtA8 .node ellipse,#mermaid-svg-96QQALv0qutFQtA8 .node polygon,#mermaid-svg-96QQALv0qutFQtA8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-96QQALv0qutFQtA8 .rough-node .label text,#mermaid-svg-96QQALv0qutFQtA8 .node .label text,#mermaid-svg-96QQALv0qutFQtA8 .image-shape .label,#mermaid-svg-96QQALv0qutFQtA8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-96QQALv0qutFQtA8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-96QQALv0qutFQtA8 .rough-node .label,#mermaid-svg-96QQALv0qutFQtA8 .node .label,#mermaid-svg-96QQALv0qutFQtA8 .image-shape .label,#mermaid-svg-96QQALv0qutFQtA8 .icon-shape .label{text-align:center;}#mermaid-svg-96QQALv0qutFQtA8 .node.clickable{cursor:pointer;}#mermaid-svg-96QQALv0qutFQtA8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-96QQALv0qutFQtA8 .arrowheadPath{fill:#333333;}#mermaid-svg-96QQALv0qutFQtA8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-96QQALv0qutFQtA8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-96QQALv0qutFQtA8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-96QQALv0qutFQtA8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-96QQALv0qutFQtA8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-96QQALv0qutFQtA8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-96QQALv0qutFQtA8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-96QQALv0qutFQtA8 .cluster text{fill:#333;}#mermaid-svg-96QQALv0qutFQtA8 .cluster span{color:#333;}#mermaid-svg-96QQALv0qutFQtA8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-96QQALv0qutFQtA8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-96QQALv0qutFQtA8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-96QQALv0qutFQtA8 .icon-shape,#mermaid-svg-96QQALv0qutFQtA8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-96QQALv0qutFQtA8 .icon-shape p,#mermaid-svg-96QQALv0qutFQtA8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-96QQALv0qutFQtA8 .icon-shape .label rect,#mermaid-svg-96QQALv0qutFQtA8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-96QQALv0qutFQtA8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-96QQALv0qutFQtA8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-96QQALv0qutFQtA8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Graph
Node 集合
Edge 集合
nexts 邻接点
edges 出边
from
to
weight
第五部分:建议测试用例
图算法特别适合用"小图手推 + 边界图"测试。
BFS / DFS
text
1. root == null
2. 单节点
3. 一条链
4. 一个节点指向多个邻居
5. 多个节点共同指向同一个节点
6. 存在自环
7. 存在大环:1 -> 2 -> 3 -> 1
8. 图不连通:确认只遍历 root 可达部分
拓扑排序
text
1. 空图
2. 单节点
3. 一条依赖链
4. 多个零入度起点
5. 一个节点依赖多个前驱
6. DAG 有多种合法拓扑序
7. 2 节点环:1 -> 2 -> 1
8. 自环:1 -> 1
Kruskal / Prim
text
1. 单节点
2. 两点一边
3. 三角形:检查是否跳过成环边
4. 多条相同权重边
5. 连通图
6. 不连通图:检查最小生成森林
7. 无向边使用双向 Edge 表示
8. 含负权边:MST 算法本身可以处理负权
Dijkstra
text
1. 单节点
2. 只有一条边
3. 同一节点存在多条候选路径
4. 后发现的路径更短,需要松弛更新
5. 存在不可达节点
6. 边权为 0
7. 出现负权边:应拒绝使用 Dijkstra
8. 距离累加接近 Integer.MAX_VALUE
今日自测
1. BFS 为什么推荐在"入队时"就标记 visited?
查看答案
因为一个节点可能被多个前驱同时发现。第一次发现时立即标记,可以保证它只进入队列一次,避免重复入队和重复处理。
2. DFS 为什么经常倒序压栈邻居?
查看答案
栈是后进先出。若希望邻接表中靠前的节点先被弹出,就需要让靠后的节点先入栈,因此通常从 nexts 末尾向前压栈。
3. 拓扑排序为什么能用结果长度检测环?
查看答案
有向环中的节点无法通过删除环外节点把剩余入度全部降为 0,因此不会进入零入度队列。若最终结果节点数少于总节点数,就说明还有节点无法被消除,图中存在环。
4. Kruskal 为什么需要并查集?
查看答案
每次尝试加入最小边时,需要快速判断两个端点是否已经连通。若已连通,再加入该边必然形成环;并查集正好高效维护这种动态连通性。
5. Prim 为什么不需要并查集?
查看答案
Prim 始终维护"已经进入生成树的节点集合"。候选边若指向已访问节点就丢弃,指向新节点才接纳,因此 visited 已经足够完成防环判断。
6. Dijkstra 中"发现一个节点"和"确定一个节点"有什么区别?
查看答案
发现只表示目前存在一条到达它的路径,因此得到的是候选距离;当它成为所有未确定节点中距离最小者并被选中或从最小堆弹出后,才表示最短距离最终确定。
7. Dijkstra 第一次发现邻居时为什么不能只保存边权?
查看答案
边权只表示 current 到 next 的局部代价。起点到 next 的完整候选距离必须是 distcurrent + edge.weight。
8. Kruskal、Prim、Dijkstra 的"最小"分别是什么?
查看答案
Kruskal 选全局剩余安全边中的最小边;Prim 选当前生成树边界上的最小边;Dijkstra 选当前未确定节点中候选距离最小的节点。
加餐挑战
- 把 BFS 改成返回
List<Node>,不要直接打印; - 为 BFS 增加
distance,返回起点到每个节点的无权最短距离; - 用递归方式实现 DFS,并比较显式栈与递归栈;
- 拓扑排序除了返回顺序,再返回一个
hasCycle状态; - 用优先队列重写 Dijkstra,但允许重复
Record入堆,通过"过期记录检查"避免自定义 decrease-key; - 比较
O(E log E)的 Kruskal 与堆版 Prim 在稀疏图、稠密图中的表现; - 思考:如果图中存在负权边但没有负环,应该选择什么最短路算法?
- 尝试实现 Bellman-Ford,并与 Dijkstra 的"确定节点"思想做对比。
挑战 5 提示:允许重复候选
普通 PriorityQueue 不支持 decrease-key 时,可以直接把更短的新 Record 再压入堆。弹出时如果该 Record 的距离已经不是当前最佳距离,就把它视为过期记录并跳过。
Day 18 复盘清单
- 我能写出
Queue + visited的图 BFS - 我知道 BFS 中节点为什么只应该输出一次
- 我能用显式栈写 DFS,并解释为什么倒序压栈
- 我能写出 Kahn 拓扑排序,并检测有向环
- 我知道拓扑排序为什么不应该直接修改原始入度
- 我能解释 Kruskal 中并查集的作用
- 我能写出路径压缩与按大小合并
- 我能解释 Prim 的"点解锁边"过程
- 我能说清 Kruskal 与 Prim 的候选集合差异
- 我能写出 Dijkstra 的松弛公式
- 我知道第一次发现邻居时必须加上当前节点距离
- 我能解释 Dijkstra 为什么不能处理负权边
- 我能理解改写堆的
add / update / ignore - 我能说清 BFS、DFS、MST、最短路中
visited的不同语义
一句话总结: Day 18 真正要建立的是"图上的候选状态"意识:BFS 用队列控制扩散顺序,DFS 用栈控制深入顺序,拓扑排序用入度控制依赖消除,Kruskal 与 Prim 用安全的最小边扩张结构,而 Dijkstra 用最小候选距离逐步确定最短路。