前两篇(010、011)把数组、链表、哈希表、树四种基础结构聊完了。这篇收个尾------图。图在日常业务代码里出现频率不如数组和树高,但在某些场景下会碰到:依赖治理、任务编排、社交网络、推荐系统。
这篇定位是「了解即可」:知道图长什么样、什么时候可能用到、遇到这类问题知道往哪个方向查。不需要深入算法细节,那是算法工程师的事。下面我按「图的基本概念 → 遍历 → 拓扑排序 → 最短路径 → 业务场景」的顺序快速过一遍。
1. 图的基本概念 📐
1.1 图是什么
图(Graph)由**节点(Vertex)和边(Edge)**组成:
text
无向图(边没有方向):
A ------ B
| |
C ------ D
有向图(边有方向):
A → B
↑ ↓
C ← D
术语:
| 术语 | 含义 |
|---|---|
| 顶点(Vertex) | 图中的节点 |
| 边(Edge) | 顶点之间的连接 |
| 度(Degree) | 连接的边数(无向图) |
| 入度/出度 | 有向图中进入/离开该顶点的边数 |
| 路径(Path) | 顶点序列 |
| 环(Cycle) | 起点和终点相同的路径 |
1.2 图的存储
java
// 邻接表(常用,空间 O(V + E))
Map<String, List<String>> graph = new HashMap<>();
graph.put("A", Arrays.asList("B", "C"));
graph.put("B", Arrays.asList("A", "D"));
graph.put("C", Arrays.asList("A", "D"));
graph.put("D", Arrays.asList("B", "C"));
// 邻接矩阵(稠密图,空间 O(V²))
int[][] matrix = {
{0, 1, 1, 0},
{1, 0, 0, 1},
{1, 0, 0, 1},
{0, 1, 1, 0}
};
1.3 有向无环图(DAG)
DAG = 有向图 + 无环。这是业务中最常见的图类型:
text
任务依赖图(A 完成后才能做 B):
A → B → D
↓ ↓
C → E
特点:可以拓扑排序,没有无限循环。
2. 遍历:BFS 与 DFS 🔍
2.1 广度优先搜索(BFS)
一层一层遍历,用队列:
java
// BFS 模板
public void bfs(String start) {
Queue<String> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
String node = queue.poll();
System.out.println(node); // 处理节点
for (String neighbor : graph.getOrDefault(node, Collections.emptyList())) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
}
特点:
- 最短路径(无权图)
- 用队列,层层推进
- 适合「找最近」的场景
2.2 深度优先搜索(DFS)
一条路走到黑,用栈或递归:
java
// DFS 递归模板
public void dfs(String node, Set<String> visited) {
if (visited.contains(node)) return;
visited.add(node);
System.out.println(node); // 处理节点
for (String neighbor : graph.getOrDefault(node, Collections.emptyList())) {
dfs(neighbor, visited);
}
}
// 调用
dfs("A", new HashSet<>());
特点:
- 用递归或栈
- 适合「找所有路径」「检测环」
- 递归深度可能很深(注意 StackOverflow)
2.3 BFS vs DFS 对比
| 场景 | BFS | DFS |
|---|---|---|
| 最短路径(无权) | ✅ | ❌ |
| 检测环 | ✅ | ✅ |
| 拓扑排序 | ✅ | ✅ |
| 找所有路径 | ❌ | ✅ |
| 内存占用 | O(宽度) | O(深度) |
3. 拓扑排序:DAG 的线性排序 📋
3.1 什么是拓扑排序
对 DAG 的顶点进行排序,使得对于每条有向边 (u → v),u 都在 v 前面。
text
任务依赖:A → B → D
A → C → E
拓扑排序结果:A, C, B, E, D(一种合法顺序)
3.2 Kahn 算法(基于入度)
java
public List<String> topologicalSort(Map<String, List<String>> graph) {
// 1. 计算入度
Map<String, Integer> inDegree = new HashMap<>();
for (String node : graph.keySet()) {
inDegree.putIfAbsent(node, 0);
for (String neighbor : graph.get(node)) {
inDegree.merge(neighbor, 1, Integer::sum);
}
}
// 2. 入度为 0 的节点入队
Queue<String> queue = new LinkedList<>();
for (String node : graph.keySet()) {
if (inDegree.get(node) == 0) queue.offer(node);
}
// 3. BFS 拓扑排序
List<String> result = new ArrayList<>();
while (!queue.isEmpty()) {
String node = queue.poll();
result.add(node);
for (String neighbor : graph.getOrDefault(node, Collections.emptyList())) {
inDegree.merge(neighbor, -1, Integer::sum);
if (inDegree.get(neighbor) == 0) {
queue.offer(neighbor);
}
}
}
// 检测环(结果数量 < 节点数量说明有环)
if (result.size() != graph.size()) {
throw new IllegalStateException("图中存在环,无法拓扑排序");
}
return result;
}
3.3 业务场景
| 场景 | 为什么需要拓扑排序 |
|---|---|
| 任务调度 | 确保依赖的任务先执行 |
| 编译依赖 | 头文件/模块依赖顺序 |
| 报表生成 | 数据准备 → 统计 → 导出 |
| CI/CD 流水线 | 构建 → 测试 → 部署 |
4. 最短路径算法 🛣️
4.1 BFS(无权图)
如果边没有权重,BFS 天然找到最短路径:
java
// BFS 找最短路径
public int shortestPath(String start, String target) {
if (start.equals(target)) return 0;
Queue<String> queue = new LinkedList<>();
Map<String, Integer> dist = new HashMap<>();
queue.offer(start);
dist.put(start, 0);
while (!queue.isEmpty()) {
String node = queue.poll();
int d = dist.get(node);
for (String neighbor : graph.getOrDefault(node, Collections.emptyList())) {
if (!dist.containsKey(neighbor)) {
dist.put(neighbor, d + 1);
if (neighbor.equals(target)) return d + 1;
queue.offer(neighbor);
}
}
}
return -1; // 不连通
}
4.2 Dijkstra 算法(有权图,非负权重)
java
// Dijkstra 找最短路径
public Map<String, Integer> dijkstra(String start) {
// 距离表
Map<String, Integer> dist = new HashMap<>();
// 优先级队列(按距离从小到大)
PriorityQueue<String> pq = new PriorityQueue<>(
Comparator.comparingInt(s -> dist.getOrDefault(s, Integer.MAX_VALUE)));
dist.put(start, 0);
pq.offer(start);
while (!pq.isEmpty()) {
String node = pq.poll();
int d = dist.get(node);
for (Map.Entry<String, Integer> edge : graph.getOrDefault(node, Map.of()).entrySet()) {
String neighbor = edge.getKey();
int weight = edge.getValue();
if (dist.getOrDefault(neighbor, Integer.MAX_VALUE) > d + weight) {
dist.put(neighbor, d + weight);
pq.offer(neighbor);
}
}
}
return dist;
}
特点:
- 适合非负权重边
- 单源最短路径
- 时间复杂度 O(E log V)
4.3 业务场景
| 场景 | 算法 | 原因 |
|---|---|---|
| 地图导航 | Dijkstra / A* | 路径规划 |
| 网络路由 | Dijkstra | OSPF 协议 |
| 社交网络「几度关系」 | BFS | 无权图最短路径 |
| 任务调度(带耗时) | 加权 DAG + DP | DAG 可拓扑排序 |
5. 业务中什么时候会碰到图 📊
5.1 依赖治理
java
// 场景:微服务之间的依赖关系
// 服务 A 调用 B,B 调用 C,C 调用 D
// 部署/升级时需要按依赖顺序
public class ServiceDependency {
private String service;
private List<String> dependsOn; // 依赖的服务列表
}
// 拓扑排序决定部署顺序
List<String> deployOrder = topologicalSort(dependencyGraph);
5.2 任务编排
java
// 场景:数据处理流水线
// 任务 A(数据采集)→ 任务 B(清洗)→ 任务 C(统计)→ 任务 D(报表)
// DAG 描述任务依赖
// 调度器按拓扑顺序执行
5.3 社交关系
java
// 场景:好友推荐
// A 的好友是 B,B 的好友是 C
// A 和 C 可能认识(共同好友数)
// BFS 找「几度关系」
int degree = bfsDegree(userA, userC);
5.4 推荐系统
java
// 场景:商品推荐
// 用户 → 商品 → 用户(购买过相似商品的用户也买了)
// 构建用户-商品二部图,用图算法推荐
5.5 什么时候绕开图
| 场景 | 绕开方式 |
|---|---|
| 简单层级(树) | 用树结构,不用图 |
| 线性依赖 | 用列表 + 顺序字段 |
| 简单依赖(只有一层) | 用外键 + 排序字段 |
原则:能用树就不用图,能用列表就不用图。图算法复杂度和实现成本高,先评估是否真的需要。
6. 常见图库与工具 🛠️
如果业务真的需要图算法,可以用现成库:
| 库 | 语言 | 特点 |
|---|---|---|
| JGraphT | Java | 纯 Java,图算法全 |
| GraphStream | Java | 动态图,可视化 |
| NetworkX | Python | Python 图算法首选 |
| Neo4j | - | 图数据库,持久化 + 查询 |
| Redis Graph | - | Redis 模块,支持 Gremlin |
Java 示例(JGraphT):
java
// JGraphT 依赖
// <dependency>
// <groupId>org.jgrapht</groupId>
// <artifactId>jgrapht-core</artifactId>
// <version>1.5.2</version>
// </dependency>
import org.jgrapht.Graph;
import org.jgrapht.graph.DefaultDirectedGraph;
import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
Graph<String, DefaultEdge> graph = new DefaultDirectedGraph<>(DefaultEdge.class);
graph.addVertex("A");
graph.addVertex("B");
graph.addEdge("A", "B");
DijkstraShortestPath<String, DefaultEdge> dijkstra =
new DijkstraShortestPath<>(graph);
double distance = dijkstra.getPath("A", "B").getLength();
小结 💡
- 图由顶点和边组成,分为有向图和无向图。DAG(有向无环图)是业务中最常见的图类型。
- 遍历:BFS(队列)找最短路径,DFS(递归/栈)找所有路径或检测环。
- 拓扑排序:对 DAG 的顶点排序,确保依赖在前。Kahn 算法基于入度,是任务调度的核心。
- 最短路径:BFS 用于无权图,Dijkstra 用于非负权重的有权图。
- 业务场景:依赖治理、任务编排、社交网络、推荐系统。大多数业务用树或列表就能解决,图是最后的选择。
- 工具:JGraphT(Java)、NetworkX(Python)、Neo4j(图数据库)。
P0 阶段(001~012)总结 📚
从网络协议(HTTP、TLS、TCP/IP、DNS)到 Java 线程与进程,从 Linux 基础到数据结构(数组、链表、哈希表、树),P0 阶段为你搭建了「计算机基础 + Java 基础」的骨架。
接下来的 P1 阶段(013~032) 将深入 Java 语言本身:JVM 内存模型、GC、类加载、并发编程(synchronized、volatile、j.u.c、线程池)。这些是写出高性能、无 bug Java 代码的核心。
P0 阶段的文章编号与主题回顾:
| 编号 | 主题 |
|---|---|
| 001 | HTTP 方法、状态码、Header、请求链路 |
| 002 | HTTPS 证书、TLS 握手 |
| 003 | TCP/IP 分层、超时 |
| 004 | DNS 解析、域名 |
| 005 | REST 风格、Controller 设计 |
| 006 | WebSocket 场景与鉴权 |
| 007 | 进程与线程、Java 线程池 |
| 008 | Linux 权限、进程、端口、日志 |
| 009 | 复杂度直觉、大 O |
| 010 | 数组、链表、哈希表、集合选型 |
| 011 | 树与排序、B+ 树、TopK |
| 012 | 图与最短路径、DAG、拓扑排序 |
P1 阶段预告(013) ☕:Java 内存模型鸟瞰------栈、堆、方法区、JVM 运行时数据区,对象创建与内存布局。