LeetCode 图论四题精讲:BFS、拓扑排序、Trie 树的模板与优化

文章目录

前言

刷 LeetCode 图论题的时候,你会发现很多题思路一样,只是换了个壳。这篇文章用四道经典题(岛屿数量、腐烂橘子、课程表、Trie 树),帮你串起 BFS/DFS 遍历、多源 BFS、拓扑排序、前缀树这几块高频考点。读完你会发现,图论题的模板就那几个,剩下的都是细节优化。


一、200. 岛屿数量 ------ 网格图 DFS/BFS 的模板题

题目本质

给你一个 m x nchar[][] 网格,'1' 是陆地、'0' 是水。求岛屿数量(上下左右相邻的陆地算同一座岛)。

本质:求无向图的连通分量个数。

核心技巧:入队即标记

BFS 写法最容易踩的坑就是重复入队 。比如一个 '1' 同时被上面和左边的邻居发现,两次加入队列,队列就膨胀了。

复制代码
错误写法:出队时才标记
入队 → 入队 → 入队 → ... → 出队 → 标记

正确写法:入队时就标记
入队并标记 → 后续邻居发现它已经是 '0',跳过
java 复制代码
// 标准 BFS 模板,关键就一行:grid[x][y] = '0' 在 offer 之前
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{i, j});
grid[i][j] = '0';  // ← 入队即标记!

while (!q.isEmpty()) {
    int[] cur = q.poll();
    for (int[] d : dirs) {
        int x = cur[0] + d[0], y = cur[1] + d[1];
        if (x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == '1') {
            grid[x][y] = '0';  // ← 入队前标记
            q.offer(new int[]{x, y});
        }
    }
}

DFS 更简单,递归天然避免重复入队问题,四行代码搞定。

网格遍历和普通图遍历的唯一区别:邻居从邻接表变成了四个方向数组。


二、994. 腐烂橘子 ------ 多源 BFS

题目本质

每分钟所有烂橘子会同时感染上下左右的新鲜橘子。求全部腐烂需要几分钟,如果最后还有新鲜橘子返回 -1。

本质:多源 BFS 的最短时间 / 传播层数。

和单源 BFS 的区别

只有一个区别------初始化时把所有起点一起入队

java 复制代码
// 所有烂橘子同时作为第 0 层入队
for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
        if (grid[i][j] == 2) q.offer(new int[]{i, j});
        else if (grid[i][j] == 1) fresh++;
    }
}

两个优化点

fresh 计数器代替最后全盘扫描

java 复制代码
// ❌ 慢:最后 O(n*m) 扫一遍
for (int i = 0; i < n; i++)
    for (int j = 0; j < m; j++)
        if (grid[i][j] == 1) return -1;

// ✅ 快:O(1) 判断
return fresh == 0 ? minutes : -1;

int[] 代替自定义 Node:减少 GC 开销,LeetCode 上能差出 10-20ms。

java 复制代码
// ❌ class Node { int x, y; }
// ✅ new int[]{i, j}

三、207. 课程表 ------ 拓扑排序判环

题目本质

给你 n 门课和 prerequisites 数组([a, b] 表示先修 b 才能修 a),问能不能修完所有课。

本质:有向图判环。DAG(有向无环图)才能拓扑排序,有环就无法排完。

Kahn 算法(BFS 版拓扑排序)

入度为 0 的课可以直接修,修完后把它指向的课的入度减 1,重复此过程。最后看修的课数是否等于总数。

java 复制代码
int[] indegree = new int[numCourses];
List<Integer>[] adj = new List[numCourses];  // 邻接表

// 建图
for (int[] p : prerequisites) {
    indegree[p[0]]++;
    adj[p[1]].add(p[0]);
}

// BFS:入度为 0 的入队
Deque<Integer> q = new ArrayDeque<>();
for (int i = 0; i < numCourses; i++)
    if (indegree[i] == 0) q.offer(i);

