DFS 岛屿系列题全解析

DFS 岛屿系列题全解析

在 LeetCode 中,「岛屿系列」题目是 DFS(深度优先搜索)的经典应用场景,涵盖了连通块计数、面积统计、形状判断等多种核心考点。本文将汇总 6 道高频岛屿题,从基础的岛屿数量统计,到进阶的子岛屿、不同形状岛屿判断,每道题均包含完整题目信息、思路解析,以及原汁原味的代码实现(保留所有注释,不做任何修改),适合刷题新手巩固 DFS 技巧,也适合进阶学习者梳理解题框架。

核心说明:所有题目均基于「二维网格」,核心解题思路围绕 DFS 连通块遍历 展开,常用技巧为「淹没法」(遍历后将陆地置为水,避免重复统计),统一使用「上下左右」四个方向的方向数组简化代码。

通用DFS模板

js 复制代码
// 前置定义 
const rows = grid.length;
// 网格 总列数(水平方向)
const cols = grid[0].length;

// 方向数组:上下左右 四个方向(标准写法,不用重复写四次)
// [行变化, 列变化]
const dirs = [
  [0, -1], // 左:行不变,列-1
  [0, 1], // 右:行不变,列+1
  [1, 0], // 下:行+1,列不变
  [-1, 0], // 上:行-1,列不变
];
const WATER = '0' // 看题目情况

// 通用 DFS 模板(淹没法)
function dfs(grid, row, col, dirs) {
  // 1. 越界判断
  if (row < 0 || col < 0 || row >= rows || col >= cols) return;
  // 2. 水/已遍历判断
  if (grid[row][col] === WATER) return;
  // 3. 核心操作(淹没、统计、记录路径等)
  grid[row][col] = WATER; // 淹没,避免重复遍历
  // 4. 四个方向递归
  for (let [dr, dc] of dirs) {
    dfs(grid, row + dr, col + dc, dirs, WATER);
  }
  // 5. 可选:回溯操作(如 694 题的 path += 'b')
}

一、基础入门:岛屿数量(LeetCode 200)

题目信息

  • 题号:LeetCode 200. 岛屿数量(Medium)

  • 链接:leetcode.cn/problems/nu...

  • 题目描述:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿由相邻的陆地(上下左右)连接形成,且完全被水包围。

  • 示例: 输入:grid = [ ["1","1","1","1","0"], ["1","1","0","1","0"], ["1","1","0","0","0"], ["0","0","0","0","0"] ] 输出:1

输入:grid = [ ["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"] ] 输出:3

解题思路

核心思路:「遇到陆地就计数,然后淹没整座岛」,避免重复统计。

  1. 遍历整个二维网格,逐行逐列检查每一个格子;

  2. 若遇到陆地('1'),说明发现新岛屿,计数器 +1;

  3. 调用 DFS 函数,将当前陆地及所有相邻的陆地(上下左右)全部置为水('0'),即「淹没」整座岛;

  4. 遍历结束后,计数器的值即为岛屿总数。

该思路本质是「FloodFill 算法」,通过 DFS 遍历连通块,并用淹没法标记已遍历区域,时间复杂度 O(m×n)(m、n 分别为网格的行数和列数),空间复杂度 O(m×n)(最坏情况下,整个网格都是陆地,DFS 递归栈深度为 m×n)。

完整代码(原样保留)

javascript 复制代码
/**
 * @param {character[][]} grid
 * @return {number}
 */
/**
 * LeetCode 200 岛屿数量
 * 核心思想:遇到陆地(1)就计数+1,然后用 DFS 把整座岛全部淹成海水(0),防止重复计数
 */
var numIslands = function (grid) {
//  前置定义拿过来 
  const WATER = '0'

  // 记录最终岛屿数量
  let res = 0;

  // ==================== 1. 遍历整个网格 ====================
  // 逐行逐列扫描每一个格子
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      // 如果当前是海水(0),直接跳过,不处理
      if (grid[row][col] === '0') continue;

      // ==================== 关键逻辑 ====================
      // 代码走到这里 → 发现了一块【新岛屿】
      // 1. 岛屿数量 +1
      res++;
      // 2. 立刻调用 DFS,把【整座岛】全部淹成 0
      // 这样后面遍历就不会重复计数
      dfs(row, col);
    }
  }

  // 遍历完成,返回总岛屿数
  return res;

  // ==================== 2. DFS 核心函数:淹没岛屿 ====================
  // 从 (row, col) 开始,把所有相连的陆地全部变成 0
  // 上面的模板 function dfs
};

