文章目录
前言
本文记录力扣Hot100里面关于图论的四道题,包括常见解法和一些关键步骤理解,也有例子便于大家理解
一、岛屿数量
1.题目
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
'1','1','1','1','0'\], \['1','1','0','1','0'\], \['1','1','0','0','0'\], \['0','0','0','0','0'
]
输出:1
示例 2:
输入:grid = [
'1','1','0','0','0'\], \['1','1','0','0','0'\], \['0','0','1','0','0'\], \['0','0','0','1','1'
]
输出:3
2.代码
java
class Solution {
// DFS 函数:从 (r,c) 开始,把整片相连的陆地全部变成 0
void dfs(char[][] grid, int r, int c) {
int nr = grid.length; // 网格一共有多少行
int nc = grid[0].length; // 网格一共有多少列
// 如果越界 或者 当前是水,直接返回
if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') {
return;
}
grid[r][c] = '0'; // 把当前陆地淹没,标记为已访问
dfs(grid, r - 1, c); // 往上搜索
dfs(grid, r + 1, c); // 往下搜索
dfs(grid, r, c - 1); // 往左搜索
dfs(grid, r, c + 1); // 往右搜索
}
// 主函数:计算岛屿数量
public int numIslands(char[][] grid) {
// 如果网格为空,直接返回 0
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length; // 行数
int nc = grid[0].length; // 列数
int num_islands = 0; // 岛屿数量,初始 0
// 遍历整个网格的每一个格子
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
// 发现一块没被访问过的陆地
if (grid[r][c] == '1') {
num_islands++; // 岛屿数量 +1
dfs(grid, r, c); // DFS 淹没整个岛屿
}
}
}
return num_islands; // 返回最终岛屿总数
}
}
3.例子
['1','1','0','0','0']
['1','1','0','0','0']
['0','0','1','0','0']
['0','0','0','1','1']
第一步:初始化
- 行数
nr = 4 - 列数
nc = 5 - 岛屿数量
num_islands = 0
开始双重循环遍历每一格
第 1 个格子:(0,0) → 值是 '1'
num_islands从 0 → 1- 调用
dfs(0,0) - DFS 开始淹没整片相连的 1
DFS 淹没过程(自动把相连 1 全变 0)
- (0,0) → 变 0
- (0,1) → 变 0
- (1,0) → 变 0
- (1,1) → 变 0
淹没后网格变成:
['0','0','0','0','0']
['0','0','0','0','0']
['0','0','1','0','0']
['0','0','0','1','1']
继续遍历:
(0,2)=0、(0,3)=0、(0,4)=0
(1,0)=0、(1,1)=0、(1,2)=0、(1,3)=0、(1,4)=0
走到 (2,2) → 值是 '1'
num_islands从 1 → 2- 调用
dfs(2,2) - 淹没这个单独的 1 → 变 0
网格现在:
['0','0','0','0','0']
['0','0','0','0','0']
['0','0','0','0','0']
['0','0','0','1','1']
继续遍历:
(2,3)=0、(2,4)=0
(3,0)=0、(3,1)=0、(3,2)=0
走到 (3,3) → 值是 '1'
num_islands从 2 → 3- 调用
dfs(3,3) - 淹没:
- (3,3) → 0
- (3,4) → 0
最终网格全是 0:
['0','0','0','0','0']
['0','0','0','0','0']
['0','0','0','0','0']
['0','0','0','0','0']
遍历结束
返回 num_islands = 3
二、腐烂的橘子
1.题目
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
- 值 0 代表空单元格;
- 值 1 代表新鲜橘子;
- 值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数 。如果不可能,返回 -1 。
示例 1:

