图论基础

图遍历的完整指南:BFS vs. DFS

掌握计算机科学的核心算法。了解广度优先搜索 (BFS) 和深度优先搜索 (DFS) 的内部工作原理、何时策略性地使用它们以及它们在现代软件工程中的重要实际应用。

阅读需要 15 分钟 更新时间:2026年6月 高级水平
LGT
Learn Graph Theory Team
Expert Operations Research Engineers

图遍历是几乎所有图论算法的基础机制。它是系统地访问、检查、分析以及经常更新图中每个顶点(节点)的过程。无论你是在谷歌地图中寻找最短路径、在社交网络上推荐好友、为搜索引擎抓取互联网内容,还是通过算法解决复杂的谜题,你都在使用图遍历。

这一领域的核心是两个传奇算法:广度优先搜索 (Breadth-First Search, BFS)深度优先搜索 (Depth-First Search, DFS)。虽然它们都保证将完全访问连通分量中每个可达的节点一次,但它们访问节点的顺序和策略导致了截然不同的应用、时间/空间复杂度以及行为模式。

在这篇综合指南中,我们将剖析这两种算法,检查它们的数据结构,审查实现代码,比较它们的复杂度,最后查看真实场景,帮助你有信心地回答那个终极问题:"我什么时候应该使用 BFS,什么时候应该使用 DFS?"

1. 深入广度优先搜索 (BFS)

广度优先搜索以同心圆的"波纹"方式探索图。它从选定的根节点(或任何起始节点)开始,首先访问其所有直接相邻的邻居。只有在完全穷尽距离正好为 1 条边的所有节点之后,它才会移动到距离为 2 的节点,依此类推。

水滴类比: 将 BFS 想象成水滴在平静的池塘中产生涟漪。涟漪在所有方向上均匀向外扩展。它不会沿直线射出;它按半径均匀地覆盖区域。

BFS 的机制

为了确保严格按层访问节点,BFS 依赖于一种 队列 (Queue)(先进先出,即 FIFO)数据结构来记录接下来应该访问哪些节点。

标准 BFS 算法遵循以下步骤:

  1. 初始化一个队列和一个布尔数组(或 Hash Set)以跟踪 已访问 的节点。
  2. 将起始节点排入队列并立即将其标记为 已访问
  3. 开始一个循环,只要队列不为空就继续。
  4. 从队列前端取出节点。这是你当前的节点。按你希望的方式处理它。
  5. 遍历此当前节点的每个相邻邻居。
  6. 如果邻居尚未被访问,则将其标记为 已访问 并将其排入队列。
  7. 重复直到队列为空。

BFS 实现示例

下面是一个使用邻接表表示法和高效的 collections.deque 在 Python 中清晰实现 BFS 的示例。

from collections import deque

def breadth_first_search(graph, start_node):
    # 初始化一个队列和一个集合用于存放已访问节点
    queue = deque([start_node])
    visited = {start_node}
    
    # 存储遍历顺序
    traversal_order = []

    while queue:
        # 从队列前端取出节点
        current_node = queue.popleft()
        traversal_order.append(current_node)

        # 获取出队节点的所有相邻节点
        for neighbor in graph[current_node]:
            if neighbor not in visited:
                # 立即在入队之前标记为已访问
                # 避免队列中出现重复条目
                visited.add(neighbor)
                queue.append(neighbor)
                
    return traversal_order

# 示例图 (邻接表)
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}

print(breadth_first_search(graph, 'A'))
# 输出: ['A', 'B', 'C', 'D', 'E', 'F']

停止阅读,开始可视化

阅读代码很好,但观察算法实时执行可以建立真正的直觉。观看队列如何在我们的平台上管理 BFS 波动。

启动交互式 BFS 可视化工具

2. 深入深度优先搜索 (DFS)

深度优先搜索采取了截然不同且更具攻击性的方法。它不是广度优先地探索,而是深度优先地探索。它从根节点开始,尽可能沿着单一分支探索,直到遇到"死胡同"(一个没有未访问邻居的节点)。一旦卡住,它就会在路径上 回溯 (backtrack) 足够高,找到一个新的未探索分支,顺着探索下去。

迷宫类比: 将 DFS 想象成在物理迷宫中导航。你把右手放在右墙上,一直向前走。你拐过一个又一个弯,直到遇到死胡同。此时,你转身,走回上一个十字路口,选择那条未探索的道路。

DFS 的机制

因为 DFS 需要记住它来自哪里以便能够回溯,所以它使用一种 栈 (Stack)(后进先出,即 LIFO)数据结构。通常,开发人员会递归地实现 DFS,优雅地使用原生 CPU 调用栈,而不是手动创建 Stack 对象。

