LeetCode 200:岛屿数量——Java DFS 染色法详解

一、题目说明

给定一个由字符 '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 染色

这道题最关键的问题不是如何计算某座岛屿的面积,而是如何保证每座岛屿只统计一次。

我们可以从左到右、从上到下遍历整个网格:

  1. 如果当前格子是 '0',说明它是水域,直接跳过;

  2. 如果当前格子是 '2',说明这块陆地以前已经访问过,也直接跳过;

  3. 如果当前格子是 '1',说明发现了一块尚未访问的陆地,也就发现了一座新岛屿,岛屿数量加一;

  4. 从这个位置开始执行 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' // 正确

第五,把斜对角也当作连通方向,这不符合题目对岛屿连接方式的定义。

相关推荐
旖旎夜光1 小时前
LeetCode 202:快乐数(双指针问题) —— 题解
数据结构·c++·算法·leetcode·双指针
带刺的坐椅1 小时前
Solon 的日志设计哲学:不只是用 SLF4J 做统一门面
java·logback·solon·log4j2
Flittly2 小时前
【雕虫大技】Agent 动态 Skill 供应链安全加固(二):执行隔离沙箱实战
java·spring boot·spring
程序员黑豆2 小时前
Java变量详解:从入门到精通
java·前端·ai编程
karry_k2 小时前
Skill 到底是什么?从提示词、脚本到多智能体工作流的完整指南
java·人工智能·后端
qq_401700412 小时前
Qt自定义信号槽详解:带参发射 vs sender()获取,两种多信号关联方案全掌握
java·数据库·qt
周杰伦fans2 小时前
数列极限的定义
java·开发语言
君君思密达2 小时前
Leetcode Hot 100 题目详解-哈希表
数据结构·算法·leetcode
小白的后端世界3 小时前
LangChain 模型初始化参数详解:从基础配置到企业级实践
java·人工智能·langchain