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)
-
题目描述:给你一个由 '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'),说明发现新岛屿,计数器 +1;
-
调用 DFS 函数,将当前陆地及所有相邻的陆地(上下左右)全部置为水('0'),即「淹没」整座岛;
-
遍历结束后,计数器的值即为岛屿总数。
该思路本质是「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)
-
题目描述:二维矩阵 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
解题思路
核心思路:「先淹边界,再数中间」,边界上的陆地一定不是封闭岛。
-
第一步:遍历网格的四条边界,将所有边界上的陆地(0)及与其相连的陆地全部淹成水(1);
-
第二步:遍历网格的中间区域(不包含边界),此时剩下的陆地一定是封闭岛;
-
遇到中间的陆地,计数器 +1,同时调用 DFS 淹没整座岛,避免重复统计;
-
遍历结束后,计数器的值即为封闭岛屿的数量。
与「岛屿数量」题的区别:多了「先淹边界」的步骤,核心还是 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)
-
题目描述:给你一个大小为 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)及与其相连的陆地全部淹成水(0);
-
第二步:遍历网格的所有区域,统计剩余的陆地(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)
-
题目描述:给你一个大小为 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),说明发现新岛屿,重置当前岛屿面积计数器;
-
调用 DFS 函数,遍历整座岛,统计当前岛屿的面积(每遍历一块陆地,面积计数器 +1),同时将陆地淹成水(避免重复统计);
-
每统计完一座岛,更新最大岛屿面积;
-
遍历结束后,返回最大岛屿面积。
与「岛屿数量」题的区别:多了「面积统计」的步骤,核心还是 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)
-
题目描述:给你两个 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
解题思路
-
遍历 grid2,遇到陆地(1)就调用 DFS 遍历整座岛;
-
DFS 过程中,检查当前陆地在 grid1 中是否为水(0):若有任意一块陆地在 grid1 中是水,则该岛不是子岛屿;
-
遍历完一座岛后,若所有陆地都满足在 grid1 中也是陆地,则子岛屿计数器 +1;
-
遍历结束后,返回计数器的值。
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)
-
题目描述:给定一个非空 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)就调用 DFS 遍历整座岛;
-
DFS 过程中,记录「移动路径」作为岛屿的形状签名:
-
用 's' 表示起点;
-
用 'l'/'r'/'u'/'d' 表示左/右/下/上四个移动方向;
-
用 'b' 表示回溯(关键!避免不同形状生成相同签名);
-
-
将每座岛的路径签名存入 Set(自动去重);
-
遍历结束后,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 中所有连通块相关问题。