标准递归 DFS 算法遵循以下步骤:

  1. 从当前节点开始并立即将其标记为 已访问
  2. 处理节点(例如打印其值)。
  3. 遍历它的所有相邻邻居。
  4. 如果某个邻居未被访问,就在该邻居上递归调用 DFS 函数。
  5. 当循环结束(没有未访问的邻居留下),函数返回,自动回溯到调用栈中前一个调用者。

DFS 实现示例

下面是一个使用 Python 的优雅的递归 DFS 实现。

def depth_first_search_recursive(graph, current_node, visited=None, traversal_order=None):
    # 初始化集合和列表
    if visited is None:
        visited = set()
    if traversal_order is None:
        traversal_order = []

    # 将当前节点标记为已访问并记录
    visited.add(current_node)
    traversal_order.append(current_node)

    # 递归访问所有未访问的邻居
    for neighbor in graph[current_node]:
        if neighbor not in visited:
            depth_first_search_recursive(graph, neighbor, visited, traversal_order)
            
    return traversal_order

# 示例图 (邻接表)
graph = {
    'A': ['B', 'C'],
    'B': ['A', 'D', 'E'],
    'C': ['A', 'F'],
    'D': ['B'],
    'E': ['B', 'F'],
    'F': ['C', 'E']
}

print(depth_first_search_recursive(graph, 'A'))
# 输出: ['A', 'B', 'D', 'E', 'F', 'C']

注意输出结果与 BFS 的不同。DFS 积极追随从 A 到 B 到 D 的路径,然后才开始回溯。

可视化回溯的魔力

递归调用栈可能很难在脑海中映射。直观地观察 DFS 卡住时如何深入潜行并在边缘上回溯。

启动交互式 DFS 可视化工具

3. BFS vs. DFS:终极比较

在技术面试和实际系统设计中,选择错误的遍历算法可能会导致灾难性的内存耗尽或极其低效的执行时间。让我们分解技术差异。

指标 广度优先搜索 (BFS) 深度优先搜索 (DFS)
数据结构 队列 (FIFO) - 迭代方法 栈 (LIFO) - 通常是递归方法
时间复杂度 O(V + E)
必须访问每个顶点并检查每条边。
O(V + E)
同样访问每个顶点并探索每条边。
空间复杂度 O(W) 其中 W 是图的最大宽度
在最坏情况下密集图中可能为 O(V)
O(H) 其中 H 是图的最大深度/高度
如果图是一条长直线,可能为 O(V)
最短路径保证 是。 对于无权图,BFS 保证能找到最短路径。 否。 DFS 可能会在找到直达的短路径之前找到一条巨大而曲折的路径。
内存开销 如果树/图非常宽(例如社交网络),则开销很高。队列必须容纳当前层的所有节点。 如果树/图非常深,则开销很高。调用栈必须容纳当前节点的所有祖先。
最优性 最适合寻找靠近源的目标。 最适合寻找远离源的目标或探索所有可能性。

理解空间复杂度的权衡

虽然这两种算法的共同时间复杂度都是 O(V + E),但它们的空间复杂度根据图的拓扑结构有很大差异。

4. 实际应用:何时使用哪个?

BFS 和 DFS 之间的选择通常归结为你试图从图结构中提取什么。

使用广度优先搜索 (BFS) 的情况:

使用深度优先搜索 (DFS) 的情况:

5. 高级变体

在现代计算中,基础的 BFS 和 DFS 通常会被增强以处理大规模数据。

结论

广度优先搜索和深度优先搜索是图论无可争议的基石。BFS 是你逐层扩散的波动,非常适合最短路径和广阔的网络分析。DFS 是你高效利用内存、极具攻击性的迷宫跑者,非常适合检测循环、拓扑排序和深度解谜。

掌握这两种算法对于通过顶级科技公司的技术面试并设计强大、可扩展的软件架构是不可协商的。

常见问题

我应该在什么时候选择广度优先搜索 (BFS) 而不是深度优先 (DFS)?

在无权图中寻找最短路径,或者寻找距离起点较近的目标节点时,首选广度优先搜索 (BFS)。

DFS 相比 BFS 在哪些场景下更有优势?

深度优先搜索 (DFS) 更适合探索所有可能路径(如迷宫求解)、拓扑排序、环检测,或者当内存受限时,因为 DFS 在深层图中的内存开销通常较小。

BFS 和 DFS 使用的数据结构有什么不同?

BFS 使用队列(Queue,先进先出)来进行按层遍历;DFS 使用栈(Stack,后进先出),在代码实现中通常以递归的方式隐式表达。

资料来源和延伸阅读

准备好掌握图算法了吗?

不要再阅读静态文本了。通过我们完全免费的交互式算法可视化工具,在实践中学习。逐个节点地浏览 BFS、DFS、Dijkstra 以及其他 24 种算法。

立即打开免费可视化工具