图论专题(一):Hello, Graph! 掌握“建图”与“遍历”的灵魂
哈喽各位,我是前端小L。
欢迎来到我们的新专题——“图论”!准备好,我们的思维即将从“一条线”升维到一张“网”。
今天,我们将从图论最基础、最核心的问题开始:A点和B点,是连通的吗? 这个问题看似简单,却蕴含了后续所有复杂图算法的基础。我们将用它来““磨刀”,彻底掌握图的表示和遍历。
力扣 1971. 寻找图中是否存在路径
https://leetcode.cn/problems/find-if-path-exists-in-graph/

题目分析:
-
输入:
n(顶点数, 0到n-1),edges(一个[u, v]列表),source(起点),destination(终点)。 -
图的类型:无向图。
[u, v]意味着u和v之间有一条双向的路。 -
目标:判断从
source出发,能否“走”到destination。
第一关:如何把“边”建成“地图”?—— 邻接表
计算机并不“认识” [[0, 1], [1, 2], [0, 2]] 这种“边列表”。它太慢了,要找 0 的邻居,我们得遍历整个列表。 我们需要一种更高效的“地图”——邻接表 (Adjacency List)。
邻接表在 C++ 中,就是一个“向量的向量”:vector<vector<int>> graph(n);
-
graph是一个大vector,长度为n。 -
graph[i]是一个小vector,里面存储了所有与i直接相连的邻居。
建图过程 (O(E)): 我们遍历 edges 列表(E 是边的数量),对于每一条边 [u, v]:
-
graph[u].push_back(v);(添加一条u->v的路) -
graph[v].push_back(u);(因为是无向图,还要添加v->u的路)
C++
// 1. 建图 (邻接表)
vector<vector<int>> graph(n);
for (const auto& edge : edges) {int u = edge[0];int v = edge[1];graph[u].push_back(v);graph[v].push_back(u); // 无向图,双向添加
}
第二关:如何“走”这张地图?—— DFS 与 BFS
有了地图,我们就可以从 source 出发“探险”了。有两种经典的探险策略:
-
DFS (深度优先搜索):像在走迷宫,“一条路走到黑”。
-
策略:从
source出发,访问它的第一个邻居v1,然后再访问v1的第一个邻居v2... 直到走到“死胡同”,再回溯(返回上一层),去探索v1的第二个邻居。 -
实现:通常用递归。
-
-
BFS (广度优先搜索):像“水波纹”一样,“一层一层地向外扩散”。
-
策略:从
source出发(第0层),访问它所有“一度人脉”(邻居,第1层),然后再访问所有“二度人脉”(邻居的邻居,第2层)... -
实现:通常用队列 (Queue)。
-
“Aha!”时刻:防止“兜圈”的灵魂—— visited 数组
无论DFS还是BFS,我们都会遇到一个致命问题: [0, 1] 这条边,graph[0] 里有 1,graph[1] 里有 0。
-
DFS:
dfs(0)-> 看到邻居1-> 调用dfs(1)。 -
dfs(1)-> 看到邻居0-> 调用dfs(0)。 -
dfs(0)-> 看到邻居1-> 调用dfs(1)。 -
... 栈溢出 (Stack Overflow)!
解决方案:我们需要一个“足迹”数组,走过的地方就不要再走了! vector<bool> visited(n, false);
遍历的“铁律”:
-
(BFS):当一个节点入队 (push) 时,必须立刻标记
visited[node] = true。 -
(DFS):在
dfs(node)函数的第一行,必须立刻标记visited[node] = true。
代码实现 (O(V+E) 时间, O(V+E) 空间)
我们提供两种解法,它们的核心思想都是“遍历”。
解法一:DFS (递归)
class Solution {
public:bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {// 1. 建图 (邻接表)vector<vector<int>> graph(n);for (const auto& edge : edges) {graph[edge[0]].push_back(edge[1]);graph[edge[1]].push_back(edge[0]);}// 2. “灵魂”:visited 数组vector<bool> visited(n, false);// 3. 启动 DFSdfs(source, graph, visited);// 4. 检查终点是否被访问到return visited[destination];}void dfs(int u, vector<vector<int>>& graph, vector<bool>& visited) {// “铁律”:立刻标记visited[u] = true;// 探索所有邻居for (int v : graph[u]) {if (!visited[v]) {dfs(v, graph, visited);}}}
};
解法二:BFS (队列)
C++
#include <queue> // 引入队列class Solution {
public:bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {// 1. 建图vector<vector<int>> graph(n);for (const auto& edge : edges) {graph[edge[0]].push_back(edge[1]);graph[edge[1]].push_back(edge[0]);}vector<bool> visited(n, false);queue<int> q;// 3. 启动 BFSq.push(source);visited[source] = true; // “铁律”:入队时立刻标记while (!q.empty()) {int u = q.front();q.pop();if (u == destination) {return true; // 可以在这里提前退出}// 探索所有邻居for (int v : graph[u]) {if (!visited[v]) {visited[v] = true; // “铁律”:入队时立刻标记q.push(v);}}}// 4. 检查终点是否被访问到return visited[destination];}
};
深度复杂度分析
-
V (Vertices):顶点数,即
n。 -
E (Edges):边数,即
edges.size()。 -
时间复杂度 O(V + E):
-
建图:需要 O(E) 时间,因为我们遍历了
edges数组一次。 -
遍历 (DFS/BFS):我们需要访问每个顶点
V最多一次(因为visited数组的保护)。在访问每个顶点u时,我们会遍历它的所有邻居,这相当于遍历了它的所有“出边”。对于整个图,所有“出边”的总和,在无向图中等于2 * E。 -
总时间 = O(E) + O(V + 2E) = O(V + E)。
-
-
空间复杂度 O(V + E):
-
邻接表
graph:需要存储所有的边,总空间是 O(V + E)。 -
visited数组:需要 O(V) 空间。 -
辅助空间:DFS 需要 O(V) 的递归栈空间(最坏情况,如一条长链);BFS 需要 O(V) 的队列空间(最坏情况,如一个“星型图”)。
-
总空间 = O(V + E) + O(V) = O(V + E)。
-
总结
今天,我们为“图论”专题打下了最坚实的地基。我们学会了:
-
用邻接表
vector<vector<int>>来“建图”。 -
图的遍历有 DFS(递归)和 BFS(队列)两种核心方式。
-
visited数组是防止无限循环的“灵魂”。
在下一篇中,我们将把今天学到的 DFS/BFS,应用到最常见的“隐式图”——二维网格(“岛屿问题”)上!
下期见!
