惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
云风的 BLOG
云风的 BLOG
美团技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
爱范儿
爱范儿
Stack Overflow Blog
Stack Overflow Blog
WordPress大学
WordPress大学
GbyAI
GbyAI
雷峰网
雷峰网
P
Proofpoint News Feed
IT之家
IT之家
人人都是产品经理
人人都是产品经理
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
aimingoo的专栏
aimingoo的专栏
小众软件
小众软件
T
The Blog of Author Tim Ferriss
月光博客
月光博客
V
Visual Studio Blog
L
LINUX DO - 热门话题
L
Lohrmann on Cybersecurity
T
Troy Hunt's Blog
Project Zero
Project Zero
U
Unit 42
T
Tor Project blog
Scott Helme
Scott Helme
L
LINUX DO - 最新话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
I
InfoQ
Cloudbric
Cloudbric
P
Proofpoint News Feed
The Cloudflare Blog
H
Heimdal Security Blog
Google DeepMind News
Google DeepMind News
The GitHub Blog
The GitHub Blog
Attack and Defense Labs
Attack and Defense Labs
有赞技术团队
有赞技术团队
T
Threat Research - Cisco Blogs
T
The Exploit Database - CXSecurity.com
J
Java Code Geeks
博客园 - 【当耐特】
Security Latest
Security Latest
The Register - Security
The Register - Security
F
Fortinet All Blogs
I
Intezer
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MongoDB | Blog
MongoDB | Blog
N
Netflix TechBlog - Medium
NISL@THU
NISL@THU
T
Tenable Blog

qlAD 的技术笔记

2026 年 6 月 7 日 -- 我有社交恐惧症,恐惧外面到处雷电风雨声(大学生活记录) 手把手带你实现哈希表(C/C++ 数据结构) 手把手带你实现二叉树(C/C++ 数据结构) 手把手带你实现栈和队列(C/C++ 数据结构) 手把手带你实现单/双链表(C/C++ 数据结构) 手把手带你实现动态数组(C/C++ 数据结构) 2026 年 1 月 2 日 -- 我不怕冷,因为我内心炽热 【2025 年度总结】(大学生活记录) 2025 年 12 月 2 日 -- 省电模式:大一 100 天耗电日志(大学生活记录) 蓝桥杯备赛第一期 —— 枚举与模拟 2025 年 11 月 3 日 -- 用爱发电、大爱无私(大学生活记录) 使用循环嵌套输出规则图形找规律讲解(嵌套循环-图形输出) 2025 年 10 月 1 日 -- 二字开头第一年的生日记录及感悟随笔(大学生活记录) Python 初级 04 -- 目前为止 Python 中你们做题可能会遇到的问题以及七七八八 2025 年 9 月 24 日 -- 开学后的第一课及军训汇演(大学生活记录) Python 初级 03 -- 程序设计的三种结构之选择结构和循环结构 Python 初级 02 -- 基础的数据类型和输入输出 2025 年 9 月 17 日 -- 大连 2025 国际大体联足球世界杯(大学生活记录) Python 初级 01 -- 初识编程、理解计算机语言及程序运行方式 适合大一新生学习编程的前一课 -- 编程先导(编程的本质) 2025 年 9 月 12 日 -- 三天军训感受以及一些七七八八(大学生活记录) 2025 年 9 月 9 日 -- 体检、军训前的准备(大学生活记录) 适合大一新生的计算机基础知识 -- 个人认为足够版 2025 年 9 月 7 日 -- 既来之、则安之(大学生活记录) C++ 程序的内存布局 —— 代码区、全局/静态区、栈区和堆区 联想小新 pro 16 2025 开箱记录:跳过联网、office 激活、更换 win 11 专业版 R 语言中的数学函数 计算机软件著作权申请过程记录 从聚类到回归:用 Python 解析鸢尾花数据集的完整数据科学流程 条件概率、全概率与贝叶斯公式 —— 一篇文章带你死磕概率公式 高中化学物质反应规则表 —— 方程式之间也有规律可循 集合元素性质、关系、子集公式、基本运算 —— 一篇文章带你全方面死磕集合 如何为你的项目选择合适的许可证 -- 全方位指南 解释 3:1 和 9:3:3:1 的详细来源 —— 建立减数分裂与孟德尔遗传学定律之间的联系 重新设置磁盘分区以及 Btrfs 子卷(subvolume)布局的无损方案 零基础实战:用 Docker 启动 dockermailserver 绕过 25 端口封锁搭建个人邮箱服务器 基于 GitHub Actions + OSS 的自托管 APT 仓库全面指南 如何在安装 Debian 时配置 Btrfs 子卷 | 详细分步指南 一个合格的个人网站站长应该知道哪些事? CSS 基础入门教程 —— 选择器、样式声明 HTML 基础入门 —— 基本结构和语法 给博客添加一个文章数据统计的页面 Git 学习笔记(命令备忘表) Anki 牌组选项详细解释与设置 手动编译带有 KernelSU 的 OnePlus 6 内核 安卓类原生系统补全计划 —— PixelExperience(一加 OnePlus) 如何使用 use-sound 为 React 应用添加声音效果 标准化项目的 GitHub Flow 工作流 如何搭建设计自己的博客网站? 使用安知鱼主题同款的 Twikoo 评论组件 配置 GitHub Actions 实现自动化提交百度收录 第00期 | 环境搭建 & 递归 (一) | 基本数列递归 逐字符讲解 C 语言 HelloWorld 程序 2023 年回顾 🤟 Anki:记忆神器,助你高效学习 Gentoo 学习笔记 美化你的 PixelExperience OS Markdown 语法指南 搭建 OneDrive 目录索引 为什么用 65 表示大写字母 A
手把手带你实现图结构(C/C++ 数据结构)
qlAD · 2026-02-10 · via qlAD 的技术笔记

