图论模块 — LeetCode 热门 100 题精讲(Java 向)

从岛屿数量到课程表,DFS、BFS、拓扑排序、并查集、前缀树 — 五招搞定图论高频题

图论问题概述

图论是算法面试中极具区分度的模块,它涵盖了矩阵网格(本质是图的特殊形式)、有向图、无向图以及树形结构的扩展。图论题的核心技巧包括:

  • DFS / BFS:用于连通性检测、路径搜索、染色标记等。
  • 拓扑排序:解决有向图的依赖关系问题,如课程安排。
  • 并查集(Union-Find):高效处理动态连通性问题。
  • 前缀树(Trie):多叉树的特殊应用,用于字符串高效查找。

LeetCode 热门 100 题中有四道图论经典题:200. 岛屿数量(网格 DFS/BFS)、994. 腐烂的橘子(多源 BFS)、207. 课程表(拓扑排序)、208. 实现 Trie(前缀树)(多叉树设计)。下面逐一剖析。


1. 岛屿数量(LeetCode 200)

题目:给定一个由 ‘1’(陆地)和 ‘0’(水)组成的二维网格,计算岛屿数量。岛屿被水包围,且水平或垂直相邻的陆地视为同一岛屿。

核心思路:遍历网格,每遇到一个 ‘1’ 就启动 DFS/BFS,将该岛屿所有相邻的 ‘1’ 标记为已访问(变为 ‘0’),岛屿计数加一。

DFS 实现

public int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) return 0;
    int m = grid.length, n = grid[0].length;
    int count = 0;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == '1') {
                count++;
                dfs(grid, i, j);
            }
        }
    }
    return count;
}
private void dfs(char[][] grid, int i, int j) {
    if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == '0') return;
    grid[i][j] = '0'; // 标记已访问
    dfs(grid, i - 1, j);
    dfs(grid, i + 1, j);
    dfs(grid, i, j - 1);
    dfs(grid, i, j + 1);
}

复杂度:时间 O(mn),空间 O(mn)(递归栈最坏情况)。

关键点:原地修改 grid 避免 visited 数组;也可以使用 BFS(队列)避免递归深度过大。


2. 腐烂的橘子(LeetCode 994)

题目:给定网格,每个格子可能是新鲜橘子(1)、腐烂橘子(2)或空(0)。每分钟腐烂橘子会将上下左右的新鲜橘子腐烂。求全部腐烂所需的最短分钟数,若无法全部腐烂则返回 -1。

核心思路:多源 BFS。将所有初始腐烂橘子入队,同时统计新鲜橘子总数。BFS 逐层扩展,每扩展一层即增加一分钟。当新鲜橘子数为 0 时返回层数;若 BFS 结束后仍有新鲜橘子则返回 -1。

public int orangesRotting(int[][] grid) {
    int m = grid.length, n = grid[0].length;
    Queue<int[]> q = new LinkedList<>();
    int fresh = 0;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 2) q.offer(new int[]{i, j});
            else if (grid[i][j] == 1) fresh++;
        }
    }
    if (fresh == 0) return 0;
    int minutes = 0;
    int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}};
    while (!q.isEmpty() && fresh > 0) {
        int size = q.size();
        minutes++;
        for (int i = 0; i < size; i++) {
            int[] cell = q.poll();
            for (int[] dir : dirs) {
                int x = cell[0] + dir[0];
                int y = cell[1] + dir[1];
                if (x >= 0 && x < m && y >= 0 && y < n && grid[x][y] == 1) {
                    grid[x][y] = 2;
                    fresh--;
                    q.offer(new int[]{x, y});
                }
            }
        }
    }
    return fresh == 0 ? minutes : -1;
}

复杂度:时间 O(mn),空间 O(mn)。

关键点:多源 BFS 的层数即为分钟数;用 fresh 计数器提前终止。


3. 课程表(LeetCode 207)

题目:给定课程数量 numCourses 和先修关系数组 prerequisites,其中 prerequisites[i] = [a, b] 表示学 a 前必须先学 b。判断是否可以完成所有课程(即图是否有环)。

核心思路:拓扑排序(Kahn 算法)。构建邻接表和入度数组,将所有入度为 0 的节点入队,依次弹出并减少其邻接节点的入度,若某个节点入度变为 0 则入队。若最终入队节点数等于课程总数则无环,否则有环。

public boolean canFinish(int numCourses, int[][] prerequisites) {
    List<Integer>[] graph = new ArrayList[numCourses];
    int[] indegree = new int[numCourses];
    for (int i = 0; i < numCourses; i++) graph[i] = new ArrayList<>();
    for (int[] edge : prerequisites) {
        graph[edge[1]].add(edge[0]);
        indegree[edge[0]]++;
    }
    Queue<Integer> q = new LinkedList<>();
    for (int i = 0; i < numCourses; i++) if (indegree[i] == 0) q.offer(i);
    int count = 0;
    while (!q.isEmpty()) {
        int cur = q.poll();
        count++;
        for (int next : graph[cur]) {
            indegree[next]--;
            if (indegree[next] == 0) q.offer(next);
        }
    }
    return count == numCourses;
}

复杂度:时间 O(V+E),空间 O(V+E)。

关键点:拓扑排序是检测有向图环的标准方法;也可用 DFS 三色标记检测环。


4. 实现 Trie(前缀树)(LeetCode 208)

题目:实现 Trie 类,支持插入单词、搜索单词、搜索前缀。

核心思路:Trie 是多叉树,每个节点包含一个长度为 26 的子节点数组和一个布尔标志 isEnd。插入时逐字符创建节点,搜索时逐字符向下查找。

class Trie {
    class TrieNode {
        TrieNode[] children = new TrieNode[26];
        boolean isEnd;
    }
    private TrieNode root;
    public Trie() { root = new TrieNode(); }
    public void insert(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (node.children[idx] == null) node.children[idx] = new TrieNode();
            node = node.children[idx];
        }
        node.isEnd = true;
    }
    public boolean search(String word) {
        TrieNode node = searchPrefix(word);
        return node != null && node.isEnd;
    }
    public boolean startsWith(String prefix) {
        return searchPrefix(prefix) != null;
    }
    private TrieNode searchPrefix(String prefix) {
        TrieNode node = root;
        for (char c : prefix.toCharArray()) {
            int idx = c - 'a';
            if (node.children[idx] == null) return null;
            node = node.children[idx];
        }
        return node;
    }
}

复杂度:插入和搜索的时间均为 O(L)(L 为单词长度),空间 O(总字符数 * 26)。

关键点:Trie 本质上是一个多叉树,它是处理字符串集合的高效结构,常用于自动补全、单词搜索等。


四题对比与总结

题目 图类型 核心技巧 时间复杂度 空间复杂度 关键点
岛屿数量 网格无向图 DFS/BFS 标记 O(mn) O(mn) 原地修改染色
腐烂的橘子 网格多源 BFS 多源 BFS 层数 O(mn) O(mn) 多源同时扩展
课程表 有向图 拓扑排序(入度) O(V+E) O(V+E) 检测有环
前缀树 多叉树 节点数组 + isEnd O(L) O(26×N) 字符串快速查找

共同本质:图论题的核心是 状态传播。DFS/BFS 用于扩散信息(染色、腐烂、依赖传递),拓扑排序处理先后顺序,前缀树本质上是树形结构但常归入图论范畴。

总结心法

  • 遇到网格连通性 → DFS/BFS 标记。
  • 遇到多起点均匀扩散 → 多源 BFS。
  • 遇到依赖关系 → 拓扑排序(入度队列)。
  • 遇到大量字符串查找 → Trie 前缀树。


🤖