二、进阶练习:封闭岛屿数量(LeetCode 1254)

题目信息

  • 题号:LeetCode 1254. 统计封闭岛屿的数目(Medium)

  • 链接:leetcode.cn/problems/nu...

  • 题目描述:二维矩阵 grid 由 0(陆地)和 1(水)组成。封闭岛屿是指完全被水包围的陆地,且不与网格的边界相连。请计算封闭岛屿的数量。

  • 示例: 输入:grid = [[1,1,1,1,1,1,1,0],[1,0,0,0,0,1,1,0],[1,0,1,0,1,1,1,0],[1,0,0,0,0,1,0,1],[1,1,1,1,1,1,1,0]] 输出:2

输入:grid = [[0,0,1,0,0],[0,1,0,1,0],[0,1,1,1,0]] 输出:1

解题思路

核心思路:「先淹边界,再数中间」,边界上的陆地一定不是封闭岛。

  1. 第一步:遍历网格的四条边界,将所有边界上的陆地(0)及与其相连的陆地全部淹成水(1);

  2. 第二步:遍历网格的中间区域(不包含边界),此时剩下的陆地一定是封闭岛;

  3. 遇到中间的陆地,计数器 +1,同时调用 DFS 淹没整座岛,避免重复统计;

  4. 遍历结束后,计数器的值即为封闭岛屿的数量。

与「岛屿数量」题的区别:多了「先淹边界」的步骤,核心还是 DFS 淹没法,时间复杂度和空间复杂度均为 O(m×n)。

完整代码(原样保留)

javascript 复制代码
/**
 * LeetCode 1254. 统计封闭岛屿的数目
 * 规则:只有完全被水包围、不接触四条边的陆地,才是封闭岛屿
 * 核心思路:先淹掉所有靠边的岛 → 再数中间剩下的岛
 */
var closedIsland = function (grid) {
  // 前置定义
  // 常量定义:1 代表水(方便阅读,不用写魔法数字)
  const WATER = 1;
  
  // 记录最终封闭岛屿的数量
  let res = 0;

  // ==================== 第一步:淹掉所有【边界上的陆地】 ====================
  // 这些陆地贴着边,绝对不可能是封闭岛,必须先淹掉
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      // 如果当前是水,直接跳过
      if (grid[row][col] === WATER) continue;

      // 判断当前格子是否在【四条边界】上
      const isEdge = row === 0 || row === rows - 1 || col === 0 || col === cols - 1;

      // 如果是边界上的陆地 → 调用 DFS 把整座岛淹掉
      if (isEdge) dfs(row, col);
    }
  }

  // ==================== 第二步:统计中间的【封闭岛屿】 ====================
  // 只遍历【中间区域】(不包含四条边),剩下的陆地一定是封闭岛
  for (let row = 1; row < rows - 1; row++) {
    for (let col = 1; col < cols - 1; col++) {
      // 如果是水,跳过
      if (grid[row][col] === WATER) continue;

      // 代码走到这里 → 发现一个【封闭岛屿】
      res++; // 计数 +1
      dfs(row, col); // 淹掉,防止重复计数
    }
  }

  // 返回最终结果
  return res;

  // ==================== DFS 函数:淹没陆地(核心) ====================
  // 功能:把当前陆地及所有相连陆地全部淹成水
  // dfs
};

三、延伸练习:飞地数量(LeetCode 1020)

题目信息

  • 题号:LeetCode 1020. 飞地的数量(Medium)

  • 链接:leetcode.cn/problems/nu...

  • 题目描述:给你一个大小为 m x n 的二进制矩阵 grid ,其中 0 表示海水,1 表示陆地。飞地是指无法到达边界的陆地。请返回飞地的数量。

  • 示例: 输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]] 输出:3

输入:grid = [[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]] 输出:0

解题思路

核心思路:与「封闭岛屿数量」完全一致,只是最终统计的是「剩余陆地的个数」,而非「岛屿的个数」。

  1. 第一步:遍历网格的四条边界,将所有边界上的陆地(1)及与其相连的陆地全部淹成水(0);

  2. 第二步:遍历网格的所有区域,统计剩余的陆地(1)的个数,即为飞地的数量。

