【012】图与最短路径:了解即可

前两篇(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 运行时数据区,对象创建与内存布局。

相关推荐
比特森林探险记1 小时前
【无标题】
java·前端
椰猫子2 小时前
Javaweb(Filter、Listener、AJAX、JSON)
java·开发语言
j_xxx404_2 小时前
C++算法:哈希表(简介|两数之和|判断是否互为字符重排)
数据结构·c++·算法·leetcode·蓝桥杯·力扣·散列表
朝新_2 小时前
【Spring AI 】核心知识体系梳理:从入门到实战
java·人工智能·spring
一 乐2 小时前
旅游|基于springboot + vue旅游信息推荐系统(源码+数据库+文档)
java·vue.js·spring boot·论文·旅游·毕设·旅游信息推荐系统
脱氧核糖核酸__3 小时前
LeetCode热题100——189.轮转数组(题解+答案+要点)
数据结构·c++·算法·leetcode
我命由我123453 小时前
Android 开发中,关于 Gradle 的 distributionUrl 的一些问题
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
橙露3 小时前
SpringBoot 全局异常处理:优雅封装统一返回格式
java·spring boot·后端
贾斯汀玛尔斯3 小时前
每天学一个算法-快速排序(Quick Sort)
数据结构·算法