趋近智
广度优先搜索(BFS)逐层遍历图,而深度优先搜索(DFS)采用不同的方式。顾名思义,DFS 在回溯之前会沿着每条路径尽可能深入。可以想象成在迷宫中穿行:沿着一条路径走到死胡同,然后回溯到上一个交叉点,尝试另一条未被检查的路径。这种策略对多种图算法来说非常有用,可用于分析网络结构和依赖关系。
DFS 通常从任意节点(在搜索中常被称为“源”或“根”)开始,并沿某条路径进行。以下是一般过程:
与 BFS 使用队列管理要访问的节点不同,DFS 自然地使用栈。这个栈可以是程序的调用栈(在递归实现中),也可以是显式栈数据结构(在迭代实现中)。
递归提供了一种巧妙的方法来实现 DFS。函数会为其每个未访问的邻居调用自身,有效地利用调用栈管理回溯过程。
假设我们的图使用邻接表(一个字典,键是节点,值是其邻居列表)表示。
import collections
def dfs_recursive(graph, node, visited):
"""
从'node'开始递归执行深度优先搜索。
参数:
graph (dict): 图的邻接表表示。
示例:{'A': ['B', 'C'], 'B': ['D'], ...}
node: 当前DFS操作的起始节点。
visited (set): 用于记录已访问节点的集合。
返回:
无。就地修改'visited'集合。
"""
if node not in visited:
print(f"正在访问节点:{node}") # 处理节点(例如,打印)
visited.add(node)
if node in graph: # 检查节点是否有邻居
for neighbor in graph[node]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
# 示例用法:
graph_adj = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
print("从节点A开始的递归DFS:")
visited_nodes_rec = set()
dfs_recursive(graph_adj, 'A', visited_nodes_rec)
print("已访问节点:", visited_nodes_rec)
# 处理不连通图(如果需要)
# 您可以遍历所有节点,如果未访问则启动DFS
# all_nodes = list(graph_adj.keys()) # 或通过其他方式获取所有节点
# visited_all = set()
# for node in all_nodes:
# if node not in visited_all:
# print(f"\n从{node}开始新的DFS组件")
# dfs_recursive(graph_adj, node, visited_all)
递归方式与 DFS 的定义相符:访问一个节点,然后逐个递归访问其未访问的邻居。
虽然递归对 DFS 来说通常很直观,但对于非常深的图,它可能导致栈溢出错误。使用显式栈的迭代方法可避免此限制。
import collections
def dfs_iterative(graph, start_node):
"""
使用栈迭代执行深度优先搜索。
参数:
graph (dict): 图的邻接表表示。
start_node: 搜索的起始节点。
返回:
set: 一个集合,包含搜索期间访问过的所有节点。
"""
if start_node not in graph:
print(f"起始节点 {start_node} 不在图中。")
return set()
visited = set()
stack = collections.deque([start_node]) # 将 deque 用作栈
print("从节点A开始的迭代DFS:")
while stack:
node = stack.pop() # 获取最后添加的节点(后进先出)
if node not in visited:
print(f"正在访问节点:{node}") # 处理节点
visited.add(node)
# 将未访问的邻居添加到栈中
# 以反向顺序添加邻居,以便按标准顺序访问它们
# (尽管顺序对正确性并非严格必要)
if node in graph:
# 如果需要模拟递归顺序,则反向处理邻居
for neighbor in reversed(graph[node]):
if neighbor not in visited:
stack.append(neighbor)
return visited
# 示例用法(使用与之前相同的graph_adj):
visited_nodes_iter = dfs_iterative(graph_adj, 'A')
print("已访问节点:", visited_nodes_iter)
在迭代版本中,我们将起始节点压入栈。然后,只要栈不为空,我们就弹出一个节点,访问它(如果尚未访问),并将其未访问的邻居压入栈。在 Python 中,使用 collections.deque 进行高效的栈操作(从同一端添加和弹出)很常见。请注意,邻居被压入栈的顺序会影响 DFS 遍历的具体路径,但从起始点可达的已访问节点集合最终将是相同的。
考虑下面这个简单图。让我们从节点 'A' 开始追踪 DFS 路径,假设邻居按字母顺序进行检查。
从节点A开始的一种可能的DFS遍历(按字母顺序检查邻居):A -> B -> D -> E -> F -> C -> G。红色节点是起始点。带数字的粉色边显示了访问未访问节点的顺序。
路径深入:从 A 到 B,从 B 到 D。D 没有未访问的邻居,因此回溯到 B。B 的下一个未访问邻居是 E。前进 A -> B -> E。E 的下一个未访问邻居是 F。前进 A -> B -> E -> F。F 的邻居(C、E、A)都已被访问或在当前回到起点的路径上,因此回溯到 E,然后 B,再然后 A。A 的下一个未访问邻居是 C。前进 A -> C。C 的下一个未访问邻居是 F(已访问)。C 的下一个未访问邻居是 G。前进 A -> C -> G。G 没有未访问的邻居。回溯到 C,然后 A。A 的所有邻居都已访问。DFS 完成。访问顺序将是 A、B、D、E、F、C、G。
visited 集合,这需要 O(V) 空间。
因此,总空间复杂度通常由栈或递归深度决定,最终为 O(V)。DFS 这种偏向于深度行进的特性使其适用于多种图相关任务,这些任务常见于机器学习背景中:
理解 DFS 提供了另一个重要的工具,用于遍历和分析支撑许多机器学习问题的图结构,从特征依赖到网络分析和推荐系统。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造