【Java实习面试算法冲刺】图论

第9类题型:图论

问题背景:为什么图论题你总觉得自己会 BFS / DFS,一到面试还是容易把题型判断错

很多实习面试里的图论题,表面根本不写"图"这个字。

它可能长成二维矩阵,像 200. 岛屿数量994. 腐烂的橘子;也可能长成依赖关系,像 207. 课程表。真正让人失分的地方,往往不是代码敲不出来,而是你没有在前 30 秒里判断清楚:

  • 这题的点和边分别是什么。
  • 这是在求连通块、最短扩散层数,还是在判断有没有环。
  • 该用 DFS、BFS,还是拓扑排序。

如果你现在的状态是"看过图论模板,但一遇到矩阵题和依赖题还是会犹豫",那这一类题必须单独练透。读完这篇,你至少要把 4 件事固定下来:先把题目抽象成图、先决定遍历方式、访问标记要在什么时候打、依赖题要怎么建邻接表和入度表。


文章目录

  • 第9类题型:图论
    • [问题背景:为什么图论题你总觉得自己会 BFS / DFS,一到面试还是容易把题型判断错](#问题背景:为什么图论题你总觉得自己会 BFS / DFS,一到面试还是容易把题型判断错)
    • [1. 核心知识点](#1. 核心知识点)
    • [2. 图论题型选择表:DFS / BFS / 拓扑排序什么时候用](#2. 图论题型选择表:DFS / BFS / 拓扑排序什么时候用)
    • [3. 动画辅助理解:图论题先建模,再决定怎么遍历](#3. 动画辅助理解:图论题先建模,再决定怎么遍历)
    • [4. 用样例手推 DFS / 拓扑排序](#4. 用样例手推 DFS / 拓扑排序)
      • [先手推 DFS:`200. 岛屿数量`](#先手推 DFS:200. 岛屿数量)
      • [再手推拓扑排序:`207. 课程表`](#再手推拓扑排序:207. 课程表)
    • [5. 这类题在面试里考什么](#5. 这类题在面试里考什么)
    • [6. 高频题清单](#6. 高频题清单)
    • [7. 这类题最容易犯的 4 个错误](#7. 这类题最容易犯的 4 个错误)
    • [8. 代表题精讲 1](#8. 代表题精讲 1)
    • [9. 代表题精讲 2](#9. 代表题精讲 2)
    • [10. 其余题模板与关键片段](#10. 其余题模板与关键片段)
    • [11. 图论题先判断什么](#11. 图论题先判断什么)
    • [12. 错题本记录方式](#12. 错题本记录方式)
    • [13. 面试前 3 分钟速记](#13. 面试前 3 分钟速记)
    • [14. 快速验证:同一套判断能不能落到 200 / 994 / 207](#14. 快速验证:同一套判断能不能落到 200 / 994 / 207)
    • [15. 本地样例验证结果](#15. 本地样例验证结果)
    • [16. 最后总结:图论题真正难的不是模板,而是建模速度](#16. 最后总结:图论题真正难的不是模板,而是建模速度)

1. 核心知识点

实习面试里的图论,最常见的不是 Dijkstra 或 Floyd,而是下面 3 类基础模型:

  • 网格 DFS:常见于"数连通块""把一整片区域染色"。
  • 多源 BFS:常见于"按层扩散""求最少几轮传播到位"。
  • 拓扑排序:常见于"课程依赖""任务先后关系""判断是否有环"。

你可以先把它们理解成三个固定问法:

  • 如果题目在问"这一整片算一个块吗",优先想 DFS / BFS。
  • 如果题目在问"最少经过几步扩散完成",优先想 BFS。
  • 如果题目在问"这些依赖能不能都做完",优先想拓扑排序。

2. 图论题型选择表:DFS / BFS / 拓扑排序什么时候用

题目特征 常见问题 优先解法 你在面试里要先说清楚什么
二维矩阵,上下左右连通 有几个连通块、整片区域是否连通 DFS / BFS 一个格子是不是一个节点,连边规则是什么
从多个起点同时扩散 最少几轮传播完成、最短层数 BFS 为什么要按层处理,初始队列里有哪些起点
课程 / 任务存在先后依赖 是否有环、能否完成全部任务 拓扑排序 邻接表怎么建,入度表示什么

真正高频的失误不是"不会写",而是"题型选错":

  • 200. 岛屿数量 本质是数连通块,不是在求最短路径。
  • 994. 腐烂的橘子 本质是多源扩散,不适合拿 DFS 去硬做分钟数。
  • 207. 课程表 本质是判断依赖图里是否有环,不是在普通遍历里随便走一遍。

3. 动画辅助理解:图论题先建模,再决定怎么遍历

你可以先打开这页分步动画:

图论:建图、入队、访问邻居分步动画

这个动画最值得盯住的不是按钮,而是 4 个固定动作:

  1. 先说明节点和边怎么定义。
  2. 再决定起点怎么入栈 / 入队。
  3. 访问邻居时立刻处理 visited,避免重复访问。
  4. 最后根据题目要求统计答案,是连通块数量、扩散轮数,还是可完成课程数。

4. 用样例手推 DFS / 拓扑排序

这一节非常重要。图论题如果只看模板,不手推状态,你在面试里还是容易卡。

先手推 DFS:200. 岛屿数量

样例:

text 复制代码
grid = [
  ['1','1','0','0'],
  ['1','0','0','1'],
  ['0','0','1','1']
]

你可以把每个值为 '1' 的格子看成一个陆地节点,上下左右相邻就有边。

手推过程:

扫描位置 看到什么 动作 当前岛屿数
(0,0) '1' 且未访问 岛屿数 +1,从 (0,0) 开始 DFS,把 (0,0)(0,1)(1,0) 全部淹没 / 标记 1
继续扫描第一、二行 已访问陆地或海水 跳过 1
(1,3) '1' 且未访问 岛屿数 +1,DFS 扩到 (2,3)(2,2) 2
扫描结束 没有新的未访问陆地 返回答案 2

你在面试里要说清楚的关键点是:

  • 这题不是"从某个起点找到一条路径",而是"扫描全图,遇到新陆地就扩掉整片连通块"。
  • 每次 DFS 的目的不是求最短路,而是"把这一整片陆地都处理掉,避免重复计数"。

再手推拓扑排序:207. 课程表

样例:

text 复制代码
numCourses = 4
prerequisites = [[1,0],[2,0],[3,1],[3,2]]

这表示:

  • 1 之前先学 0
  • 2 之前先学 0
  • 3 之前先学 12

所以邻接表和入度表会变成:

text 复制代码
0 -> [1, 2]
1 -> [3]
2 -> [3]
3 -> []

indegree = [0, 1, 1, 2]

手推过程:

队列 出队课程 更新后入度变化 已处理课程数
[0] 0 1: 1->02: 1->0,把 12 入队 1
[1,2] 1 3: 2->1 2
[2] 2 3: 1->0,把 3 入队 3
[3] 3 无后续课程 4

最后处理课程数等于 numCourses,说明图里没有环,可以学完。

你在面试里要强调:

  • 入度为 0 的课,说明它当前没有前置依赖,可以先学。
  • 如果图里有环,那么环上的课程入度永远不可能全部降到 0
  • 所以最后只要比较"处理掉的课程数"和"总课程数"是否相等,就能判断能不能学完。

5. 这类题在面试里考什么

图论题考的不是名词量,而是你能不能把题目稳定抽象成"点 + 边 + 遍历规则"。

面试官通常会看:

  • 你能不能先判断它是网格图、普通图还是依赖图。
  • 你会不会解释为什么该用 DFS、BFS 或拓扑排序。
  • 你能不能把 visited、邻接表、入度表维护清楚。
  • 你是否知道答案统计发生在什么时候,是每次发现新连通块时、每一层 BFS 结束时,还是拓扑出队时。

真正拉开差距的地方在于:你不只是把代码敲出来,还能解释"为什么这个统计口径正好对应题目要的答案"。

6. 高频题清单

题目 来源 难度 高频属性
200. 岛屿数量 LeetCode 热题 100 Medium 高频
994. 腐烂的橘子 LeetCode 热题 100 Medium 高频
207. 课程表 LeetCode 热题 100 Medium 高频
733. 图像渲染 面试经典 150 Easy 基础高频

7. 这类题最容易犯的 4 个错误

  1. 看到矩阵题,没有意识到本质上是网格图。
  2. visited 标记时机错了,导致重复入队或重复 DFS。
  3. 把"求最少几轮扩散"的题写成 DFS,结果分钟数不好统计。
  4. 课程表 不会把依赖关系转成邻接表和入度表。

8. 代表题精讲 1

题目

200. 岛屿数量

思路

这题本质上是"数二维网格里的连通块数量"。

扫描整个矩阵时,只要看到一个还没处理过的 '1',就说明发现了一座新岛屿,此时答案加一,然后从这个格子出发 DFS,把整片和它连通的陆地全部标记掉。这样后面再扫到这片区域时,就不会重复计数。

这题为什么适合 DFS:

  • 每次进入 DFS,目标都是"扩完这一整片连通块"。
  • 不需要维护层数,也不需要比较最短路径。
  • 递归写法足够直接,面试里表达也最顺。

Java 代码

java 复制代码
class Solution {
    private static final int[][] DIRS = {
        {1, 0}, {-1, 0}, {0, 1}, {0, -1}
    };

    public int numIslands(char[][] grid) {
        int rows = grid.length;
        int cols = grid[0].length;
        int islands = 0;

        for (int row = 0; row < rows; row++) {
            for (int col = 0; col < cols; col++) {
                if (grid[row][col] == '1') {
                    islands++;
                    dfs(grid, row, col);
                }
            }
        }

        return islands;
    }

    private void dfs(char[][] grid, int row, int col) {
        if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length) {
            return;
        }
        if (grid[row][col] != '1') {
            return;
        }

        grid[row][col] = '0';
        for (int[] dir : DIRS) {
            dfs(grid, row + dir[0], col + dir[1]);
        }
    }
}

复杂度

  • 时间复杂度:O(m * n)
  • 空间复杂度:递归栈最坏 O(m * n)

边界提醒

这题最容易错的地方有三个:

  • 忘了在 DFS 里把当前格子标记掉,导致重复访问。
  • 只从一个起点出发,却忘了外层还要扫描整张图。
  • 把对角线也当成连通方向,结果答案算多。

这题默认只看上下左右,不看斜对角。这个细节如果面试里不先说,容易被追问。

如果这是面试现场,你可以这样说

这题我会把矩阵看成一个网格图,每个陆地格子是节点,上下左右相邻就有边。扫描矩阵时,只要遇到一个还没处理过的 '1',就说明发现了一整片新连通块,答案加一,然后用 DFS 把这片陆地全部标记掉,避免后面重复计数。

9. 代表题精讲 2

题目

207. 课程表

思路

这题问的是:所有课程依赖关系里是否存在环。只要没有环,就能完成所有课程。

最稳的做法是拓扑排序:

  1. 建邻接表,记录一门课学完后能解锁哪些课。
  2. 统计每门课当前的入度。
  3. 把所有入度为 0 的课程入队。
  4. 每次弹出一门课,就把它指向的后续课程入度减一。
  5. 如果某门课入度减到 0,说明它现在没有前置依赖了,可以入队。
  6. 最后比较处理课程数和总课程数。

Java 代码

java 复制代码
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }

        int[] indegree = new int[numCourses];
        for (int[] edge : prerequisites) {
            int course = edge[0];
            int pre = edge[1];
            graph.get(pre).add(course);
            indegree[course]++;
        }

        Queue<Integer> queue = new ArrayDeque<>();
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) {
                queue.offer(i);
            }
        }

        int finished = 0;
        while (!queue.isEmpty()) {
            int current = queue.poll();
            finished++;
            for (int next : graph.get(current)) {
                indegree[next]--;
                if (indegree[next] == 0) {
                    queue.offer(next);
                }
            }
        }

        return finished == numCourses;
    }
}

复杂度

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

边界提醒

这题最容易误判的地方是:

  • 以为"遍历过所有点"就够了,但实际上关键是能不能按依赖顺序不断找到入度为 0 的点。
  • 邻接表方向建反了。这里必须是 pre -> course,表示学完前置课程后,才能解锁后续课程。
  • 忘了最后比较 finished == numCourses,导致即使有环也返回了 true

如果这是面试现场,你可以这样说

这题本质是在判断依赖图里有没有环。我会用拓扑排序:先统计每门课的入度,把所有入度为 0 的课入队,再不断出队并更新后续课程的入度。如果最后能处理掉全部课程,说明图里没有环;否则就说明有课程被环卡住了。

10. 其余题模板与关键片段

994. 腐烂的橘子

这题的关键不是"会不会遍历",而是"为什么必须按层扩散"。

  • 所有初始腐烂橘子都要先入队,因为它们是同一时刻的多个起点。
  • 每处理完一层队列,才代表过去了 1 分钟。
  • 因此这题优先想多源 BFS,而不是 DFS。

关键片段:

java 复制代码
while (!queue.isEmpty() && fresh > 0) {
    int size = queue.size();
    minutes++;
    for (int i = 0; i < size; i++) {
        int[] current = queue.poll();
        for (int[] dir : DIRS) {
            int nextRow = current[0] + dir[0];
            int nextCol = current[1] + dir[1];
            if (isFresh(grid, nextRow, nextCol)) {
                grid[nextRow][nextCol] = 2;
                fresh--;
                queue.offer(new int[] {nextRow, nextCol});
            }
        }
    }
}

733. 图像渲染

这题是最基础的网格 DFS / BFS 题。面试里如果你第一次接触图论题,往往会先从它开始热身。

11. 图论题先判断什么

真正动手写代码前,你可以先问自己 4 个问题:

  1. 这题里的节点是什么,是格子、课程,还是普通图节点。
  2. 边怎么连,是上下左右、依赖关系,还是邻接关系。
  3. 题目要的是连通块数量、最短扩散层数,还是是否存在环。
  4. visited 应该在什么时候标记,是入队时、出队时,还是进入 DFS 时。

这 4 个问题一旦先说清楚,图论题的大部分思路就已经定下来了。

12. 错题本记录方式

图论题建议重点记:

  • 这题里的点和边分别是什么。
  • 我为什么判断该用 DFS / BFS / 拓扑排序。
  • visited 或入度表是怎么维护的。
  • 这题的答案到底是在什么时候统计出来的。

13. 面试前 3 分钟速记

  • 矩阵上下左右扩散,优先想网格图。
  • 数连通块,常见 DFS / BFS。
  • 求最少几轮扩散,优先想多源 BFS。
  • 课程依赖、先修关系,优先想拓扑排序。
  • 图题一上来先说节点、边、访问标记。

14. 快速验证:同一套判断能不能落到 200 / 994 / 207

你在面试前可以用下面这张表做最后自检,看看自己是不是真的把这类题吃透了:

题目 先识别什么 正确做法 为什么不是别的做法
200. 岛屿数量 网格图、数连通块 DFS / BFS 它不是求最短层数,不需要按层统计分钟
994. 腐烂的橘子 多起点同时扩散 多源 BFS 题目要最少几分钟,天然需要按层推进
207. 课程表 依赖关系、有无环 拓扑排序 核心不是遍历所有点,而是看能否持续找到入度为 0 的点

如果你能在 30 秒内把这三题都归到正确模型,并说明"为什么不用另两种",那这类图论高频题基本就已经掌握到位了。

15. 本地样例验证结果

为了避免"思路看起来对,但代码细节有误",我按文中的 200207 代码思路做了本地样例验证,输出如下:

text 复制代码
numIslands=2
canFinish=true

这两行结果分别对应:

  • 200. 岛屿数量 的手推样例最终得到 2 座岛。
  • 207. 课程表 的依赖样例最终可以完成全部课程,因此返回 true

如果你自己复盘时写出的结果和这里不一致,优先检查 3 个地方:DFS 是否真的把整片陆地都标记掉了、拓扑排序的边方向有没有建反、入度减到 0 时有没有及时入队。

16. 最后总结:图论题真正难的不是模板,而是建模速度

图论题最容易让人慌的地方,是题面不一定直接告诉你"这是图"。但只要你养成固定顺序,很多题都会变简单:

  1. 先找节点和边。
  2. 再判断这是连通块、扩散层数,还是依赖有环。
  3. 然后决定用 DFS、BFS,还是拓扑排序。
  4. 最后明确答案统计发生在什么时候。

面试里你只要能把这条链路说顺,图论题就不会再只是"背过几个模板",而是真正掌握了这一类题的公共骨架。


上期速递:【Java实习面试算法冲刺】回溯

感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!

相关推荐
尽兴-1 小时前
传统 Java/Vue 系统如何一步步接入 AI 维护
java·vue.js·人工智能·ai·大模型
shuoshuohaohao1 小时前
《HTTP协议》
java·web
大鱼>1 小时前
宠物监控数据安全与隐私保护:端到端加密与合规实践
人工智能·深度学习·算法·iot·宠物
一路向北North1 小时前
Spring Security OAuth2.0(22):分布式系统授权-搭建网关
java·后端·spring
被自己蠢哭1 小时前
BFS广度优先搜索问题:#P00496. 华为od机试—自动泊车
算法
:-)2 小时前
分支限界算法
算法
存在的五月雨2 小时前
SpringBoot后端限制重复提交
java·spring boot·后端
Sam_Deep_Thinking2 小时前
一次线程池线上故障复盘:四层防线如何避免数据丢失
java·面试·程序员
paopaokaka_luck2 小时前
基于Springboot3+Vue3的宠物殡葬管理系统(webSocket实时通讯、接入腾讯地图、支付宝沙盒支付、Echarts图形化分析)
java·开发语言·网络·后端