// 逐层剥离
while (!q.isEmpty()) {
    int cur = q.poll();
    cnt++;
    for (int next : adj[cur]) {
        if (--indegree[next] == 0) q.offer(next);
    }
}
return cnt == numCourses;

两个性能坑

Map<Integer, List<Integer>>List<Integer>[] 数组:HashMap 查表开销远大于数组直接索引。

别用 getOrDefaultmap.getOrDefault(key, new ArrayList<>()) 即使 key 存在也会先 new ArrayList<>() 然后丢弃,白白创建垃圾对象。


四、208. 实现 Trie ------ 多叉树的变种

题目本质

实现前缀树,支持插入、搜索单词、搜索前缀。

本质:26 叉树。每个节点有 26 个子节点指针 + 一个 isEnd 标记。

数据结构选型

java 复制代码
// ❌ List<Trie>:get/set 有方法调用开销,还要构造函数初始化 26 个 null
// ✅ Trie[]:直接内存访问,数组元素默认就是 null

class Trie {
    Trie[] children = new Trie[26];
    boolean isEnd = false;
}

三个操作的逻辑

java 复制代码
// insert:逐字符往下走,没有就创建,最后标 isEnd
public void insert(String word) {
    Trie node = this;
    for (char c : word.toCharArray()) {
        int idx = c - 'a';
        if (node.children[idx] == null)
            node.children[idx] = new Trie();
        node = node.children[idx];
    }
    node.isEnd = true;
}

// search:必须走到底且 isEnd 为 true
public boolean search(String word) {
    Trie node = this;
    for (char c : word.toCharArray()) {
        node = node.children[c - 'a'];
        if (node == null) return false;
    }
    return node.isEnd;
}

// startsWith:走到底即可,不要求 isEnd
public boolean startsWith(String prefix) {
    Trie node = this;
    for (char c : prefix.toCharArray()) {
        node = node.children[c - 'a'];
        if (node == null) return false;
    }
    return true;
}

Trie 虽然不常被归入图论,但它本质是多叉树------而树是特殊的有向无环图。把它和前三道放在一起,图/树的遍历套路就完整了。


总结:四道题的共通模式

核心算法 题号 关键技巧
DFS/BFS 连通分量 200 入队即标记
多源 BFS 994 所有起点一起入队 + fresh 计数
拓扑排序 207 数组邻接表 + 别用 getOrDefault
前缀树 208 Trie\[\] 代替 List

你会发现,图论题的优化往往不是算法层面的(算法已经是 O(V+E) 最优),而是Java 实现细节 ------int[] vs Node 类、数组 vs HashMap、入队时标记 vs 出队时标记。把这些细节吃透,Medium 级别的图论题基本就是模板填空。


相关推荐
浩瀚地学1 小时前
【面试算法笔记】0202-链表-基本功能实现
java·经验分享·笔记·算法·面试
你想知道什么?1 小时前
Matplotlib学习笔记
笔记·学习·matplotlib
3DVisionary1 小时前
如何用单目高速DIC把“看不见“的振动变成“数得清“的数据
java·spring boot·python·数码相机·微小振动·精密件·单目高速dic
tkevinjd1 小时前
416分割等和子集
java·python·算法·leetcode·职场和发展
Keven_112 小时前
算法札记:Tarjan与拓扑序(Topo)的关系
算法·拓扑·tarjan
qq_452396232 小时前
第三篇:《Jenkins Pipeline as Code:Declarative Pipeline 深度实战》
java·servlet·jenkins
AOwhisky2 小时前
Linux(CentOS)系统管理入门笔记:第二期核心知识点自测与详解
linux·笔记·centos
ShallWeL2 小时前
AUC、F1、召回率怎么选
人工智能·算法·机器学习
再让我睡两分钟2 小时前
【无标题】
android·java·数据库·人工智能·prompt·ai应用开发