广度优先遍历与最短路径:从零理解图算法的核心逻辑
你有没有想过,在迷宫中找出口,或者在社交网络中找“最近”的朋友,背后到底用了什么算法?其实,这些看似复杂的场景,都可以用一种高效而优雅的搜索策略来解决——广度优先遍历与最短路径。它不只在算法题中常见,更是实际项目中路径规划、网络路由、推荐系统的重要基石。
如果你刚接触图结构,或者对“最短路径”感到困惑,别担心。这篇文章会带你从零开始,一步步理解广度优先遍历(BFS)是如何找到最短路径的,同时通过真实代码示例,让你真正“看懂”并“用上”它。
什么是图?为什么需要广度优先遍历?
在计算机世界里,图是一种用来表示“关系”的数据结构。比如,城市之间的公路网络、微信好友之间的关注关系、网页之间的超链接,都可以抽象成图。
图由“顶点”(Vertex)和“边”(Edge)组成。顶点代表实体,边代表实体之间的连接。比如 A 城市和 B 城市之间有公路,就有一条从 A 到 B 的边。
在图中,常见的遍历方式有两种:广度优先遍历(BFS)和深度优先遍历(DFS)。我们今天聚焦的是 BFS。
想象你在参加一场“寻宝游戏”,起点是家,终点是宝藏屋。你有三种方式去探索:
- 深度优先:一口气往一条路走到底,万一走错了,回头还得重来;
- 广度优先:先看家周围 100 米内的所有房子,再看 200 米内的,一层一层往外扩散。
显然,BFS 更适合找“最近”的宝藏,因为它按“距离”一层层探索,最先到达目标的路径,就是最短路径。
广度优先遍历的核心思想与执行流程
广度优先遍历的关键在于“层序访问”:从起点出发,先访问所有一步可达的点,再访问所有两步可达的点,依此类推。
我们用一个简单的例子说明:
假设有一个图,顶点是 0 到 5,连接关系如下:
0 — 1 — 2
| | |
3 — 4 — 5
起点是 0,我们想用 BFS 找到所有点的访问顺序。
实现的核心是使用一个队列(Queue),先进先出(FIFO)的结构。想象你排队买奶茶,谁先来谁先买。
Python 实现代码示例
from collections import deque
def bfs_traversal(graph, start):
# visited 用于记录已访问的顶点,避免重复访问
visited = set()
# queue 用于存储待访问的顶点,初始加入起点
queue = deque([start])
# 用于记录遍历顺序
traversal_order = []
while queue:
# 从队列头部取出一个顶点
current = queue.popleft()
# 如果该顶点已访问,跳过
if current in visited:
continue
# 标记为已访问,并加入遍历顺序
visited.add(current)
traversal_order.append(current)
# 将当前顶点的所有未访问邻居加入队列
for neighbor in graph[current]:
if neighbor not in visited:
queue.append(neighbor)
return traversal_order
graph = {
0: [1, 3],
1: [0, 2, 4],
2: [1, 4, 5],
3: [0, 4],
4: [1, 2, 3, 5],
5: [2, 4]
}
result = bfs_traversal(graph, 0)
print("广度优先遍历结果:", result)
输出结果:
广度优先遍历结果: [0, 1, 3, 2, 4, 5]
代码逐行解释:
deque([start]):创建一个双端队列,把起点加入其中。queue.popleft():每次从队列前端取出一个点,保证“先来先处理”。visited.add(current):防止重复访问,是 BFS 的关键。for neighbor in graph[current]:遍历当前点的所有邻居。if neighbor not in visited:只有未访问的邻居才加入队列,避免死循环。
这个过程就像水波一圈圈扩散,每一轮扩散的范围就是“距离起点的步数”。
广度优先遍历与最短路径的内在联系
在无权图中(即每条边的权重都为 1),广度优先遍历天然能找出从起点到任意点的最短路径。
为什么?因为 BFS 是按“步数”一层层扩展的。当你第一次到达某个点时,那一定是通过最少步数到达的。
比如上面的例子,从 0 到 5:
- 0 → 1 → 4 → 5(3 步)
- 0 → 3 → 4 → 5(3 步)
- 0 → 1 → 2 → 5(3 步)
BFS 会先访问距离为 1 的点(1, 3),再访问距离为 2 的点(2, 4),最后访问距离为 3 的点(5)。所以当 5 第一次被访问时,就是最短路径。
如何记录最短路径?从遍历中还原路径
我们不仅想知道“最短距离”,还想知道“怎么走的”。这就需要记录“前驱节点”(parent)。
Python 实现:记录路径的 BFS
from collections import deque
def bfs_shortest_path(graph, start, target):
# 记录每个点的前驱节点,用于回溯路径
parent = {}
# 记录已访问节点
visited = set()
# 队列存储待访问节点
queue = deque([start])
visited.add(start)
while queue:
current = queue.popleft()
# 如果找到目标,停止搜索
if current == target:
break
# 遍历所有邻居
for neighbor in graph[current]:
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current # 记录前驱
queue.append(neighbor)
# 回溯路径
path = []
node = target
while node is not None:
path.append(node)
node = parent.get(node) # 获取前驱
# 反转路径,从起点到终点
path.reverse()
# 如果路径长度为1,说明起点就是终点
if path[0] == start and len(path) == 1:
return [start]
return path if path[0] == start else None
graph = {
0: [1, 3],
1: [0, 2, 4],
2: [1, 4, 5],
3: [0, 4],
4: [1, 2, 3, 5],
5: [2, 4]
}
result = bfs_shortest_path(graph, 0, 5)
print("从 0 到 5 的最短路径:", result)
输出结果:
从 0 到 5 的最短路径: [0, 1, 4, 5]
核心逻辑说明:
parent[neighbor] = current:记录谁把我们“带到”了邻居。- 回溯时从目标出发,不断找前驱,直到起点。
- 最后反转,得到从起点到终点的路径。
这个技巧在导航系统、游戏 AI、网络路由中非常实用。
实际应用场景:迷宫求解与社交推荐
场景一:迷宫最短路径
假设你有一个 5x5 的迷宫,用 0 表示通路,1 表示墙。你从左上角 (0,0) 出发,想走到右下角 (4,4)。
BFS 可以帮你找到最短路径。只需将每个格子看作一个顶点,上下左右相邻且为通路的格子之间连边。
场景二:社交网络中的“三度好友”
在微信中,A 想认识 C,但 C 不在好友列表。如果 A 和 B 是好友,B 和 C 是好友,那么 A 可以通过 B 认识 C。这本质上就是“距离为 2”的最短路径问题。
BFS 可以快速找出 A 的所有“两度以内”的联系人,实现“推荐好友”功能。
总结:掌握广度优先遍历与最短路径的关键点
广度优先遍历与最短路径,是图算法中最具实用价值的组合之一。它简单、高效,且在无权图中能保证找到最短路径。
- BFS 按层扩展,天然适合找最短路径;
- 使用队列实现,保证“先来先处理”;
- 通过记录前驱节点,可还原路径;
- 适用于迷宫、导航、社交推荐、网络路由等场景。
掌握它,你不仅能解决 LeetCode 的 BFS 题,更能理解真实世界中许多系统的底层逻辑。
如果你现在还觉得“图”很抽象,不妨从一个简单的邻接表开始,手写一次 BFS,再尝试找最短路径。你会发现,原来算法也可以这么“有温度”。
常见问题与注意事项
| 问题 | 解答 |
|---|---|
| BFS 适合有权图吗? | 不适合。有权图需用 Dijkstra 算法,但 BFS 只适用于边权为 1 的图。 |
| 为什么用队列不用栈? | 队列保证层序访问,栈是深度优先,不符合 BFS 逻辑。 |
| 如何避免重复访问? | 使用 visited 集合,是防止死循环的关键。 |
| 时间复杂度是多少? | O(V + E),V 是顶点数,E 是边数,非常高效。 |
广度优先遍历与最短路径,不是死板的算法,而是一种思维方式:按距离一层层探索,不走回头路,优先找最近的解。当你在项目中遇到“最快路径”“最近关系”这类问题时,不妨先问问自己:能不能用 BFS 来解决?答案很可能是“能”。