cover

一、图的基本概念

1、有向图和无向图

图由顶点和连接顶点的边组成,根据其边是否有方向性分为两类:有向图和无向图。

有向图和无向图

左图中 V1 有一条指向 V2 的边,说明 V1 可以到达 V2,但 V2 无法到达 V1;

右图中 V1 和 V2 之间的边没有方向性,说明它们可以互相到达,在实现层面,可以把无向图模拟为双向的有向图。

2、连通图和非连通图

根据顶点之间的连通性,图又可以分为连通图和非连通图。

连通图和非连通图

左图中的每个顶点都可以通过边与其他顶点相连,说明这是一个连通图;

右图中的顶点被分成了两个部分,左边的顶点之间相互连通,右边也相互连通,但他们之间没有任何连接,说明这是一个非连通图。

如果图中并非任意两个顶点之间都存在路径,则该图为非连通图。

3、再看二叉树

二叉树也是一种特殊的图结构

二叉树在图论中的定义

二叉树是一种特殊的图结构:

  • 从图论角度看,若将二叉树视为无向图,它是「连通无环图」;
  • 从数据结构角度看,二叉树是「每个节点最多有两个子节点的有根有序树」,边具有方向性(父节点指向子节点);
  • 在无向图视角下,节点的度不超过 3(最多 2 个子节点边 + 1 个父节点边)。

这里引用 《hello 算法》 中的一张图片:

链表、树、图之间的关系

在树结构中,只允许父节点指向子节点,不存在子节点指向父节点的情况,子节点之间也不会互相连接;

而图中没有那么多限制,顶点之间可以相互指向,形成复杂的网络结构。

二、图的代码实现

对于链表和树结构,我们可以用节点(Node)和指针来实现;对于图结构,我们也可以用类似的方式来实现。

1、邻接链表(Adjacency List)

我们知道图是由顶点和边组成,每个顶点可以有多个边连接到其他顶点。

因此我们可以创建一个大小为 n 的数组来存放 n 个顶点的信息。

而每个顶点中包含着一条链表,链表中存储着与该顶点相连的所有顶点的信息。

这样我们就可以通过数组和链表的组合来实现图结构了,教材中叫作邻接表(Adjacency List,也称邻接链表)。

邻接链表

这里你有可能会问:链表的顺序会不会影响图的结构?

其实链表的顺序并不影响图的结构,因为图的结构是由顶点和边的连接关系(邻接关系)决定的,而不是由链表中元素的顺序决定的。

拿下面的有向图来说:

邻接链表示意图

所以不管是有向图还是无向图,我们都可以用邻接链表来实现。

代码实现:

就拿上面这个有向图来说,图中的顶点和边的关系如下:

