哈喽各位,我是前端L。
欢迎来到我们的图论专题第六篇!我们已经学会了如何在"显式"的图(由节点和边列表定义)上进行探险。但如果,地图本身就是一张"网格"呢?
今天,我们要解决的"岛屿数量"问题,是算法面试中最经典、最基础 的网格遍历题。它将完美地向我们展示,如何将一个m x n的矩阵,视作 一个拥有 m * n 个节点、由"上下左右"关系连接的"隐式图"。而我们的DFS/BFS,就是在这张新地图上"航行"的完美工具。
力扣 200. 岛屿数量
https://leetcode.cn/problems/number-of-islands/

题目分析:
-
输入 :一个
m x n的二维网格grid,由'1'(陆地) 和'0'(水) 组成。 -
目标:计算图中"岛屿"的数量。
-
岛屿定义 :由水平或竖直 相邻的
'1'(陆地)连接而成的区域,且四周被水('0')环绕。
"Aha!"时刻:将"网格"翻译成"图"
-
节点 (Vertex) :每一个单元格
(r, c)都是图中的一个节点。 -
边 (Edge) :每个单元格
(r, c)与它的上(r-1, c)、下(r+1, c)、左(r, c-1)、右(r, c+1)邻居之间,都存在一条"隐式"的边。 -
我们要找什么? :我们只关心由
'1'(陆地)构成的"连通区域"。 -
问题被完美转化: 计算这个"隐式图"中,由
'1'构成的"连通分量 (Connected Components)"的个数。
解决方案:"淹没"岛屿 (Flood Fill)
如何计算"连通分量"的个数? 我们需要一个"侦察兵 "(主循环)和一个"作战部队"(DFS/BFS)。
算法流程:
-
初始化岛屿计数
islandCount = 0。 -
"侦察兵"出动 :用两层
for循环 ,遍历矩阵中的每一个 单元格(r, c)。 -
发现新目标 :在遍历时,
if (grid[r][c] == '1'):-
"Aha!" 我们发现了一块"陆地"!
-
由于我们的"作战部队"会把访问过的陆地都"淹没"(标记掉),所以,任何时候我们遇到的
'1',都必定是一个全新 的、未被发现的岛屿的"登陆点"。 -
islandCount++
-
-
"作战部队"出动:
-
从
(r, c)这个"登陆点"开始,启动一次 DFS 或 BFS。 -
这个 DFS/BFS 的任务,就是"淹没 (Flood Fill) ":将所有与
(r, c)连通 的、同属于这个岛屿的'1',全部标记为已访问 (比如,直接改成'0'或'2'),防止它们被"侦察兵"重复发现。
-
-
for循环结束后,islandCount就是最终答案。
代码实现 (O(V+E) -> O(m*n))
解法一:DFS (递归"淹没")
C++
#include <vector>
using namespace std;
class Solution {
private:
// "作战部队":DFS 函数
// 任务:从 (r, c) 出发,淹没所有相连的 '1'
void dfs_sink(vector<vector<char>>& grid, int r, int c) {
int m = grid.size();
int n = grid[0].size();
// 1. Base Case (越界或遇到水)
if (r < 0 || r >= m || c < 0 || c >= n || grid[r][c] == '0') {
return;
}
// 2. "淹没" (标记为已访问)
grid[r][c] = '0';
// 3. 递归探索邻居
dfs_sink(grid, r + 1, c); // 下
dfs_sink(grid, r - 1, c); // 上
dfs_sink(grid, r, c + 1); // 右
dfs_sink(grid, r, c - 1); // 左
}
public:
int numIslands(vector<vector<char>>& grid) {
if (grid.empty() || grid[0].empty()) {
return 0;
}
int m = grid.size();
int n = grid[0].size();
int islandCount = 0;
// "侦察兵":遍历所有单元格
for (int r = 0; r < m; ++r) {
for (int c = 0; c < n; ++c) {
if (grid[r][c] == '1') {
// 发现了新岛屿!
islandCount++;
// "作战部队"出动,淹没它
dfs_sink(grid, r, c);
}
}
}
return islandCount;
}
};
解法二:BFS (队列"淹没")
C++
#include <vector>
#include <queue>
using namespace std;
class Solution_BFS {
public:
int numIslands(vector<vector<char>>& grid) {
if (grid.empty() || grid[0].empty()) {
return 0;
}
int m = grid.size();
int n = grid[0].size();
int islandCount = 0;
// 邻居的方向数组
int dr[] = {0, 0, 1, -1};
int dc[] = {1, -1, 0, 0};
for (int r = 0; r < m; ++r) {
for (int c = 0; c < n; ++c) {
if (grid[r][c] == '1') {
islandCount++;
grid[r][c] = '0'; // 标记为已访问
queue<pair<int, int>> q;
q.push({r, c});
while (!q.empty()) {
pair<int, int> curr = q.front();
q.pop();
// 探索4个邻居
for (int i = 0; i < 4; ++i) {
int nr = curr.first + dr[i];
int nc = curr.second + dc[i];
// 检查邻居是否合法且是 '1'
if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == '1') {
grid[nr][nc] = '0'; // 淹没
q.push({nr, nc});
}
}
}
}
}
}
return islandCount;
}
};
深度复杂度分析
-
V (Vertices) :顶点数,即
m * n。 -
E (Edges) :边数,每个顶点最多4条边,所以
E最多是4 * m * n的级别。 -
时间复杂度 O(m * n):
-
我们的"侦察兵"
for循环,会访问m * n个单元格。 -
"作战部队" (DFS/BFS) 会在
grid[r][c] == '1'时启动。由于启动后它会"淹没"所有它能到达的1,确保了每个'1'单元格,只会被 DFS/BFS 核心逻辑访问一次。 -
总的来看,每个单元格
(r, c)(无论是0还是1)都被主循环和遍历逻辑,常数次地访问。 -
总时间复杂度 O(V + E) -> O(mn + 4 m*n) -> O(m * n)。
-
-
空间复杂度:
-
DFS :
O(m * n)。在最坏情况下(一个"蛇形"岛屿占满了整个网格),递归栈 的深度可能是m * n。 -
BFS :
O(min(m, n))。在最坏情况下(比如一个"棋盘格"),队列的大小最多是min(m, n)级别。(修正:一个"圆形"岛屿,队列大小可能达到 O(m n))*。(再修正:BFS的最坏空间是O(V),即O(m*n),例如一个从(0,0)开始的巨大岛屿)。 -
(注:如果我们不使用"原地修改"
grid[r][c]='0',而是用一个visited[m][n]数组,那么空间复杂度会额外增加 O(mn))*
-
总结
今天,我们打响了"图论"专题的"隐式图"第一枪!
-
"二维网格" = "隐式图"
-
"岛屿数量" = "连通分量个数"
-
DFS/BFS+visited(或原地修改) = "淹没算法 (Flood Fill)"
这个"网格即图"的思维模型,是图论应用中最重要、最常见的模式。
在下一篇中,我们将继续使用这个模型,但我们的任务不再是"计数",而是要计算"岛屿的最大面积"!
下期见!