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' // 正确

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

相关推荐
nice_lcj52016 分钟前
深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
java·开发语言·后端·面试
骑着蜗牛撵大象32744 分钟前
2026 后端 AI 工程化:Spring AI 管调用、MCP 管工具、Agent 内嵌管自治
java·人工智能·后端·spring·ai·可观测性·mcp
tryxr1 小时前
JVM 垃圾回收机制
java·jvm·gc·垃圾回收
圣保罗的大教堂2 小时前
leetcode 453. 最小操作次数使数组元素相等 中等
leetcode
揣一尾游鱼yu2 小时前
Spring上(IOC)笔记
java·笔记·spring·ioc
荣码2 小时前
长上下文与大文档处理:200页PDF怎么喂给AI,我踩了4个坑
java·python
极限挑战哥2 小时前
共享电单车联营平台分账架构实践:二清合规与自动分账零人工方案
java·大数据·人工智能·微信小程序·架构·微信公众平台
皮皮林5513 小时前
比 VMware 更快,更轻的虚拟机,横空出世!
java
Thomas.Sir3 小时前
第13课:MyBatis-Plus整合SpringBoot4.x,分页、条件查询、逻辑删除实战
java·spring boot
meiying88383 小时前
多快递公司快递单号批量查询接口怎么对接?Python 聚合查询实现
java·大数据·python