【LeetCode Hot100 刷题日记 (51/100)】200. 岛屿数量——深度优先搜索 / 广度优先搜索 / 并查集🌍

4 阅读6分钟

📌 题目链接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] = truegrid[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 操作

✅ 步骤 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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!