顶点指向的顶点
顶点 1顶点 2、顶点 3 和 顶点 4
顶点 2顶点 4
顶点 3顶点 2 和 顶点 5
顶点 4顶点 3 和 顶点 5
顶点 5没有指向任何顶点

我们可以用一个数组来存储每个顶点的信息,每个元素是一个链表,链表中存储着与该顶点相连的所有顶点的信息。

#include <iostream>
#include <vector>
#include <list>

using namespace std;

class Graph {
private:
    vector<list<int>> adjList_; // 邻接链表
    int numVertices_; // 顶点数量
public:
    Graph(int numVertices) : numVertices_(numVertices) {
        adjList_.resize(numVertices); // 初始化邻接链表
    }

    // 添加边(from 指向 to)
    void addEdge(int from, int to) {
        // 校验顶点合法性(1 ~ numVertices_)
        if (from < 1 || from > numVertices_ || to < 1 || to > numVertices_) {
            cout << "顶点编号超出范围!" << endl;
            return;
        }
        adjList_[from - 1].push_back(to - 1); // 转成 0 开始的索引存储
    }

    // 打印图的邻接链表
    void printGraph() {
        for (int i = 0; i < numVertices_; ++i) {
            cout << "顶点 " << i + 1 << ":"; // 显示 1 开始的编号
            for (int neighbor: adjList_[i]) {
                cout << " " << neighbor + 1; // 邻接顶点也显示 1 开始
            }
            cout << endl;
        }
    }
};

int main() {
    Graph graph(5); // 创建一个有 5 个顶点的图(编号 1 ~ 5)

    /*
    | 顶点   | 指向的顶点               |
    | ----- | ----------------------- |
    | 顶点 1 | 顶点 2、顶点 3 和 顶点 4  |
    | 顶点 2 | 顶点 4                  |
    | 顶点 3 | 顶点 2 和 顶点 5         |
    | 顶点 4 | 顶点 3 和 顶点 5         |
    | 顶点 5 | 没有指向任何顶点          |
    */

    // 添加边(用 1 开始的编号)
    graph.addEdge(1, 2); // 顶点 1 指向 顶点 2
    graph.addEdge(1, 3); // 顶点 1 指向 顶点 3
    graph.addEdge(1, 4); // 顶点 1 指向 顶点 4
    graph.addEdge(2, 4); // 顶点 2 指向 顶点 4
    graph.addEdge(3, 2); // 顶点 3 指向 顶点 2
    graph.addEdge(3, 5); // 顶点 3 指向 顶点 5
    graph.addEdge(4, 3); // 顶点 4 指向 顶点 3
    graph.addEdge(4, 5); // 顶点 4 指向 顶点 5

    // 打印图的邻接链表
    graph.printGraph();

    return 0;
}

2、邻接矩阵(Adjacency Matrix)

这里你有可能会发现问题:对于有向图来说,顶点 1 指向顶点 2,因此我们只用操作顶点 1 的链表就可以了;

对于无向图来说,顶点 1 和顶点 2 之间是连接(双向)的关系,因此我们需要同时操作顶点 1 和顶点 2 的链表。

我们可以使用另外一种方式来实现图结构,那就是邻接矩阵。

邻接矩阵同样适用于有向图和无向图。对于无向图,由于边是双向的,其邻接矩阵一定是对称矩阵。

邻接矩阵

我们创建一个 n x n 的二维数组来存储图的信息,其中 n 是顶点的数量。

  • 行索引代表起点顶点
  • 列索引代表终点顶点

第 i 行 j 列的元素表示顶点 i 是否指向顶点 j,如果存在边则为 1,否则为 0。

比如说上图中,[0][0] 的元素为 0, 表示顶点 A 没有指向自己;

[3][2](第四行第三列)的元素为 1, 表示顶点 D 指向顶点 C,而 [2][3](第三行第四列)的元素为 1, 表示顶点 C 指向顶点 D。

对于无向图,邻接矩阵是对称的,因为如果顶点 i 和 j 之间有边,那么顶点 j 和 i 之间必然也有边(无向边的双向性)。

无向图邻接矩阵代码实现:

#include <iostream>
#include <vector>

using namespace std;

class Graph {
private:
    vector<vector<int>> adjMatrix_; // 邻接矩阵
    int numVertices_; // 顶点数量
public:
    Graph(int numVertices) : numVertices_(numVertices) {
        adjMatrix_.resize(numVertices, vector<int>(numVertices, 0)); // 初始化
    }

