题目描述
给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例
示例 1:
输入:grid = [
['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']
]
输出:1
示例 2:
输入:grid = [
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
输出:3
提示:
- m == grid.length
- n == grid[i].length
- 1 <= m, n <= 300
- grid[i][j] 的值为 '0' 或 '1'
解题思路总览
| 方法 | 核心思想 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|---|
| DFS(深度优先搜索) | 从每个未访问的陆地开始遍历 | O(m*n) | O(m*n) | 推荐解法 |
| BFS(广度优先搜索) | 用队列逐层扩展 | O(m*n) | O(m*n) | 思路类似 |
| 并查集 | 将陆地进行合并 | O(m*n * alpha) | O(m*n) | 较复杂 |
| 迭代 DFS | 用栈模拟递归 | O(m*n) | O(m*n) | 避免递归栈溢出 |
一、核心解法:DFS(深度优先搜索)
核心思想
遍历整个网格,遇到未访问的陆地 '1' 时,就找到了一个新岛屿,然后从这个位置出发,深度优先搜索把所有相邻的陆地都标记为已访问。
核心步骤:
1. 遍历网格每个位置
2. 遇到未访问的 '1',岛屿数 +1
3. 从该位置出发 DFS,标记所有相连的 '1'
4. 继续遍历,重复上述过程
关键洞察
为什么 DFS 能解决问题?
因为岛屿的定义是:相邻(上下左右)的陆地连成的区域。
当我们找到一个未访问的陆地 '1' 时:
- 它是一个新岛屿的起点
- 通过 DFS 可以找到所有和它相连的陆地
- 这些陆地都属于同一个岛屿
遍历完成后,所有陆地都被标记,岛屿数量就是答案。
图解
grid = [
['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']
]
遍历过程:
初始: visited 全部为 false
ans = 0
(i=0, j=0): grid[0][0]='1', 未访问
ans = 1
DFS(0,0): 标记 [0,0],[0,1],[0,2],[0,3],[1,0],[1,1],[1,2],[2,0],[2,1],[2,2]
visited: 第一行全标记,第二行前两列标记,第三行前两列标记
(i=0, j=1-3): 已被访问,跳过
(i=0, j=4): '0',跳过
(i=1, j=0-4): 已被访问,跳过
(i=2, j=0-4): 已被访问,跳过
(i=3, j=0-4): 全为 '0',跳过
输出: ans = 1
grid = [
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
遍历过程:
初始: visited 全部为 false
ans = 0
(i=0, j=0): grid[0][0]='1', 未访问
ans = 1
DFS(0,0): 标记 [0,0],[0,1],[1,0],[1,1]
visited: 左上角 2x2 区域
(i=0, j=2-4): '0' 或已访问,跳过
(i=1, j=2-4): 跳过
(i=2, j=0-1): '0',跳过
(i=2, j=2): grid[2][2]='1', 未访问
ans = 2
DFS(2,2): 标记 [2,2]
(i=2, j=3-4): 已访问或 '0'
(i=3, j=0-2): '0',跳过
(i=3, j=3): grid[3][3]='1', 未访问
ans = 3
DFS(3,3): 标记 [3,3],[3,4]
visited: 右下角 2x2 区域
输出: ans = 3
二、算法流程图
输入: grid = [
['1','1','1'],
['1','1','0'],
['0','0','1']
]
初始化:
visited = [[false,false,false],
[false,false,false],
[false,false,false]]
ans = 0
dir = [[-1,0],[0,-1],[1,0],[0,1]] // 上左下右
遍历 (i=0, j=0):
grid[0][0]='1', visited[0][0]=false
ans = 1
DFS(0,0):
visited[0][0]=true
尝试上: i=-1, 越界
尝试左: j=-1, 越界
尝试下: (1,0)='1', 未访问 -> DFS(1,0)
visited[1][0]=true
上下左右探索 -> 递归到 (0,0), (2,0), (1,1)
尝试右: (0,1)='1', 未访问 -> DFS(0,1)
visited[0][1]=true
继续探索...
遍历完成:
ans = 2 (两个岛屿)
输出: 2
三、完整代码实现
cpp
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
if (grid.empty()) return 0;
int m = grid.size();
int n = grid[0].size();
vector<vector<bool>> visited(m, vector<bool>(n, false));
// 方向:上、左、下、右
int dir[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
function<void(int, int)> dfs = [&](int i, int j) {
// 如果已经访问过,直接返回
if (visited[i][j]) return;
// 标记为已访问
visited[i][j] = true;
// 尝试四个方向
for (int k = 0; k < 4; k++) {
int ii = i + dir[k][0];
int jj = j + dir[k][1];
// 检查越界和是否为陆地
if (ii < 0 || ii >= m || jj < 0 || jj >= n) continue;
if (grid[ii][jj] == '0') continue;
// 递归搜索
dfs(ii, jj);
}
};
int ans = 0;
// 遍历整个网格
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 找到未访问的陆地,就是新岛屿
if (grid[i][j] == '1' && !visited[i][j]) {
ans++;
dfs(i, j);
}
}
}
return ans;
}
};
四、逐行解析
cpp
if (grid.empty()) return 0;
- 处理空网格的特殊情况
cpp
int m = grid.size();
int n = grid[0].size();
vector<vector<bool>> visited(m, vector<bool>(n, false));
m, n:网格的行数和列数visited:记录每个位置是否被访问过
cpp
int dir[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
- 四个方向的偏移量:上、左、下、右
cpp
function<void(int, int)> dfs = [&](int i, int j) {
- 定义 DFS 函数,用 lambda 方便递归调用
cpp
if (visited[i][j]) return;
visited[i][j] = true;
- 如果已经访问过,直接返回(避免重复访问)
- 否则标记为已访问
cpp
for (int k = 0; k < 4; k++) {
int ii = i + dir[k][0];
int jj = j + dir[k][1];
if (ii < 0 || ii >= m || jj < 0 || jj >= n) continue;
if (grid[ii][jj] == '0') continue;
dfs(ii, jj);
}
- 遍历四个方向
- 越界检查和是否为陆地的检查
- 递归搜索相邻的陆地
cpp
if (grid[i][j] == '1' && !visited[i][j]) {
ans++;
dfs(i, j);
}
- 如果遇到未访问的陆地,说明发现了新岛屿
- ans++,然后从该位置开始 DFS 标记整个岛屿
五、DFS 的原理
DFS 的核心思想:沿着一条路走到底,然后回溯。
对于岛屿问题:
1. 从起点出发,把起点标记为已访问
2. 依次尝试四个方向
3. 如果相邻位置是未访问的陆地,递归进入
4. 每个位置只会被访问一次
类似"染色"的过程:
- 找到一个未染色陆地,染成新颜色
- 递归把所有相邻的陆地染成同样的颜色
- 继续找下一个未染色陆地
六、与并查集对比
| 维度 | DFS | 并查集 |
|---|---|---|
| 代码复杂度 | 简单 | 较复杂 |
| 时间复杂度 | O(m*n) | O(m*n * alpha) |
| 空间复杂度 | O(m*n) | O(m*n) |
| 实现方式 | 递归或栈 | 数组 + 路径压缩 |
| 适用场景 | 岛屿、连通区域 | 需要动态合并的场景 |
七、复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 备注 |
|---|---|---|---|
| DFS | O(m*n) | O(m*n) | 推荐,递归深度最多 m*n |
| BFS | O(m*n) | O(m*n) | 用队列存储 |
| 并查集 | O(m*n) | O(m*n) | 较复杂 |
| 迭代 DFS | O(m*n) | O(m*n) | 避免递归栈溢出 |
详细分析:
时间复杂度:
每个格子最多被访问一次
每次访问时进行常数次操作
总计:O(m*n)
空间复杂度:
visited 数组:O(m*n)
递归栈深度:最坏 O(m*n)(当整个网格都是陆地时)
总计:O(m*n)
八、边界情况分析
| 情况 | 处理方式 |
|---|---|
| 空网格 | return 0 |
| 全是水 ('0') | return 0 |
| 全是陆地 ('1') | return 1 |
| 单个陆地 | return 1 |
| 网格只有一行 | 正常处理 |
| 网格只有一列 | 正常处理 |
示例:全是水
grid = [
['0','0','0'],
['0','0','0']
]
遍历:
所有位置都是 '0',不满足 grid[i][j] == '1'
ans = 0
输出: 0
示例:全是陆地
grid = [
['1','1','1'],
['1','1','1']
]
遍历:
(0,0): 发现新岛屿,ans=1,DFS 标记整个网格
剩余位置都已访问
输出: 1
九、面试追问 FAQ
| 问题 | 回答要点 |
|---|---|
| Q: 为什么需要 visited 数组? | 防止重复访问同一个位置,导致无限递归或重复计数 |
| Q: 能否不用 visited? | 可以原地修改 grid,但会破坏输入数据 |
| Q: 递归深度太大怎么办? | 用栈模拟递归,或者 BFS |
| Q: 时间复杂度为什么是 O(m*n)? | 每个格子最多被访问一次 |
| Q: 能否用 BFS 代替 DFS? | 可以,BFS 用队列,DFS 用栈或递归 |
| Q: 四个方向的顺序重要吗? | 不重要,只要遍历完四个方向即可 |
十、相关题目
| 题目编号 | 题目名称 | 难度 | 核心差异 |
|---|---|---|---|
| 200 | 岛屿数量 | 中等 | 基础题,统计岛屿个数 |
| 695 | 岛屿的最大面积 | 中等 | 求最大岛屿面积 |
| 463 | 岛屿的周长 | 简单 | 求岛屿周长 |
| 694 | 不同岛屿的数量 | 困难 | 求不同形状岛屿数量 |
| 剑指 Offer 13 | 机器人的运动范围 | 中等 | BFS/DFS + 条件限制 |
| 79 | 单词搜索 | 中等 | DFS 回溯 |
十一、总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 遍历网格,遇到未访问的陆地就计数并 DFS 标记 |
| 关键数据结构 | visited 数组,防止重复访问 |
| 方向数组 | dir[4][2] = {{-1,0},{0,-1},{1,0},{0,1}} |
| 时间复杂度 | O(m*n)(每个格子最多访问一次) |
| 空间复杂度 | O(m*n)(visited + 递归栈) |
| 变形 | 求岛屿面积(统计岛屿内格子数)、求岛屿周长 |
| 易错点 | 越界检查、visited 判断 |
岛屿数量是经典的连通区域问题,通过 DFS/BFS 遍历网格,标记已访问的陆地,统计连通区域的数量。