本质是「封闭岛屿数量」的变种,核心还是 DFS 淹没法,区别在于最终统计的是「陆地单元格数量」,而非「岛屿个数」。

完整代码(原样保留)

javascript 复制代码
/**
 * LeetCode 1020 飞地的数量
 * 核心思路:先淹掉边界连通的陆地 → 剩下的陆地总数就是答案
 */
var numEnclaves = function (grid) {
  // 前置定义
  // 题目定义:0 = 水,1 = 陆地
  const WATER = 0;

  
  // ===================== 第一步:淹掉所有边界连通的陆地 =====================
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      // 水直接跳过
      if (grid[row][col] === WATER) continue;

      // 判断是否在四条边上
      const isEdge = row === 0 || row === rows - 1 || col === 0 || col === cols - 1;
      // 边界上的陆地 → 淹掉
      if (isEdge) dfs(row, col);
    }
  }

  // ===================== 第二步:统计中间剩余的陆地 =====================
  let res = 0;
  // 只遍历中间区域(不包含边界)
  for (let row = 1; row < rows - 1; row++) {
    for (let col = 1; col < cols - 1; col++) {
      // 是水就跳过
      if (grid[row][col] === WATER) continue;
      // 是陆地就计数
      res++;
    }
  }

  return res;
  // 搬过来dfs
  
};

四、拓展练习:岛屿最大面积(LeetCode 695)

题目信息

  • 题号:LeetCode 695. 岛屿的最大面积(Medium)

  • 链接:leetcode.cn/problems/ma...

  • 题目描述:给你一个大小为 m x n 的二进制矩阵 grid 。岛屿是由一些相邻的 1 (代表土地) 构成的组合,相邻要求为上下左右相邻。请你计算并返回最大的岛屿面积。如果没有岛屿,则返回 0 。

  • 示例: 输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]] 输出:6

解题思路

核心思路:「遍历岛屿,统计每座岛的面积,记录最大值」。

  1. 遍历整个二维网格,逐行逐列检查每一个格子;

  2. 若遇到陆地(1),说明发现新岛屿,重置当前岛屿面积计数器;

  3. 调用 DFS 函数,遍历整座岛,统计当前岛屿的面积(每遍历一块陆地,面积计数器 +1),同时将陆地淹成水(避免重复统计);

  4. 每统计完一座岛,更新最大岛屿面积;

  5. 遍历结束后,返回最大岛屿面积。

与「岛屿数量」题的区别:多了「面积统计」的步骤,核心还是 DFS 遍历连通块,时间复杂度和空间复杂度均为 O(m×n)。

完整代码(原样保留)

javascript 复制代码
/**
 * LeetCode 695 最大岛屿面积
 * 题意:找出 grid 中最大的岛屿面积(岛屿由相邻陆地1组成,只算上下左右)
 * 核心思路:遇到岛屿 → DFS统计面积 → 记录最大值
 */
var maxAreaOfIsland = function (grid) {
  // 。。。。
  // 定义:0 代表水
  const WATER = 0;

 
  // 记录最大的岛屿面积
  let maxCount = 0;
  // 记录当前正在统计的岛屿面积
  let curCount = 0;

  // ==================== 第一步:遍历整个网格 ====================
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      // 如果是水,直接跳过
      if (grid[row][col] === WATER) continue;

      // 发现新岛屿 → 重置当前面积
      curCount = 0;
      // DFS 统计面积并淹没岛屿
      dfs(row, col);
      // 更新最大面积
      maxCount = Math.max(maxCount, curCount);
    }
  }

  // 返回最大岛屿面积
  return maxCount;

  // ==================== DFS:统计面积 + 淹没岛屿 ====================
  function dfs(row, col) {
    // 越界直接返回
    if (row < 0 || col < 0 || row >= rows || col >= cols) return;
    // 如果是水,直接返回
    if (grid[row][col] === WATER) return;

    // 当前陆地面积 +1
    curCount++;
    // 把陆地淹成水,避免重复统计
    grid[row][col] = WATER;

    // 向四个方向扩散
    for (let [dr, dc] of dirs) {
      dfs(row + dr, col + dc);
    }
  }
};

五、进阶挑战:子岛屿数量(LeetCode 1905)

