一、题目说明
给定一个由字符 '1' 和 '0' 组成的二维网格 grid:
-
'1'表示陆地; -
'0'表示水域; -
水平方向或竖直方向相邻的陆地属于同一座岛屿;
-
网格四周都可以看作被水包围。
我们的任务是统计网格中岛屿的数量。
例如:
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
左上角的四个 '1' 连在一起,中间有一个单独的 '1',右下角还有两个相连的 '1',因此一共有三座岛屿。
二、核心思路:遍历网格 + DFS 染色
这道题最关键的问题不是如何计算某座岛屿的面积,而是如何保证每座岛屿只统计一次。
我们可以从左到右、从上到下遍历整个网格:
-
如果当前格子是
'0',说明它是水域,直接跳过; -
如果当前格子是
'2',说明这块陆地以前已经访问过,也直接跳过; -
如果当前格子是
'1',说明发现了一块尚未访问的陆地,也就发现了一座新岛屿,岛屿数量加一; -
从这个位置开始执行 DFS,把与它上下左右连通的所有
'1'全部改成'2'。
因此,这种解法可以概括为:
外层遍历负责发现新岛屿,DFS 负责将整座岛屿标记为已访问。
为什么发现一个未访问的 '1' 就可以把答案加一?因为如果它属于以前统计过的岛屿,那么它早已在之前的 DFS 中被染成 '2'。现在它仍然是 '1',说明它与此前发现的岛屿都不连通,必然是一座新岛屿的一部分。
三、DFS 如何染完整座岛屿
假设 DFS 当前来到坐标 (i, j),首先需要判断这个位置能否继续访问。
以下情况应立即返回:
-
行坐标越界;
-
列坐标越界;
-
当前格子不是
'1'。
如果当前格子是有效陆地,就先将其改成 '2',表示已经访问,然后继续搜索它的四个方向:
(i - 1, j)
↑
(i, j - 1) ← 当前格子 → (i, j + 1)
↓
(i + 1, j)
只要相邻位置仍然是 '1',DFS 就会继续扩散,直到遇到水域、已访问陆地或网格边界。这样,一次 DFS 结束后,与起点连通的整座岛屿都会被染成 '2'。
这里直接修改原网格,相当于把 grid 同时当作访问标记使用,因此不需要再创建 visited 数组。
四、执行过程推演
仍然使用下面的网格:
1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1
遍历到左上角 (0, 0) 时,第一次遇到 '1':
-
岛屿数量变为
1; -
从
(0, 0)开始 DFS; -
与它连通的
(0, 1)、(1, 0)和(1, 1)都被染成'2'。
网格变为:
2 2 0 0 0
2 2 0 0 0
0 0 1 0 0
0 0 0 1 1
外层循环继续遍历。前四块陆地已经是 '2',不会重复计数。当遍历到 (2, 2) 时,再次遇到 '1',答案加一并完成第二次染色。最后遍历到 (3, 3),发现第三座岛屿,DFS 同时把 (3, 4) 染色。
最终,DFS 被启动了三次,答案就是 3。这也说明:
DFS 启动的次数,就是岛屿的数量。
五、完整 Java 代码
class Solution {
public int numIslands(char[][] grid) {
int count = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; 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] != '1') {
return;
}
// 将当前陆地标记为已访问
grid[i][j] = '2';
dfs(grid, i - 1, j); // 上
dfs(grid, i + 1, j); // 下
dfs(grid, i, j - 1); // 左
dfs(grid, i, j + 1); // 右
}
}
六、代码关键点
1. 为什么先计数,再执行 DFS?
外层循环遇到的 '1' 是新岛屿的入口,因此先执行 count++。DFS 的职责只是找到并标记这座岛屿的其他陆地,不应在递归过程中重复计数。
2. 为什么必须先染色,再递归?
相邻陆地之间可以互相访问。如果不先将当前格子改成 '2',例如 (0, 0) 访问 (0, 1) 后,后者又可能重新访问 (0, 0),从而造成无限递归。
所以正确顺序是:
grid[i][j] = '2';
// 再递归搜索四个方向
3. 为什么只搜索四个方向?
题目规定只有水平或竖直相邻的陆地才连通,斜对角接触不属于同一座岛屿。因此只能搜索上、下、左、右,不能搜索八个方向。
4. 为什么判断 grid[i][j] != '1'?
这一项同时排除了水域 '0' 和已访问格子 '2',比为两种情况分别写判断更加简洁。
七、复杂度分析
设网格有 m 行、n 列。
-
时间复杂度:
O(m × n)。外层循环会检查每个格子,每块陆地最多被 DFS 有效访问一次。 -
空间复杂度:最坏为
O(m × n)。虽然没有创建访问数组,但如果所有格子组成一座岛屿,递归调用栈在极端情况下可能达到这一量级。
原地染色节省的是额外的访问数组空间,并不代表递归栈不存在。
八、常见错误
第一,发现 '1' 后只计数,却没有执行 DFS。这样同一座岛屿中的每块陆地都会被重复统计。
第二,DFS 中没有将访问过的陆地改成 '2',会导致重复搜索,甚至无限递归和栈溢出。
第三,先递归再染色。当前节点可能在染色前被相邻节点再次访问,正确做法是进入有效陆地后立即染色。
第四,把字符写成数字:
grid[i][j] == 1 // 错误
grid[i][j] == '1' // 正确
第五,把斜对角也当作连通方向,这不符合题目对岛屿连接方式的定义。