图的遍历:DFS、BFS 与连通分量

学完图的基本概念和存储方式之后,第一件真正要做的事,通常就是遍历。因为一张图只靠“画出来”是没有意义的,真正要解决问题时,你总要从某个顶点出发,沿着边把可达区域探索出来。图的遍历,正是在回答这样一个问题:

如何系统地访问图中的顶点,使得每个相关顶点都被按某种规则走到?

和树遍历相比,图遍历更复杂。因为图可能:

  • 有环;
  • 不连通;
  • 存在多条不同路径;
  • 在有向图中还可能出现单向可达。

这就意味着,图遍历不能像树那样只靠递归定义自然展开,还必须考虑“是否访问过”的状态管理。也正因为此,DFS 和 BFS 不只是两种遍历方式,它们几乎是整个图算法体系的起点。很多图问题表面不同,本质上都离不开这两种搜索框架。

一、为什么图遍历比树遍历更复杂

树里最好的性质之一是:

  • 通常没有环;
  • 从根出发不会绕回来;
  • 父子关系比较明确。

但图中情况完全不同:

  • 走过的点可能再次通过别的边走回来;
  • 一条边可能把你带回之前访问过的区域;
  • 甚至整张图可能由多个互不连通部分组成。

所以图遍历的第一原则就是:

必须记录哪些顶点已经访问过,防止无限重复。

这也是 visited 标记在图遍历里如此关键的原因。

二、什么是深度优先搜索 DFS

DFS,英文是 Depth First Search,可以理解成:

从当前顶点出发,优先沿着一条路径一直走到尽头,走不动了再回退,继续探索其他分支。

这和前面树里的递归遍历有很强的亲缘关系。它的味道通常是:

  • 先尽可能深入;
  • 再逐层回溯;
  • 本质上是一种“路径优先”的搜索方式。

三、DFS 为什么和栈紧密相关

因为 DFS 的核心行为就是:

  • 沿着当前路径不断深入;
  • 走不通时回到最近的分叉点;
  • 再尝试新的分支。

这里“最近分叉点优先回退”本质上就是后进先出,因此:

  • 递归版 DFS 依赖系统调用栈;
  • 非递归版 DFS 通常显式使用栈。

所以你可以把 DFS 看成“栈思想在图上的应用”。

四、什么是广度优先搜索 BFS

BFS,英文是 Breadth First Search,可以理解成:

从起点出发,先访问所有距离为 1 的顶点,再访问距离为 2 的顶点,然后是距离为 3 的顶点,按层次一圈圈向外扩展。

它和 DFS 完全不同,不强调先走深,而强调“先近后远”。

所以 BFS 的典型特征是:

  • 很像波纹向外扩散;
  • 更关注层次和最短步数;
  • 本质上是一种“按层推进”的搜索。

五、BFS 为什么和队列紧密相关

因为 BFS 的访问规律是:

  • 先发现的顶点,应该先展开它的邻居;
  • 后发现的顶点排在后面等待。

这正是先进先出的过程。所以 BFS 几乎天然和队列绑定:

  • 起点先入队;
  • 每次出队一个顶点并访问;
  • 再把它尚未访问过的邻居入队;
  • 重复直到队列为空。

所以也可以说,BFS 是“队列思想在图上的应用”。

六、DFS 和 BFS 最核心的差异是什么

可以先抓住一句最本质的话:

DFS 优先把一条路走深,BFS 优先把同一层走完。

于是它们的适用味道也会很不同。

1. DFS 更适合
  • 看路径结构;
  • 做回溯;
  • 找连通块;
  • 判断是否存在某种深层结构;
  • 做拓扑、强连通等更深层图分析的基础。
2. BFS 更适合
  • 求无权图最短路;
  • 按层推进;
  • 最近层、最少步数问题;
  • 波次扩散类问题。

七、图遍历为什么通常要外层再套一层循环

因为图可能不连通。

如果你只从某一个起点做 DFS 或 BFS,那么你只能访问到和它同一个连通块里的顶点。那些和它完全不相连的部分,根本不会被走到。

