力扣(岛屿数量)

解析 LeetCode 200. 岛屿数量:DFS 遍历与原地标记法

一、题目分析

(一)问题定义

给定一个 m 行 n 列的二维字符网格 grid,每个元素是 '1' 或 '0'。'1' 表示陆地,'0' 表示水。岛屿是由水平或竖直方向相邻的 '1' 组成的最大连通区域。对角线不视为连通。要求返回网格中岛屿的总数量。

比如 grid = \['1,1,1,1,0','1,1,0,1,0','1,1,0,0,0','0,0,0,0,0'],左上角那片 8 个 '1' 全部上下左右挨着,算作 1 个岛屿。

再比如 grid = \['1,1,0,0,0','1,1,0,0,0','0,0,1,0,0','0,0,0,1,1'],有三块互不连通的 '1' 区域,答案就是 3。

(二)核心挑战

  1. 连通性必须只认上下左右,不能把对角线两个 '1' 当成一块
  2. 同一块岛屿里的所有 '1' 只能被数一次,不能因为从不同入口进去就重复加一
  3. 网格最大可能到 300×300,得保证每个格子最多访问一次,不然容易超时或爆栈
  4. 不想多开一个布尔数组存访问状态,空间要省着用
  5. 边界检查得兜住全部情况:i 小于 0、i 大于等于行数、j 小于 0、j 大于等于列数,还有当前格子根本不是 '1'

二、算法思路:DFS 深度优先遍历

(一)整体策略

外层用双重 for 循环扫完整个网格,每碰到一个还没被标记过的 '1',就调一次 dfs,把这块岛屿上所有 '1' 全改成 '0',同时岛屿总数加一。

(二)dfs 内部执行逻辑

  1. 先做统一终止判断:任意一个条件成立就立刻返回,不往下走 ------ i 越界、j 越界、当前格子不是 '1'
  2. 确认能进之后马上改 gridij = '0',这一步既是标记也是清除,后续扫描就不会再把它当新陆地
  3. 按上、下、左、右顺序分别调 dfs,每次传新坐标,不带其他参数
  4. 递归自然结束,不需要手动把 gridij 改回去 ------ 因为这不是找路径,而是清掉整块陆地,改回去反而会出错

三、代码实现与详细解析

java 复制代码
public class Solution
{
    public int numIslands(char[][] grid)
    {
        // 空输入保护:grid 本身为 null、行数为 0、列数为 0,任意一个为真就返回 0
        if (grid == null || grid.length == 0 || grid[0].length == 0)
        {
            return 0;
        }
        int m = grid.length;
        int n = grid[0].length;
        int count = 0;
        // 主循环:逐行逐列扫描每个格子
        for (int i = 0; i < m; i++)
        {
            for (int j = 0; j < n; j++)
            {
                // 发现新陆地:只有 grid[i][j] 是 '1' 才触发 dfs
                if (grid[i][j] == '1')
                {
                    dfs(grid, i, j);
                    count++;
                }
            }
        }
        return count;
    }
    // dfs 函数作用:从 (i,j) 出发,把所有与它连通的 '1' 全部改成 '0'
    private void dfs(char[][] grid, int i, int j)
    {
        int m = grid.length;
        int n = grid[0].length;
        // 终止条件合并写:任意一个为真就退出,不继续递归
        // 包括:i 小于 0、i 大于等于 m、j 小于 0、j 大于等于 n、当前格子不是 '1'
        if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != '1')
        {
            return;
        }
        // 标记动作:把当前陆地改为水,表示已访问且不再参与后续计数
        grid[i][j] = '0';
        // 四个方向分别递归:顺序不影响结果,但固定顺序利于调试
        dfs(grid, i - 1, j); // 向上走一行
        dfs(grid, i + 1, j); // 向下走一行
        dfs(grid, i, j - 1); // 向左走一列
        dfs(grid, i, j + 1); // 向右走一列
        // 递归全部返回后函数结束,没有需要恢复的状态
    }
}

(一)代码流程拆解

  1. 外层空校验:先看 grid 是否为 null,再看行数是否为 0,再看列数是否为 0,任意一个为真就返回 0
  2. 获取维度:m 记录行数,n 记录列数,count 初始化为 0
  3. 主扫描循环:i 从 0 到 m-1,j 从 0 到 n-1,每个位置检查 gridij
  4. 触发条件:仅当 gridij == '1' 时才调用 dfs 并使 count 加一
  5. dfs 入口判断:先读取当前网格尺寸,再一次性判断五个终止条件
  6. 原地标记:确认可访问后立刻赋值 gridij = '0',这是整个算法节省空间的关键
  7. 方向展开:四次独立 dfs 调用,分别对应上下左右,每次传入新坐标
  8. 无状态恢复:因为目标是清除整块岛屿,不是试探路径,所以不需要回溯还原

(二)关键逻辑解析

  • 原地标记法:不新建 visited 数组,而是复用 grid 本身。把 '1' 改成 '0' 既表示已访问,又让后续扫描跳过,一举两得。注意这个操作必须在所有判断通过后立即执行,不能延后
  • 终止条件合并:把越界和非陆地判断写在一个 if 里,避免嵌套或重复代码。其中 gridij != '1' 包含了原为 '0' 和已被改过的 '0' 两种情况
  • 方向调用顺序:虽然上下左右顺序不影响最终计数,但固定顺序能让调试时坐标变化可预期。如果将来扩展八方向,可替换为 dirs 数组循环
  • 递归深度控制:最坏情况是全 '1' 网格,此时 dfs 单次调用栈深达 m×n,但实际运行中因网格稀疏,平均深度远低于此
  • 非回溯语义:本题不是找路径或组合,而是连通分量染色,因此不需要在递归返回后重置 gridij。这点和全排列、子集等回溯题有本质区别

四、复杂度分析

(一)时间复杂度

O(m×n),每个格子最多被主循环访问一次,又被 dfs 访问一次。两次访问不会重叠:主循环只进未标记的 '1',dfs 把这些 '1' 全部变成 '0',后续主循环就跳过了

(二)空间复杂度

O(m×n),仅由递归系统栈贡献。最坏情况下全 '1' 网格会形成一条蛇形递归链,深度接近 m×n。不使用额外数组,空间效率最优

相关推荐
zander2581 小时前
LeetCode 198. 打家劫舍
算法·leetcode·深度优先
吃着火锅x唱着歌2 小时前
LeetCode 648.单词替换
算法·leetcode·职场和发展
一只积极向上的小咸鱼2 小时前
分词器tokenizer
算法·llm
Elsa️7462 小时前
leetcode 14.最长公共前缀
算法·leetcode·职场和发展
不会代码的小猴2 小时前
6. Qt网络编程
开发语言·c++·笔记·qt·算法
Zguigo2 小时前
树的前序|中序|后序遍历【使用栈实现】
数据结构·算法
zhanghaha13143 小时前
HTML系列教程:4_什么是 HTML 元素(零基础超详细讲解)
前端·算法·html
TAN-90°-3 小时前
Deep Learning for Computer Vision——Training CNNs and CNN Architectures
人工智能·深度学习·神经网络·算法·机器学习·计算机视觉·cnn
0x3F(小茶)3 小时前
Tokenization(分词算法):一切大语言模型的地基
人工智能·算法·语言模型