📌 题目链接:200. 岛屿数量 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:深度优先搜索(DFS)、广度优先搜索(BFS)、并查集(Union-Find)、图论
⏱️ 目标时间复杂度:O(MN)
💾 空间复杂度:DFS 为 O(MN),BFS 为 O(min(M, N)),并查集为 O(MN)
🧠 题目分析
本题要求我们统计一个由 '1'(陆地)和 '0'(水)组成的二维网格中岛屿的数量。岛屿的定义是:由水平或竖直方向上相邻的 '1' 连接而成的区域,且被水包围。
✅ 关键点:
- 只考虑 上下左右 四个方向(不包括对角线)。
- 每次发现一个未访问的
'1',就说明找到了一个新的岛屿。- 需要将该岛屿所有相连的
'1'标记为已访问,避免重复计数。
这类问题本质上是在无向图中求连通分量的数量,是图遍历的经典应用。
🧩 核心算法及代码讲解
本题有三种主流解法:
1️⃣ 深度优先搜索(DFS)——递归实现 ⚡
核心思想:
每当遇到一个 '1',就启动一次 DFS,将所有与之相连的 '1' 全部“淹没”(标记为 '0' 或记录为已访问),这样下次再遇到 '1' 就一定是新岛屿。
💡 面试重点:DFS 的递归写法简洁,但要注意栈溢出风险(在极端大网格时可能发生)。通常面试官会接受此解法,但可追问 BFS 或迭代 DFS。
✅ 代码与行注释(C++)
void dfs(vector<vector<char>>& grid, int r, int c) {
int nr = grid.size();
int nc = grid[0].size();
// 将当前陆地标记为水,防止重复访问
grid[r][c] = '0';
// 向上
if (r - 1 >= 0 && grid[r-1][c] == '1')
dfs(grid, r - 1, c);
// 向下
if (r + 1 < nr && grid[r+1][c] == '1')
dfs(grid, r + 1, c);
// 向左
if (c - 1 >= 0 && grid[r][c-1] == '1')
dfs(grid, r, c - 1);
// 向右
if (c + 1 < nc && grid[r][c+1] == '1')
dfs(grid, r, c + 1);
}
📌 技巧:直接修改原数组(in-place)节省空间,无需额外 visited 数组。这是本题 DFS 的常见优化。
2️⃣ 广度优先搜索(BFS)——队列实现 🧱
核心思想:
用队列模拟层级扩展。从一个 '1' 开始,将其入队并标记,然后逐层向外扩展,直到队列为空,完成一个岛屿的遍历。
⚠️ 高频面试陷阱:
必须在入队时立即标记为已访问!
如果等到出队才标记,会导致同一节点多次入队,造成 TLE(超时)或重复计算。
✅ 正确 BFS 写法要点:
- 入队即标记(
visited[x][y] = true或grid[x][y] = '0') - 使用
queue<pair<int, int>>存储坐标
3️⃣ 并查集(Union-Find)——连通分量统计 🔗
核心思想:
将每个 '1' 视为一个独立节点,遍历网格时,若当前格子是 '1',就尝试将其与上下左右的 '1' 合并(union)。最终连通分量数量即为岛屿数。
🎯 并查集优势:
- 支持动态合并
- 适合需要频繁查询连通性的场景
- 时间复杂度接近线性(α 函数 ≈ 常数)
📚 知识点补充:
- 路径压缩:
find时将路径上所有节点直接指向根,加速后续查询。- 按秩合并:合并时将小树挂到大树下,保持树平衡。
🧭 解题思路(分步详解)
✅ 步骤 1:遍历整个网格
- 双重循环遍历每个单元格
(i, j)
✅ 步骤 2:发现新岛屿
- 若
grid[i][j] == '1',说明发现一个未访问的岛屿 - 岛屿计数器
num_islands++
✅ 步骤 3:标记整座岛屿
-
调用 DFS / BFS / Union-Find 将该岛屿所有
'1'标记为已处理- DFS/BFS:直接修改为
'0'或使用 visited 数组 - Union-Find:初始化后,在遍历时进行 union 操作
- DFS/BFS:直接修改为
✅ 步骤 4:返回结果
- 遍历结束后,
num_islands即为答案
📊 算法分析
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | 面试评价 |
|---|---|---|---|---|
| DFS(递归) | O(MN) | O(MN)(最坏递归深度) | 网格不大、代码简洁 | ⭐⭐⭐⭐☆(首选) |
| BFS(队列) | O(MN) | O(min(M, N)) | 避免递归栈溢出 | ⭐⭐⭐⭐(需注意标记时机) |
| 并查集 | O(MN × α(MN)) | O(MN) | 动态连通性、扩展性强 | ⭐⭐⭐(展示数据结构功底) |
📌 α(MN) 是反阿克曼函数,实际中 ≤ 5,可视为常数。
💻 代码
✅ C++ 完整代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
private:
void dfs(vector<vector<char>>& grid, int r, int c) {
int nr = grid.size();
int nc = grid[0].size();
grid[r][c] = '0';
if (r - 1 >= 0 && grid[r-1][c] == '1') dfs(grid, r - 1, c);
if (r + 1 < nr && grid[r+1][c] == '1') dfs(grid, r + 1, c);
if (c - 1 >= 0 && grid[r][c-1] == '1') dfs(grid, r, c - 1);
if (c + 1 < nc && grid[r][c+1] == '1') dfs(grid, r, c + 1);
}
public:
int numIslands(vector<vector<char>>& grid) {
int nr = grid.size();
if (!nr) return 0;
int nc = grid[0].size();
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
dfs(grid, r, c);
}
}
}
return num_islands;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 测试用例 1
vector<vector<char>> grid1 = {
{'1','1','1','1','0'},
{'1','1','0','1','0'},
{'1','1','0','0','0'},
{'0','0','0','0','0'}
};
cout << "Test 1: " << sol.numIslands(grid1) << "\n"; // 输出: 1
// 测试用例 2
vector<vector<char>> grid2 = {
{'1','1','0','0','0'},
{'1','1','0','0','0'},
{'0','0','1','0','0'},
{'0','0','0','1','1'}
};
cout << "Test 2: " << sol.numIslands(grid2) << "\n"; // 输出: 3
return 0;
}
✅ JavaScript 完整代码
/**
* @param {character[][]} grid
* @return {number}
*/
var numIslands = function(grid) {
if (!grid || grid.length === 0) return 0;
const nr = grid.length;
const nc = grid[0].length;
let numIslands = 0;
function dfs(r, c) {
if (r < 0 || r >= nr || c < 0 || c >= nc || grid[r][c] === '0') {
return;
}
grid[r][c] = '0'; // 淹没当前陆地
dfs(r - 1, c); // 上
dfs(r + 1, c); // 下
dfs(r, c - 1); // 左
dfs(r, c + 1); // 右
}
for (let r = 0; r < nr; r++) {
for (let c = 0; c < nc; c++) {
if (grid[r][c] === '1') {
numIslands++;
dfs(r, c);
}
}
}
return numIslands;
};
// 测试
console.log(numIslands([
['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']
])); // 1
console.log(numIslands([
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
])); // 3
🌟 总结与面试建议
- DFS 是本题最简洁高效的解法,适合大多数面试场景。
- BFS 要特别注意“入队即标记” ,这是高频错误点。
- 并查集虽不常用,但能体现你对图论和数据结构的深入理解,可在时间充裕时作为 bonus 提出。
- 空间优化技巧:直接修改原数组(in-place)比额外开 visited 数组更省空间,且符合题目“可修改输入”的隐含条件。
💬 面试话术示例:
“这道题本质是求无向图的连通分量数量。我首先想到用 DFS,因为它代码简洁、逻辑清晰。遍历每个格子,遇到'1'就启动 DFS 把整块岛屿‘淹没’,这样就不会重复计数。时间复杂度是 O(MN),因为每个格子最多访问一次。”
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!