题目信息

  • 题号:LeetCode 1905. 统计子岛屿数目(Medium)

  • 链接:leetcode.cn/problems/co...

  • 题目描述:给你两个 m x n 的二进制矩阵 grid1 和 grid2 ,其中 1 表示陆地,0 表示海水。如果 grid2 的一个岛屿,每一块陆地都在 grid1 对应的位置上也是陆地,则称 grid2 的这个岛屿为 grid1 的子岛屿。请你返回 grid2 中子岛屿的数目。

  • 示例: 输入:grid1 = [[1,1,1,0,0],[0,1,1,1,1],[0,0,0,0,0],[1,0,0,0,0],[1,1,0,1,1]], grid2 = [[1,1,1,0,0],[0,0,1,1,1],[0,1,0,0,0],[1,0,1,1,0],[0,1,0,1,0]] 输出:3

解题思路

  1. 遍历 grid2,遇到陆地(1)就调用 DFS 遍历整座岛;

  2. DFS 过程中,检查当前陆地在 grid1 中是否为水(0):若有任意一块陆地在 grid1 中是水,则该岛不是子岛屿;

  3. 遍历完一座岛后,若所有陆地都满足在 grid1 中也是陆地,则子岛屿计数器 +1;

  4. 遍历结束后,返回计数器的值。

javascript 复制代码
/**
 * LeetCode 1905. 统计子岛屿数目
 * 最优解法 DFS
 * 核心思路:
 * 1. 遍历 grid2,遇到岛屿就 DFS
 * 2. DFS 过程中检查:只要 grid2 是陆地且 grid1 是水 → 不是子岛
 * 3. 整座岛遍历完都满足 → 计数+1
 */
var countSubIslands = function (grid1, grid2) {
  // 统计子岛屿的数量
  let res = 0;

  const rows = grid1.length; // 行数
  const cols = grid1[0].length; // 列数
  const WATER = 0; // 0 代表水

  // 上下左右四个方向
  const dirs = [
    [0, -1], // 左
    [0, 1], // 右
    [1, 0], // 下
    [-1, 0], // 上
  ];

  // 标记当前遍历的岛屿是否是【子岛屿】
  let isSub = true;

  // 遍历整个 grid2
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      // 如果是水,直接跳过
      if (grid2[row][col] === WATER) continue;

      // 发现新岛屿,先假设它是子岛屿
      isSub = true;
      // DFS 检查整座岛 + 淹没
      dfs(row, col);

      // 如果遍历完依旧是 true,说明是子岛屿
      if (isSub) {
        res++;
      }
    }
  }

  return res;

  // DFS 核心函数
  function dfs(row, col) {
    // 越界直接返回
    if (row < 0 || col < 0 || row >= rows || col >= cols) return;
    // 当前是水,返回
    if (grid2[row][col] === WATER) return;

    // ==================== 核心判断 ====================
    // 如果 grid2 这里是陆地,但 grid1 是水
    // 说明这座岛 绝对不是子岛屿
    if (grid1[row][col] === WATER) {
      isSub = false;
    }

    // 淹没当前陆地(避免重复遍历)
    grid2[row][col] = WATER;

    // 向四个方向继续扩散检查
    for (let [dr, dc] of dirs) {
      dfs(row + dr, col + dc);
    }
  }
};

六、终极挑战:不同岛屿数量(LeetCode 694)

题目信息

  • 题号:LeetCode 694. 不同岛屿的数量(Medium)

  • 链接:leetcode.cn/problems/nu...

  • 题目描述:给定一个非空 01 二维数组 grid ,一个岛屿是由四个方向(上下左右)的 1 组成的连通块。两个岛屿被认为是相同的,当且仅当一个岛屿可以通过平移(不旋转、不翻转)得到另一个岛屿。请返回不同岛屿的数量。

  • 示例: 输入:grid = [[1,1,0,0,0],[1,1,0,0,0],[0,0,0,1,1],[0,0,0,1,1]] 输出:1

输入:grid = [[1,1,0,1,1],[1,0,0,0,0],[0,0,0,0,1],[1,1,0,1,1]] 输出:3

解题思路

核心思路:「给岛屿生成唯一路径签名,用 Set 去重」,路径签名相同 → 岛屿形状相同(平移可重合)。

  1. 遍历整个二维网格,遇到陆地(1)就调用 DFS 遍历整座岛;

  2. DFS 过程中,记录「移动路径」作为岛屿的形状签名:

    • 用 's' 表示起点;

    • 用 'l'/'r'/'u'/'d' 表示左/右/下/上四个移动方向;

    • 用 'b' 表示回溯(关键!避免不同形状生成相同签名);

  3. 将每座岛的路径签名存入 Set(自动去重);

  4. 遍历结束后,Set 的大小即为不同岛屿的数量。