所以如果题目目标是“遍历整张图”或“统计图中有多少个连通分量”,典型做法通常是:

  • 枚举所有顶点;
  • 遇到还没访问过的顶点,就从它重新启动一次 DFS / BFS;
  • 每启动一次,通常就意味着发现了一个新的连通分量。

这一步非常关键。

八、什么是连通分量

在无向图中,连通分量可以理解成:

图中一个极大连通子图,其中任意两个顶点彼此可达。

换句话说:

  • 同一分量里的点能互相到达;
  • 不同分量之间没有路径相通。

所以统计连通分量,本质上就是:

  • 看整张图被分成了多少块互相独立的区域。

而 DFS / BFS 恰好非常适合做这件事。

九、DFS / BFS 如何求连通分量

基本思路都一样:

  1. 先把所有顶点标记为未访问;
  2. 从第一个未访问顶点开始做一次搜索;
  3. 这一轮能访问到的所有顶点,属于同一个连通分量;
  4. 再继续找下一个未访问顶点,开启下一轮搜索;
  5. 重复直到全部访问完。

于是:

  • 搜索启动次数 = 连通分量个数。

这说明连通分量并不是额外的新算法,而是 DFS / BFS 的一个非常直接的应用。

十、BFS 为什么能求无权图最短路

这是 BFS 最经典的应用之一。

在无权图里,每条边代价都相同,可以看成一步。BFS 从起点出发时:

  • 第一层是距离 1 的点;
  • 第二层是距离 2 的点;
  • 第三层是距离 3 的点。

因为它按层推进,所以第一次到达某个顶点时,走到它的边数一定是最少的。

也就是说:

BFS 的层数,本身就是无权图中的最短步数。

这也是为什么很多“最少操作次数”“最短变换步数”类问题,第一反应常常应该是 BFS。

十一、DFS 更适合哪些“结构性问题”

DFS 的强项往往不是最短路,而是更偏结构探索和递归分析。

例如:

  • 判断图中是否有环;
  • 找所有可达点;
  • 枚举路径;
  • 做回溯搜索;
  • 后面做拓扑排序、强连通分量等。

因为 DFS 能天然保留“当前路径”的语境,所以对很多需要递归展开和回退的问题特别合适。

十二、图遍历的复杂度怎么理解

如果图用邻接表表示,那么 DFS 和 BFS 的典型复杂度通常是:

1
O(|V| + |E|)

原因是:

  • 每个顶点通常只会入栈 / 入队 / 访问一次;
  • 每条边也只会被检查有限次。

如果用邻接矩阵表示,由于遍历某个顶点邻居需要扫整行,复杂度通常更接近 O(|V|^2)

这也再次说明:图算法的效率和存储方式关系非常紧密。

十三、学习图遍历时最容易踩的坑

1. 忘记 visited 标记

这是最常见、也最致命的问题,尤其在有环图中会直接导致死循环。

2. 只会从一个起点遍历,不考虑图不连通

这会漏掉整张图中的其他分量。

3. 把 DFS 和 BFS 用反

例如本来是最短步数问题,却上来就写 DFS,结果又慢又绕。

4. 不结合存储方式分析复杂度

邻接矩阵和邻接表下,遍历成本差别会很明显。

总结

图的遍历之所以重要,不只是因为它是图论入门,而是因为很多图问题本质上都只是 DFS / BFS 的不同变体。真正值得先建立起来的,是这些核心认识:

  • 图遍历比树遍历更复杂,因为图可能有环、可能不连通;
  • DFS 强调先深入再回退,本质上依赖栈;
  • BFS 强调按层推进,本质上依赖队列;
  • BFS 很适合无权图最短步数问题,DFS 更适合结构探索和回溯;
  • 连通分量统计本质上就是多次启动 DFS / BFS;
  • 真正学会图遍历,不是只会写两段模板,而是能根据题目味道判断应该“先走深”还是“先走广”。

把这一篇理解透之后,后面再去学拓扑排序、最短路径、最小生成树时,你会更自然地把它们看成是在 DFS / BFS 基础上叠加约束和优化,而不是完全陌生的新东西。

参考资源