图论专题(六):“隐式图”的登场!DFS/BFS 攻克「岛屿数量」
哈喽各位,我是前端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 + 4m*n) -> O(m * n)。
-
-
空间复杂度:
-
DFS:
O(m * n)。在最坏情况下(一个“蛇形”岛屿占满了整个网格),递归栈的深度可能是m * n。 -
BFS:
O(min(m, n))。在最坏情况下(比如一个“棋盘格”),队列的大小最多是min(m, n)级别。(修正:一个“圆形”岛屿,队列大小可能达到 O(mn))*。(再修正:BFS的最坏空间是O(V),即O(m*n),例如一个从(0,0)开始的巨大岛屿)。 -
(注:如果我们不使用“原地修改”
grid[r][c]='0',而是用一个visited[m][n]数组,那么空间复杂度会额外增加 O(mn))*
-
总结
今天,我们打响了“图论”专题的“隐式图”第一枪!
-
“二维网格” = “隐式图”
-
“岛屿数量” = “连通分量个数”
-
DFS/BFS+visited(或原地修改) = “淹没算法 (Flood Fill)”
这个“网格即图”的思维模型,是图论应用中最重要、最常见的模式。
在下一篇中,我们将继续使用这个模型,但我们的任务不再是“计数”,而是要计算“岛屿的最大面积”!
下期见!
