岛屿数量 图解题解
这道题到底在问什么
- 输入
- 1 1 0 0 1 1 0 1 1 0 0 0 1 0 0 0 0 1 1 0 0 0 1 1 1 0 0 1 0 1 0 0 0 0 1
- 输出
- 4 座岛
最优解:为什么这么做
一句话答案:LeetCode 200 岛屿数量的经典解法是扫描加 DFS 沉岛:逐格扫描网格,每遇到一块还没处理过的陆地就把岛屿数加一,随即用深度优先搜索把与它四连通的整片陆地全改成海水,保证同一座岛只被数一次。时间 O(R×C),每个格子至多访问一次,空间最坏 O(R×C)。
岛屿数量这道题在问什么
网格里每个格子要么是陆地字符 '1',要么是海水字符 '0'。水平或竖直方向相邻的陆地连成一座岛,问一共有几座。用图论的话说,这是在数「陆地的连通分量个数」——彼此能通过上下左右走到的陆地算同一块。注意连通只认四个方向,斜对角挨着的两块陆地不算同一座岛,这个定义直接决定后面搜索时只往四个方向扩散。
为什么数岛屿要靠 DFS 洪水填充
逐格扫描很容易发现陆地,难的是别把同一座岛数成好几座——一座大岛横跨几十个格子,扫描时会撞上它几十次。关键观察由此而来:计数的单位是「岛」而不是「格」,所以第一次撞见某座岛时就该把它整个处理掉,让后续扫描再也撞不上它。
怎么「整个处理掉」?从这块陆地出发,向四个方向递归扩散,把所有连得上的陆地统统标记,这个过程像洪水从一点漫开淹没整片区域,所以也叫洪水填充(flood fill),用 DFS 或 BFS 实现都行。参考代码用 DFS 直接把走到的陆地改写成 '0',形象地说就是把这座岛「沉」进海里。
沉岛操作维护着什么不变量
把数过的岛立刻沉掉,维护的是这样一条不变量:扫描指针之前经过的区域里,不存在任何还没计数的陆地。于是推论非常干净——扫描途中每撞见一个 '1',它必然属于一座全新的、没数过的岛,放心加一,绝不会重复。
正确性的另一半是「不漏」:DFS 从起点扩散,恰好覆盖与起点连通的所有陆地,既不会漏掉本岛的格子(每个方向都探),也不会淹到别的岛(中间隔着海水,递归在 '0' 处止步)。一次沉岛动作与一座岛严格一一对应,计数自然准确。
递归的边界条件为什么这样写
dfs(i, j) 开头先做两个判断:下标越界直接返回;格子不是 '1'(是海水或已被沉掉)也直接返回。顺序不能反——必须先判越界再取值,否则会数组越界,在 Python 里负下标还会静悄悄绕到另一头,制造更隐蔽的错。过了这两关才把当前格改成 '0',然后向四个方向递归。把「已访问」和「海水」统一表示成 '0',让终止条件只剩一条,是这份代码简洁的来源。
复杂度分析与常见追问
时间 O(R×C):每个格子至多被外层扫描看一次、被 DFS 进入一次,进入后就变 '0' 不会再进。空间 O(R×C):最坏整张网格全是陆地,递归深度可达格子总数;担心递归栈溢出可以换成显式队列的 BFS,逻辑不变。
两个常见追问:不想破坏输入网格,就另开一个同尺寸的 visited 布尔数组,判断条件改成「是 '1' 且没访问过」,代价是 O(R×C) 额外空间;若进一步问最大岛屿面积(LeetCode 695),让 dfs 返回它沉掉的格子数,外层取最大值即可。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3遇到新陆地 → 计数 +1 → DFS 沉掉整座岛绿色格子是陆地、蓝色是海水。核心套路就一句:一行一行扫,每碰到一块没访问过的陆地就计数 +1,然后立刻用 DFS 把整座岛沉掉(标记已访问),免得重复数。下面在这张图上一步步演。
- 4count = 0;扫描指针从 (0,0) 起逐行走右边面板上半是岛屿计数器(现在 0),下半是一只 DFS 栈(现在空)。扫描指针停在左上角第一格,准备像读书一样从左到右、从上到下一格一格走。盯住计数器和栈,整道题就是它俩配合。
- 5踩到未访问陆地 → 计数 +1 = 1扫描指针走到 (0,0),这是一块还没访问过的陆地 → 发现一座新岛屿!计数器 +1 变成 1,把这格压入 DFS 栈,准备把整座岛沉掉。
- 6把相邻新陆地 (1,0)、(0,1) 压栈;栈内剩 2取出栈顶 (0,0) 标记为已沉没(变灰),看它上下左右:(1,0)、(0,1) 是相连的新陆地,压入栈待会儿继续沉。
- 7把相邻新陆地 (1,1) 压栈;栈内剩 2取出栈顶 (0,1) 标记为已沉没(变灰),看它上下左右:(1,1) 是相连的新陆地,压入栈待会儿继续沉。
- 8四周没有新陆地了;栈内剩 1取出栈顶 (1,1) 标记为已沉没(变灰),看它上下左右:都是海水或已访问,这一支到底了。
- 9四周没有新陆地了;栈内剩 0取出栈顶 (1,0) 标记为已沉没(变灰),看它上下左右:都是海水或已访问,这一支到底了。 栈空了,这座岛整块沉完。
- 10踩到未访问陆地 → 计数 +1 = 2扫描指针走到 (0,4),这是一块还没访问过的陆地 → 发现一座新岛屿!计数器 +1 变成 2,把这格压入 DFS 栈,准备把整座岛沉掉。
- 11把相邻新陆地 (0,5) 压栈;栈内剩 1取出栈顶 (0,4) 标记为已沉没(变灰),看它上下左右:(0,5) 是相连的新陆地,压入栈待会儿继续沉。
- 12把相邻新陆地 (1,5) 压栈;栈内剩 1取出栈顶 (0,5) 标记为已沉没(变灰),看它上下左右:(1,5) 是相连的新陆地,压入栈待会儿继续沉。
- 13四周没有新陆地了;栈内剩 0取出栈顶 (1,5) 标记为已沉没(变灰),看它上下左右:都是海水或已访问,这一支到底了。 栈空了,这座岛整块沉完。
- 14踩到未访问陆地 → 计数 +1 = 3扫描指针走到 (2,3),这是一块还没访问过的陆地 → 发现一座新岛屿!计数器 +1 变成 3,把这格压入 DFS 栈,准备把整座岛沉掉。
- 15把相邻新陆地 (3,3)、(2,4) 压栈;栈内剩 2取出栈顶 (2,3) 标记为已沉没(变灰),看它上下左右:(3,3)、(2,4) 是相连的新陆地,压入栈待会儿继续沉。
- 16四周没有新陆地了;栈内剩 1取出栈顶 (2,4) 标记为已沉没(变灰),看它上下左右:都是海水或已访问,这一支到底了。
- 17把相邻新陆地 (3,2) 压栈;栈内剩 1取出栈顶 (3,3) 标记为已沉没(变灰),看它上下左右:(3,2) 是相连的新陆地,压入栈待会儿继续沉。
- 18把相邻新陆地 (3,1) 压栈;栈内剩 1取出栈顶 (3,2) 标记为已沉没(变灰),看它上下左右:(3,1) 是相连的新陆地,压入栈待会儿继续沉。
- 19把相邻新陆地 (4,1) 压栈;栈内剩 1取出栈顶 (3,1) 标记为已沉没(变灰),看它上下左右:(4,1) 是相连的新陆地,压入栈待会儿继续沉。
- 20四周没有新陆地了;栈内剩 0取出栈顶 (4,1) 标记为已沉没(变灰),看它上下左右:都是海水或已访问,这一支到底了。 栈空了,这座岛整块沉完。
- 21踩到未访问陆地 → 计数 +1 = 4扫描指针走到 (3,6),这是一块还没访问过的陆地 → 发现一座新岛屿!计数器 +1 变成 4,把这格压入 DFS 栈,准备把整座岛沉掉。
- 22把相邻新陆地 (4,6) 压栈;栈内剩 1取出栈顶 (3,6) 标记为已沉没(变灰),看它上下左右:(4,6) 是相连的新陆地,压入栈待会儿继续沉。
- 23四周没有新陆地了;栈内剩 0取出栈顶 (4,6) 标记为已沉没(变灰),看它上下左右:都是海水或已访问,这一支到底了。 栈空了,这座岛整块沉完。
- 24共 4 座岛屿整张网格扫完,每座岛都在发现的瞬间被计数、并被 DFS 整块沉没,所以绝不会重复数。最终岛屿总数 = 4。
⚠️ 容易写错的地方
✗ 错:数到岛后不沉掉,继续往下扫
✓ 对:发现岛后立刻 DFS 把整块标记
不沉掉的话同一座岛的其它格子会被当成新岛反复计数
✗ 错:把斜对角也当相连
✓ 对:只走上下左右四个方向
本题定义四连通,斜对角不算同一座岛
✗ 错:DFS 忘了边界判断就索引
✓ 对:先判 i,j 越界再访问 grid[i][j]
不判边界会数组越界(或 Python 负索引绕回)
完整代码(Python / Java / C++)
Python
def numIslands(grid):
if not grid: return 0
R, C = len(grid), len(grid[0])
def dfs(i, j):
if i < 0 or j < 0 or i >= R or j >= C: return
if grid[i][j] != '1': return # 海水/已沉没
grid[i][j] = '0' # 沉掉这格
dfs(i-1, j); dfs(i+1, j)
dfs(i, j-1); dfs(i, j+1) # 四个方向
cnt = 0
for i in range(R):
for j in range(C):
if grid[i][j] == '1': # 遇到新陆地
cnt += 1
dfs(i, j) # 沉掉整座岛
return cntJava
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) return 0;
int R = grid.length, C = grid[0].length, cnt = 0;
for (int i = 0; i < R; i++) {
for (int j = 0; j < C; j++) {
if (grid[i][j] == '1') { // 遇到新陆地
cnt++;
dfs(grid, i, j); // 沉掉整座岛
}
}
}
return cnt;
}
private void dfs(char[][] g, int i, int j) {
if (i < 0 || j < 0 || i >= g.length || j >= g[0].length) return;
if (g[i][j] != '1') return; // 海水/已沉没
g[i][j] = '0'; // 沉掉这格
dfs(g, i-1, j); dfs(g, i+1, j);
dfs(g, i, j-1); dfs(g, i, j+1); // 四个方向
}C++
class Solution {
void dfs(vector<vector<char>>& g, int i, int j) {
int R = g.size(), C = g[0].size();
if (i < 0 || j < 0 || i >= R || j >= C) return;
if (g[i][j] != '1') return; // 海水/已沉没
g[i][j] = '0'; // 沉掉这格
dfs(g, i-1, j); dfs(g, i+1, j);
dfs(g, i, j-1); dfs(g, i, j+1); // 四个方向
}
public:
int numIslands(vector<vector<char>>& grid) {
if (grid.empty()) return 0;
int cnt = 0;
for (int i = 0; i < (int)grid.size(); i++)
for (int j = 0; j < (int)grid[0].size(); j++)
if (grid[i][j] == '1') { // 遇到新陆地
cnt++; dfs(grid, i, j); // 沉掉整座岛
}
return cnt;
}
};复杂度
时间
O(R×C)
每个格子最多被访问一次(扫描一次 + DFS 进一次)
空间
O(R×C)
最坏全是陆地时,递归栈深度可达格子总数
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 岛屿数量 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能用 BFS 代替 DFS?+
能。把递归换成一个队列,发现新岛后把起点入队,循环弹出、把四邻未访问陆地入队并标记,效果一样。BFS 用显式队列、不吃递归栈,超大网格更不容易栈溢出。
不想修改输入网格怎么办?+
用一个等大的 visited 布尔数组记录访问过的格子,判断条件从「值是不是 1」改成「是 1 且没被 visited」。代价是 O(R×C) 额外空间。
如果还要求最大岛屿面积呢?+
dfs 返回它沉没的格子数(1 + 四个方向 dfs 之和),外层对每座岛取最大值即可(LC695)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 岛屿数量 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。