输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4
示例 2:
输入:grid = [[2,1,1],[0,1,1],[1,0,1]]
输出:-1
解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。
示例 3:
输入:grid = [[0,2]]
输出:0
解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。
提示:
- m == grid.length
- n == grid[i].length
- 1 <= m, n <= 10
- grid[i][j] 仅为 0、1 或 2
2.代码
java
class Solution {
// 上下左右四个方向的坐标偏移量
private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int orangesRotting(int[][] grid) {
int m = grid.length; // 网格行数
int n = grid[0].length; // 网格列数
int fresh = 0; // 新鲜橘子数量
List<int[]> badList = new ArrayList<>(); // 存放当前腐烂的橘子
// 第一步:遍历网格,统计初始数据
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
fresh++; // 遇到新鲜橘子,计数+1
} else if (grid[i][j] == 2) {
badList.add(new int[]{i, j}); // 遇到腐烂橘子,加入队列
}
}
}
int ans = 0; // 记录总共需要的分钟数
// 第二步:BFS 扩散腐烂(循环直到没有新鲜橘子 或 没有可扩散的腐烂橘子)
while (fresh > 0 && !badList.isEmpty()) {
ans++; // 每轮循环 = 过去1分钟
List<int[]> tmp = badList; // 取出这一分钟要扩散的所有橘子
badList = new ArrayList<>(); // 清空,准备存下一分钟新腐烂的橘子
// 遍历当前所有腐烂橘子,让它们同时向四周扩散
for (int[] pos : tmp) {
for (int[] dir : DIRECTIONS) { // 遍历上下左右
int i = pos[0] + dir[0];
int j = pos[1] + dir[1];
// 判断坐标是否合法 + 是新鲜橘子
if (0 <= i && i < m && 0 <= j && j < n && grid[i][j] == 1) {
fresh--; // 新鲜橘子变少
grid[i][j] = 2; // 变成腐烂橘子
badList.add(new int[]{i, j}); // 加入下一轮扩散
}
}
}
}
// 最后:如果还有新鲜橘子剩下,返回-1,否则返回时间
return fresh > 0 ? -1 : ans;
}
}
3.例子
我用你给的** exact 例子**,一步一步、逐分钟带你走一遍代码,让你彻底看懂!
输入网格
grid = [
[2, 1, 1],
[1, 1, 0],
[0, 1, 1]
]
数字含义:
- 2 = 腐烂橘子
- 1 = 新鲜橘子
- 0 = 空
第一步:代码先遍历整个网格
[2,1,1]
[1,1,0]
[0,1,1]
代码统计结果:
- fresh = 6 个新鲜橘子
- badList = [ (0,0) ] 1 个腐烂橘子
- ans = 0
第二步:开始 BFS 扩散(按分钟走)
第 1 分钟开始
ans 从 0 → 1
当前要扩散的腐烂橘子:(0,0)
上下左右:
- 上:越界
- 下:(1,0) = 1 → 变 2
- 左:越界
- 右:(0,1) = 1 → 变 2
新腐烂橘子:(1,0)、(0,1)
fresh = 6 - 2 = 4
网格现在:
[2, 2, 1]
[2, 1, 0]
[0, 1, 1]
第 2 分钟
ans 从 1 → 2
当前扩散:(0,1)、(1,0)
(0,1) 扩散:
- 右 (0,2) = 1 → 变 2
(1,0) 扩散:
- 下 (2,0) = 0 不变
- 右 (1,1) = 1 → 变 2
新腐烂:(0,2)、(1,1)
fresh = 4 - 2 = 2
网格:
[2, 2, 2]
[2, 2, 0]
[0, 1, 1]
第 3 分钟
ans 从 2 → 3
当前扩散:(0,2)、(1,1)
(0,2) 扩散:周围无新鲜橘子
(1,1) 扩散:
- 下 (2,1) = 1 → 变 2
新腐烂:(2,1)
fresh = 2 - 1 = 1
网格:
[2, 2, 2]
[2, 2, 0]
[0, 2, 1]
第 4 分钟
ans 从 3 → 4
当前扩散:(2,1)
(2,1) 扩散:
- 右 (2,2) = 1 → 变 2
新腐烂:(2,2)
fresh = 1 - 1 = 0
网格:
[2, 2, 2]
[2, 2, 0]
[0, 2, 2]
第三步:结束循环
fresh = 0→ 没有新鲜橘子了- 返回
ans = 4
三、课程表
1.题目
你这个学期必须选修 numCourses 门课程 ,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [a(i), b(i)] ,表示如果要学习课程 a(i) 则 必须 先学习课程 b(i)( )。
- 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
2.代码
java
class Solution {
// 全局变量:存储图的邻接表(课程依赖关系)
List<List<Integer>> edges;
// 标记每个课程的访问状态:0=未访问 1=访问中 2=已访问完
int[] visited;
// 标记是否无环,默认无环(true),发现环改成false
boolean valid = true;
// 主方法:输入课程总数、课程依赖数组,返回能否修完课
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 初始化邻接表,给每一门课程创建一个空的依赖列表
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
// 初始化状态数组,所有课程默认未访问(0)
visited = new int[numCourses];
// 构建有向图:[后修课, 先修课] → 存为 先修课 → 后修课 的边
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 遍历所有课程,只要没发现环,就对未访问的课程做DFS
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {
dfs(i);
}
}
// 最终返回:无环=true,有环=false
return valid;
}
// DFS核心:深度遍历,检测是否有环
public void dfs(int u) {
// 标记当前课程:正在遍历中(状态1)
visited[u] = 1;
// 遍历当前课程的所有后续课程
for (int v: edges.get(u)) {
if (visited[v] == 0) {
// 后续课程没访问过 → 继续DFS
dfs(v);
// 子递归发现环,直接退出,不用继续遍历
if (!valid) return;
} else if (visited[v] == 1) {
// 遇到了【正在遍历中】的节点 → 找到环!
valid = false;
return;
}
}
// 当前课程所有后续都遍历完了 → 标记为已完成(状态2)
visited[u] = 2;
}
}
3.例子
例 1:无环 → 可以修完课(返回 true)
输入
numCourses = 2
prerequisites = [[1,0]]
含义 :想学课程 1,必须先学课程 0
图:0 → 1
代码执行步骤
1. 初始化
edges = [[], []]visited = [0, 0]valid = true
2. 建图
info = [1,0]
edges.get(0).add(1)
→ edges = [ [1], [] ]
3. 遍历课程
i=0,visited[0]=0 → 调用 dfs(0)
** 执行 dfs(0)**
visited[0] = 1(标记正在访问)- 遍历邻居
v=1visited[1] = 0→ 调用dfs(1)
执行 dfs(1)
visited[1] = 1- 没有邻居
visited[1] = 2(标记完成)- 返回
回到 dfs(0)
- 循环结束
visited[0] = 2- 返回
4. 结束
valid = true → 返回 true
例 2:有环 → 不能修完课(返回 false)
输入
numCourses = 2
prerequisites = [[1,0], [0,1]]
含义 :
学 1 先学 0,学 0 先学 1 → 死循环(环)
图:0 ↔ 1
代码执行步骤
1. 建图
edges.get(0).add(1)
edges.get(1).add(0)
→ edges = [ [1], [0] ]
2. 开始 dfs(0)
visited[0] = 1- 遍历邻居
v=1visited[1]=0→ dfs(1)
执行 dfs(1)
visited[1] = 1- 遍历邻居
v=0visited[0] == 1(正在访问!)- 发现环!
valid = false- return
回到 dfs(0)
- 发现
valid=false→ return
3. 结束
valid = false → 返回 false
四、实现Trie(前缀树)
1.题目
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例:
输入
"Trie", "insert", "search", "search", "startsWith", "insert", "search"
\[\], \["apple"\], \["apple"\], \["app"\], \["app"\], \["app"\], \["app"\]
输出
null, null, true, false, true, null, true
解释
Trie trie = new Trie();
trie.insert("apple");
trie.search("apple"); // 返回 True
trie.search("app"); // 返回 False
trie.startsWith("app"); // 返回 True
trie.insert("app");
trie.search("app"); // 返回 True
提示:
1 <= word.length, prefix.length <= 2000
word 和 prefix 仅由小写英文字母组成
insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次
2.代码
java
class Trie {
// 子节点数组,每个节点最多有26个孩子,对应小写字母a-z
private Trie[] children;
// 标记当前节点是否是一个单词的结尾
private boolean isEnd;
// 构造方法:初始化字典树节点
public Trie() {
children = new Trie[26]; // 26个字母,所以数组长度26
isEnd = false; // 默认不是单词结尾
}
// 插入一个单词到字典树
public void insert(String word) {
Trie node = this; // 从根节点开始
for (int i = 0; i < word.length(); i++) { // 遍历单词的每一个字符
char ch = word.charAt(i); // 取出当前字符
int index = ch - 'a'; // 转成数组下标 0~25
if (node.children[index] == null) { // 如果该子节点不存在
node.children[index] = new Trie(); // 新建节点
}
node = node.children[index]; // 移动到子节点,继续处理下一个字符
}
node.isEnd = true; // 单词全部插入完成,标记最后一个节点为单词结尾
}
// 查找单词是否存在(必须完全匹配)
public boolean search(String word) {
Trie node = searchPrefix(word); // 查找前缀
return node != null && node.isEnd; // 找到且是单词结尾才返回true
}
// 判断是否存在以 prefix 为前缀的单词
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null; // 只要能找到前缀就返回true
}
// 私有工具方法:查找前缀,返回最后一个节点
private Trie searchPrefix(String prefix) {
Trie node = this; // 从根节点开始
for (int i = 0; i < prefix.length(); i++) { // 遍历前缀字符
char ch = prefix.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) { // 某个字符不存在
return null; // 前缀不存在,返回null
}
node = node.children[index]; // 继续往下走
}
return node; // 遍历完成,返回当前节点
}
}
3.例子
例:
操作序列:
["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
参数:
[[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
输出:
[null, null, true, false, true, null, true]
第1步:执行 Trie() ------ 创建字典树
- 操作:新建一棵空字典树
- 此时只有根节点
- 没有任何字母,没有任何单词
- 输出:null
第2步:执行 insert("apple") ------ 插入单词 apple
开始逐字符往下建路径:
- 从根节点出发
- 字符
a→ 没有,新建节点 - 字符
p→ 没有,新建节点 - 字符
p→ 没有,新建节点 - 字符
l→ 没有,新建节点 - 字符
e→ 没有,新建节点 - 到达最后一个字符
e,把它标记 isEnd = true- 表示:到这里是一个完整单词
现在树里的路径:
根 → a → p → p → l → e(标记结束)
- 输出:null
第3步:执行 search("apple") ------ 查找完整单词 apple
逐字符查找:
a存在p存在p存在l存在e存在- 最后一个节点
e被标记为结束(isEnd = true)
→ 两个条件都满足:路径存在 + 是单词结尾
→ 结果:true
第4步:执行 search("app") ------ 查找完整单词 app
逐字符查找:
a存在p存在p存在
路径走完了
但是 :最后这个 p 节点 没有标记 isEnd = true
它只是 apple 中间的一个节点,不是单词结尾。
search 要求:必须是完整单词才返回 true
→ 结果:false
第5步:执行 startsWith("app") ------ 判断是否有以 app 开头的单词
startsWith 规则:只要路径能走完,就返回 true
不管是不是单词结尾。
a存在p存在p存在
路径完全能走通 → 结果:true
第6步:执行 insert("app") ------ 插入单词 app
现在插入 app:
- 走到
a → p → p - 把最后这个
p节点 标记 isEnd = true
现在树里有两个单词:
app(p标记结束)apple(e标记结束)
路径变成:
根 → a → p → p(结束)→ l → e(结束)
- 输出:null
第7步:执行 search("app") ------ 再次查找完整单词 app
现在:
- 路径
a→p→p存在 - 最后
p节点 已经被标记 isEnd = true
→ 满足完整单词条件
→ 结果:true
最终输出结果
[null, null, true, false, true, null, true]
如果本篇文章对您有帮助,可以点赞,收藏或评论哦!!!关注主包不迷路,让我们一起向前进步吧!!