    // 为无向图添加边:顶点 u 和 v 之间建立连接
    void addEdge(int u, int v) {
        // 校验顶点合法性(1 ~ numVertices_)
        if (u < 1 || u > numVertices_ || v < 1 || v > numVertices_) {
            cout << "顶点编号超出范围!" << endl;
            return;
        }
        adjMatrix_[u - 1][v - 1] = 1; // 标记 u 到 v 有边
        adjMatrix_[v - 1][u - 1] = 1; // 标记 v 到 u 有边(无向图双向性)
    }

    // 打印图的邻接矩阵
    void printGraph() {
        cout << "  ";
        for (int i = 0; i < numVertices_; ++i) {
            cout << i + 1 << " "; // 显示 1 开始的编号
        }
        cout << endl;
        for (int i = 0; i < numVertices_; ++i) {
            cout << i + 1 << " "; // 显示 1 开始的编号
            for (int j = 0; j < numVertices_; ++j) {
                cout << adjMatrix_[i][j] << " ";
            }
            cout << endl;
        }
    }
};

int main() {
    Graph graph(5); // 创建一个有 5 个顶点的图(编号 1 ~ 5)

    /*
    邻接矩阵
      1 2 3 4 5
    1 0 1 1 1 0
    2 1 0 0 1 0
    3 1 0 0 0 1
    4 1 1 0 0 1
    5 0 0 1 1 0
    */

    // 添加边(用 1 开始的编号)
    graph.addEdge(1, 2); // 顶点 1 和 顶点 2 之间建立连接
    graph.addEdge(1, 3); // 顶点 1 和 顶点 3 之间建立连接
    graph.addEdge(1, 4); // 顶点 1 和 顶点 4 之间建立连接
    graph.addEdge(2, 4); // 顶点 2 和 顶点 4 之间建立连接
    graph.addEdge(3, 5); // 顶点 3 和 顶点 5 之间建立连接
    graph.addEdge(4, 5); // 顶点 4 和 顶点 5 之间建立连接

    // 打印图的邻接矩阵
    graph.printGraph();

    return 0;
}

3、效率对比

对于有 n 个顶点和 m 条边的图:

邻接链表邻接矩阵说明
添加边O(1)O(1)只需要在对应的位置添加边的信息
删除边O(n)O(1)邻接链表需要遍历链表找到边,邻接矩阵直接修改对应位置
查询边O(n)O(1)邻接链表需要遍历链表找到边,邻接矩阵直接查询对应位置
添加顶点O(1)O(n)邻接链表只需要添加一个新的链表,邻接矩阵需要扩展二维数组
删除顶点O(n + m)O(n^2)邻接链表需要删除对应的链表和相关边,邻接矩阵需要删除对应行列并调整矩阵大小
空间复杂度O(n + m)O(n^2)邻接链表只存储实际存在的边,邻接矩阵需要存储所有可能的边

三、图的遍历算法

1、深度优先搜索(DFS)

和二叉树的深度优先搜索一样,比如说前序遍历

        4
      /   \
     2     6
    / \   / \
   1   3 5   7

从根节点 4 开始,先访问当前节点,然后一直访问左子节点,直到访问到叶子节点时回溯到上一个节点,再访问右子节点,直到回溯到根节点为止。

对于图来说,没有根节点,并且顶点之间关系复杂,所以根据创建边的顺序不同,DFS 的访问顺序也会不同。

同时因为图中可能存在环,所以我们需要一个 visited 数组来记录已经访问过的顶点,避免重复访问。

// 以下函数属于 Graph 类,省略类定义,仅展示核心逻辑

void DFS(int vertex, vector<bool>& visited) {
    visited[vertex] = true; // 标记当前顶点为已访问
    cout << "访问顶点 " << vertex + 1 << endl; // 输出当前访问的顶点(显示 1 开始的编号)

    // 遍历与当前顶点相邻的所有顶点
    for (int neighbor: adjList_[vertex]) {
        if (!visited[neighbor]) { // 如果邻接顶点未被访问过
            DFS(neighbor, visited); // 递归访问邻接顶点
        }
    }
}

验证代码:

int main() {
    Graph graph(5); // 创建一个有 5 个顶点的图(编号 1 ~ 5)

    // 创建图的边(用 1 开始的编号)
    graph.addEdge(1, 2); // 顶点 1 指向 顶点 2
    graph.addEdge(1, 3); // 顶点 1 指向 顶点 3
    graph.addEdge(1, 4); // 顶点 1 指向 顶点 4
    graph.addEdge(2, 4); // 顶点 2 指向 顶点 4
    graph.addEdge(3, 2); // 顶点 3 指向 顶点 2
    graph.addEdge(3, 5); // 顶点 3 指向 顶点 5
    graph.addEdge(4, 3); // 顶点 4 指向 顶点 3
    graph.addEdge(4, 5); // 顶点 4 指向 顶点 5

    vector<bool> visited(5, false); // 初始化 visited 数组,默认所有顶点未访问
    cout << "深度优先搜索(DFS)访问顺序:" << endl;
    graph.DFS(0, visited); // 从顶点 1(索引 0)开始访问

    return 0;
}

最后输出的是:

深度优先搜索(DFS)访问顺序:
访问顶点 1
访问顶点 2
访问顶点 4
访问顶点 3
访问顶点 5

因为 DFS 的访问顺序取决于边的创建顺序(创建顺序影响邻接链表存储顺序,进而影响遍历顺序),

从上面创建图的边的代码来看,1 指向 2, 2 指向 4, 4 指向 3, 3 指向 5,所以遍历顺序就是 1 -> 2 -> 4 -> 3 -> 5。

2、广度优先搜索(BFS)

广度优先搜索是先访问当前顶点,然后访问所有与当前顶点相邻的顶点,再依次访问这些相邻顶点的相邻顶点,直到访问完所有顶点为止。

是一种由近及远的访问方式。

同样因为图中可能存在环,所以我们也需要一个 visited 数组来记录已经访问过的顶点,避免重复访问。

// 以下函数属于 Graph 类,省略类定义,仅展示核心逻辑

void BFS(int startVertex, vector<bool>& visited) {
    queue<int> q; // 创建一个队列来存储待访问的顶点
    visited[startVertex] = true; // 标记起始顶点为已访问
    q.push(startVertex); // 将起始顶点加入队列

    while (!q.empty()) {
        int vertex = q.front(); // 获取队列头部的顶点
        q.pop(); // 从队列中移除该顶点

        cout << "访问顶点 " << vertex + 1 << endl; // 输出当前访问的顶点(显示 1 开始的编号)

        // 遍历与当前顶点相邻的所有顶点
        for (int neighbor: adjList_[vertex]) {
            if (!visited[neighbor]) { // 如果邻接顶点未被访问过
                visited[neighbor] = true; // 标记邻接顶点为已访问
                q.push(neighbor); // 将邻接顶点加入队列
            }
        }
    }
}

看起来和 DFS 的代码很像,但它们的访问顺序是不同的。

int main() {
    Graph graph(5); // 创建一个有 5 个顶点的图(编号 1 ~ 5)

    // 创建图的边(用 1 开始的编号)
    graph.addEdge(1, 2); // 顶点 1 指向 顶点 2
    graph.addEdge(1, 3); // 顶点 1 指向 顶点 3
    graph.addEdge(1, 4); // 顶点 1 指向 顶点 4
    graph.addEdge(2, 4); // 顶点 2 指向 顶点 4
    graph.addEdge(3, 2); // 顶点 3 指向 顶点 2
    graph.addEdge(3, 5); // 顶点 3 指向 顶点 5
    graph.addEdge(4, 3); // 顶点 4 指向 顶点 3
    graph.addEdge(4, 5); // 顶点 4 指向 顶点 5

    vector<bool> visited(5, false); // 初始化 visited 数组,默认所有顶点未访问
    cout << "深度优先搜索(DFS)访问顺序:" << endl;
    graph.DFS(0, visited); // 从顶点 1(索引 0)开始访问

    fill(visited.begin(), visited.end(), false); // 重置 visited 数组,准备进行 BFS
    cout << "\n广度优先搜索(BFS)访问顺序:" << endl;
    graph.BFS(0, visited); // 从顶点 1(索引 0)开始访问

    return 0;
}

输出结果:

DFS 和 BFS 的访问顺序对比

DFS 的访问顺序受边的创建顺序影响;

