《算法图解》第六章为我们介绍了一种基础且强大的图搜索算法——**广度优先搜索 (Breadth-First Search, BFS)**。这种算法能够系统地探索图中的节点,常用于解决两类核心问题:一是判断从一个节点到另一个节点是否存在路径;二是在无权图中找到两个节点之间的最短路径。本笔记将深入探讨图的基本概念、BFS 的工作原理、其实现方式以及相关的性能分析。
在讨论 BFS 之前,我们需要理解什么是图。
图是一种由节点 (Nodes 或 Vertices) 和连接这些节点的边 (Edges) 组成的数据结构。图用于表示各种实体之间的关系。
graph LR
A --|> B;
B --|> C;
A --|> C;
end
graph TD
A --- B;
B --- C;
A --- C;
end
一个节点的邻居是指所有与该节点直接通过一条边相连的节点。
广度优先搜索 (BFS) 是一种用于遍历或搜索树或图数据结构的算法。它从图的某个起始节点(源节点)开始,探索其所有邻近节点,然后再逐层向外探索这些邻近节点的邻近节点,依此类推。
BFS 算法的核心思想是"逐层扩展"。
这种探索方式确保了在无权图中,BFS 找到的从起始节点到任何其他节点的路径都是最短的(以边的数量衡量)。
例如,在社交网络中找到与你关系最近(中间隔着最少朋友)的某个特定职业的人,或者在棋盘游戏中计算最少步数到达某个状态。
为了实现 BFS 的"逐层扩展"特性,即按照节点被发现的顺序来检查它们,我们需要一种特定的数据结构来管理待访问的节点列表。这个数据结构就是**队列 (Queue)**。
因此,要实现广度优先搜索并找到最短路径,必须使用队列。
在代码中表示图,常用的一种方法是使用散列表(在 Python 中是字典)。字典的键是图中的节点,对应的值是一个列表,该列表包含了该节点的所有邻居。
# 使用字典来表示图的关系
graph = {}
graph["you"] = ["alice", "bob", "claire"] # "you" 是一个节点,它的邻居是 alice, bob, claire
graph["bob"] = ["anuj", "peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom", "jonny"]
graph["anuj"] = [] # anuj 没有邻居
graph["peggy"] = []
graph["thom"] = []
graph["jonny"] = []
# 这个图可以想象成一个社交网络
# you -- alice
# | |
# bob -- peggy
# |
# anuj
# |
# claire -- thom
# |
# jonny
下面是实现 BFS 算法的通用步骤,以《算法图解》中寻找芒果销售商的例子为背景。
searched
,用于存放已经检查过(处理过其邻居)的节点。这非常重要,可以避免重复检查同一个节点,更重要的是防止在有环路的图中陷入无限循环。 person
)。 b. 检查是否已处理:如果该节点 person
已经在 searched
集合中,则跳过,处理下一个。 c. 目标判断:检查节点 person
是否满足目标条件(例如, person_is_seller(person)
是否为 True
)。 * 如果满足条件,则表示找到了目标,搜索成功。可以打印信息并返回 True
(或路径)。 * 如果不满足条件,则将该节点 person
的所有邻居加入搜索队列的末尾。然后,将 person
加入 searched
集合,表示该节点已被处理。 False
。 from collections import deque # 导入双端队列,可以高效地在两端添加和删除元素
# 图的表示 (同上节)
graph = {}
graph["you"] = ["alice", "bob", "claire"]
graph["bob"] = ["anuj", "peggy"]
graph["alice"] = ["peggy"]
graph["claire"] = ["thom", "jonny"]
graph["anuj"] = []
graph["peggy"] = []
graph["thom"] = [] # 假设 thom 是芒果销售商
graph["jonny"] = []
def person_is_seller(name):
"""判断一个人是否是芒果销售商 (书中示例:名字以 'm' 结尾)"""
return name == "thom" # 修改为 thom 是销售商
def breadth_first_search(start_node):
search_queue = deque() # 1. 创建搜索队列
# 将起始节点直接加入队列,在循环开始前处理,或先将其邻居加入
# 书中的例子是直接将第一层关系加入队列,这里我们调整为先加入起始点本身,然后在循环中处理其邻居
# 这样更通用,且能处理起始点就是目标的情况
if start_node not in graph: # 确保起始节点在图中
print(f"起始节点 {start_node} 不在图中。")
return False
search_queue.append(start_node)
searched = set() # 2. 创建已搜索节点的集合
while search_queue: # 3. 当队列不为空
person = search_queue.popleft() # a. 从队列头部取出一个节点
if person not in searched: # b. 检查是否已处理过
print(f"正在检查 {person}...")
if person_is_seller(person): # c. 目标判断
print(f"{person} 是一个芒果销售商!")
return True # 找到目标
else:
# 将其所有邻居加入队列末尾 (如果这些邻居存在于图中)
if person in graph:
for neighbor in graph[person]:
if neighbor not in searched and neighbor not in search_queue:
search_queue.append(neighbor)
searched.add(person) # 将当前节点标记为已搜索
# else:
# print(f"{person} 已经被检查过了或已在队列中,跳过。") # 可选的调试信息
print("队列为空,没有找到芒果销售商。")
return False # 4. 搜索失败
# 从 "you" 开始搜索
if breadth_first_search("you"):
print("搜索成功!")
else:
print("搜索失败,网络中没有芒果销售商。")
# 测试起始点就是目标的情况
# graph["thom_seller"] = []
# def is_seller_direct(name):
# return name == "thom_seller"
# if breadth_first_search("thom_seller"):
# print("Direct search successful!")
# else:
# print("Direct search failed.")
代码说明与调整:
searched
集合或 search_queue
中,以避免重复添加,尽管 searched
集合主要防止重复处理。 记录已检查(searched
)的节点至关重要:
广度优先搜索的运行时间主要取决于图中的节点数量和边的数量。
因此,广度优先搜索的总体时间复杂度为 **O(V + E)**,其中:
广度优先搜索 (BFS) 是一种基础而强大的图算法。其核心要点包括:
理解 BFS 为学习更复杂的图算法(如 Dijkstra 算法)奠定了坚实的基础。
本文由 mdnice 多平台发布