关键注意点:回溯标记 'b' 必须加,否则不同形状的岛屿可能生成相同的路径签名,导致去重错误。时间复杂度 O(m×n),空间复杂度 O(m×n)(存储路径签名)。

完整代码(原样保留)

javascript 复制代码
/**
 * LeetCode 694. 不同岛屿的数量
 * 核心思路:DFS 遍历岛屿,记录【移动路径】作为形状签名
 * 相同路径 = 相同形状,用 Set 去重,最后返回 Set 大小
 */
var numDistinctIslands = function (grid) {
  // Set 存储所有不同的岛屿形状(自动去重)
  let set = new Set();

  const rows = grid.length; // 网格行数
  const cols = grid[0].length; // 网格列数
  const WATER = 0; // 0 = 水

  // 【你自己的写法】用 Map 把【方向数组】映射成【方向字符】
  // 上下左右 → 对应字符 l r u d
  const dirToChar = new Map([
    [[0, -1], 'l'], // 左
    [[0, 1], 'r'], // 右
    [[1, 0], 'u'], // 下
    [[-1, 0], 'd'], // 上
  ]);

  // 记录每一座岛屿的路径签名
  let path = '';

  // 1. 遍历整个网格
  for (let row = 0; row < rows; row++) {
    for (let col = 0; col < cols; col++) {
      // 水 → 跳过
      if (grid[row][col] === WATER) continue;

      // 发现新岛屿!
      // 清空路径,准备记录新岛屿的形状
      path = '';
      // DFS 遍历整座岛,生成路径签名
      dfs(row, col, 's'); // 's' = start 起点
      // 把形状存入 Set(自动去重)
      set.add(path);
    }
  }

  // 不同形状的数量 = 答案
  return set.size;

  // 2. DFS 核心:生成路径签名 + 淹没岛屿
  function dfs(row, col, dirChar) {
    // 越界 → 退出
    if (row < 0 || col < 0 || row >= rows || col >= cols) return;
    // 是水 → 退出
    if (grid[row][col] === WATER) return;

    // ✅ 关键:把当前方向加入路径(记录形状)
    path += dirChar;
    // 淹没陆地,防止重复遍历
    grid[row][col] = WATER;

    // 遍历四个方向,递归检查
    for (let [[dr, dc], char] of dirToChar.entries()) {
      dfs(row + dr, col + dc, char);
    }

    // ✅ 关键:回溯标记 'b'
    // 保证不同形状一定生成不同路径
    path += 'b';
  }
};

总结:岛屿系列题通用模板

所有岛屿题均围绕该模板展开,差异仅在于「核心操作」和「遍历前/后的额外处理」:

  • 基础题(200、695):核心操作是「计数/统计面积」;

  • 边界相关题(1254、1020):额外处理是「先淹边界」;

  • 进阶题(1905):核心操作是「验证子岛屿条件」;

  • 终极题(694):核心操作是「记录路径签名」。

掌握该模板后,所有岛屿类 DFS 题都能迎刃而解。建议多练习、多总结,熟练掌握「淹没法」和「路径签名」这两个核心技巧,就能轻松应对 LeetCode 中所有连通块相关问题。

相关推荐
WolfGang0073213 小时前
代码随想录算法训练营 Day16 | 二叉树 part06
算法
霍理迪3 小时前
Vue的响应式和生命周期
前端·javascript·vue.js
小码哥_常4 小时前
Java后端定时任务抉择:@Scheduled、Quartz、XXL - Job终极对决
后端
uzong4 小时前
Skill 被广泛应用,到底什么是 Skill,今天详细介绍一下
人工智能·后端·面试
小码哥_常4 小时前
Kafka平替!SpringBoot+Redis Stream+消费组打造极致消息队列
后端
2401_831824964 小时前
代码性能剖析工具
开发语言·c++·算法
Sunshine for you5 小时前
C++中的职责链模式实战
开发语言·c++·算法
IT_陈寒5 小时前
Redis缓存击穿:3个鲜为人知的防御策略,90%开发者都忽略了!
前端·人工智能·后端
qq_416018725 小时前
C++中的状态模式
开发语言·c++·算法