BFS 保证按层级(距离)访问顶点,但在同一层内的访问顺序仍然取决于邻接链表中顶点的存储顺序

我们创建的边的顺序是:

1 指向 2, 1 指向 3, 1 指向 4, 2 指向 4, 3 指向 2, 3 指向 5, 4 指向 3, 4 指向 5。

步骤队列状态操作访问顺序
初始[1]标记 1 已访问,入队
1[2,3,4]1 出队,输出 1;邻居 2,3,4 未访问,标记并入队1
2[3,4]2 出队,输出 2;邻居 4 已访问(在队列中)1,2
3[4,5]3 出队,输出 3;邻居 2 已访问,5 未访问,标记并入队1,2,3
4[5]4 出队,输出 4;邻居 3,5 都已访问1,2,3,4
5[]5 出队,输出 5;无邻居1,2,3,4,5

因此 BFS 的访问顺序是 1 -> 2 -> 3 -> 4 -> 5。

3、应用场景

200. 岛屿数量 来说,

给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量。岛屿总是被水包围,并且每个岛屿只能由水平或垂直方向上相邻的陆地连接形成。

我们可以把这个二维网格看作一个图结构,其中每个陆地格子是一个顶点,如果两个陆地格子在水平或垂直方向上相邻,那么它们之间就有一条边。

DFS 解法(ACM 代码):

#include <bits/stdc++.h>
using namespace std;

const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};

vector<vector<char>> grid;

int n, m; // 行数 n,列数 m

void dfs(int x, int y) {
    // 越界 或 不是陆地,直接返回
    if (x < 0 || x >= n || y < 0 || y >= m || grid[x][y] != '1') {
        return;
    }

    grid[x][y] = '0'; // 标记为已访问

    // 递归遍历四个方向
    for (int i = 0; i < 4; i++) {
        int nx = x + dx[i];
        int ny = y + dy[i];
        dfs(nx, ny);
    }
}

int main() {
    // 先读行数 n,列数 m,再读 n 行字符串(每行 m 个字符)
    cin >> n >> m;
    grid.resize(n, vector<char>(m));

    for (int i = 0; i < n; i++) {
        string s;
        cin >> s;
        for (int j = 0; j < m; j++) {
            grid[i][j] = s[j];
        }
    }


    int count = 0;

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (grid[i][j] == '1') {
                count++;
                // 从当前陆地格子开始,DFS 将整个岛屿的陆地格子都标记为 '0',避免重复计数
                dfs(i, j);
            }
        }
    }

    cout << count << endl;

    return 0;
}

BFS 解法(ACM 代码):

#include <bits/stdc++.h>
using namespace std;

const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};

vector<vector<char>> grid;
int n, m; // 行数 n,列数 m

void bfs(int x, int y) {
    queue<pair<int, int>> q;
    q.push({x, y});
    grid[x][y] = '0'; // 标记为已访问

    while (!q.empty()) {
        pair<int, int> curr = q.front();
        int cx = curr.first;
        int cy = curr.second;
        q.pop();

        // 遍历四个方向
        for (int i = 0; i < 4; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];

            // 越界 或 不是陆地,跳过
            if (nx < 0 || nx >= n || ny < 0 || ny >= m || grid[nx][ny] != '1') {
                continue;
            }

            grid[nx][ny] = '0'; // 标记为已访问
            q.push({nx, ny}); // 将相邻的陆地格子加入队列
        }
    }
}

int main() {
    // 先读行数 n,列数 m,再读 n 行字符串(每行 m 个字符)
    cin >> n >> m;
    grid.resize(n, vector<char>(m));

    for (int i = 0; i < n; i++) {
        string s;
        cin >> s;
        for (int j = 0; j < m; j++) {
            grid[i][j] = s[j];
        }
    }

    int count = 0;

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (grid[i][j] == '1') {
                count++;
                // 从当前陆地格子开始,BFS 将整个岛屿的陆地格子都标记为 '0',避免重复计数
                bfs(i, j);
            }
        }
    }

    cout << count << endl;

    return 0;
}

两种解法的复杂度分析:

DFSBFS说明
时间复杂度O(n × m)O(n × m)每个格子最多被访问一次
空间复杂度O(n × m)O(n × m)BFS 队列在二维网格中的峰值为 O(min(n, m))