图的深度优先搜索(Depth-First Search, DFS)
深度优先搜索(DFS)是一种用于遍历或搜索图(或树)的算法。其核心思想是尽可能深地探索图的分支,当某条路径无法继续前进时,回溯到上一个节点,选择另一条未探索的路径继续探索。这种“深入到底,再回溯”的特性是DFS的显著标志。
一、DFS的基本原理
- 起点选择:从图中任意一个起始节点开始(若图有多个连通分量,需遍历每个分量)。
- 探索规则:
- 访问当前节点,并标记为“已访问”(避免重复访问)。
- 选择当前节点的一个未访问邻接节点,递归(或借助栈)深入探索该节点。
- 若当前节点的所有邻接节点均已访问,则回溯到上一个节点,继续探索其他未访问路径。
- 终止条件:所有可达节点均被访问。
二、DFS的实现方式
DFS可通过递归或栈(迭代) 实现,两种方式本质一致(递归的调用栈等价于手动维护的栈)。
1. 递归实现
-
步骤:
- 定义递归函数,参数为当前节点。
- 标记当前节点为已访问。
- 遍历当前节点的所有邻接节点,若未访问则递归调用该函数。
-
伪代码:
visited = 集合() # 存储已访问节点 def dfs_recursive(node): if node in visited: return visited.add(node) # 标记访问 print(node) # 访问节点(可替换为具体操作) for neighbor in 邻接表[node]: # 遍历所有邻接节点 dfs_recursive(neighbor) # 递归探索
2. 栈(迭代)实现
-
步骤:
- 初始化栈,将起始节点入栈,并标记为已访问。
- 当栈不为空时,弹出栈顶节点并访问。
- 将该节点的所有未访问邻接节点入栈,并标记为已访问(注意入栈顺序可能影响遍历顺序)。
-
伪代码:
def dfs_iterative(start): visited = 集合() stack = [start] visited.add(start) while stack: node = stack.pop() # 弹出栈顶节点 print(node) # 访问节点 # 逆序入栈(保证遍历顺序与递归一致,可选) for neighbor in reversed(邻接表[node]): if neighbor not in visited: visited.add(neighbor) stack.append(neighbor)
三、示例:无向图的DFS遍历
假设有如下无向图(邻接表表示):
邻接表 = {
0: [1, 2],
1: [0, 3, 4],
2: [0, 5],
3: [1],
4: [1, 5],
5: [2, 4]
}
以节点0为起点,DFS遍历顺序可能为:0 → 1 → 3 → 4 → 5 → 2
(递归/栈实现的典型结果,具体顺序受邻接节点遍历顺序影响)。
四、DFS的应用场景
- 连通性分析:判断图中两点是否连通,或找出所有连通分量。
- 拓扑排序:在有向无环图(DAG)中,DFS可用于生成拓扑序列(逆后序遍历)。
- 路径搜索:如迷宫问题、求解两点间的一条路径。
- 解决谜题:如N皇后、数独等(通过回溯剪枝)。
- 检测环:在图中检测是否存在环(有向图和无向图均适用)。
- 生成树/森林:DFS遍历过程中,访问边构成的树称为DFS树(森林)。
五、DFS的时间与空间复杂度
-
时间复杂度:
设图中节点数为V
,边数为E
。遍历所有节点和边的时间为O(V + E)
(邻接表存储);若用邻接矩阵存储,时间为O(V²)
(需遍历所有可能的边)。 -
空间复杂度:
主要来自存储已访问节点的集合和递归栈(或手动栈)。最坏情况下(链状图),空间复杂度为O(V)
。
六、DFS与BFS的对比
特性 | DFS(深度优先搜索) | BFS(广度优先搜索) |
---|---|---|
数据结构 | 栈(递归调用栈或手动栈) | 队列 |
遍历方式 | 深度优先,可能先探索远距离节点 | 广度优先,优先探索近距离节点 |
适用场景 | 连通性、拓扑排序、路径搜索(任意) | 最短路径(无权图)、层次遍历 |
空间复杂度 | 最坏O(V) (链状图) | 最坏O(V) (完全图) |
总结
DFS是一种直观且强大的图遍历算法,通过“深入探索+回溯”的策略,能够高效处理多种图相关问题。其递归实现简洁易懂,迭代实现则避免了递归深度限制的问题,实际应用中可根据场景选择合适的方式。
图的深度优先搜索
(Depth-First Search, DFS)
-
基本思想:
从图的某一顶点出发,沿一条路径尽可能深地访问,直到无法继续时回溯到上一个顶点,再选择其他未访问的邻接点继续探索。 -
实现方式:
- 递归实现:通过函数调用栈隐式管理回溯过程。
- 显式栈实现:使用栈数据结构显式模拟递归过程。
-
算法步骤:
- 标记起始顶点为已访问。
- 依次访问当前顶点的每个未访问的邻接点,递归或迭代进行深度优先搜索。
- 当所有邻接点均被访问后,回溯到上一层顶点。
-
时间复杂度:
- 邻接表存储:O(V + E)(V为顶点数,E为边数)。
- 邻接矩阵存储:O(V²)。
-
应用场景:
- 拓扑排序
- 连通分量检测
- 迷宫求解
- 生成树或森林
-
示例伪代码(递归):
DFS(v): mark v as visited for each neighbor u of v: if u is not visited: DFS(u)
深度优先搜索(DFS)和广度优先搜索(BFS)是图与树遍历中最基础的两种算法,核心区别体现在遍历策略、数据结构、适用场景等方面。以下从多个维度详细对比两者的区别:
一、核心思想与遍历策略
-
深度优先搜索(DFS)
遵循“深入到底,回溯探索”的策略:从起始节点出发,优先沿着一条路径尽可能深入探索(访问未探索的邻接节点),直到无法继续(路径终点),再回溯到上一节点,选择另一条未探索的路径重复过程。
形象比喻:类似走迷宫时,一条路走到黑,走不通就退回到上一个岔路口换条路走。 -
广度优先搜索(BFS)
遵循“逐层扩散,先近后远”的策略:从起始节点出发,优先访问其所有直接邻接节点(第一层),再依次访问这些邻接节点的邻接节点(第二层),以此类推,按层次顺序遍历所有可达节点。
形象比喻:类似水波扩散,从起点开始,逐层向外覆盖所有节点。
二、底层数据结构
-
DFS:依赖栈(Stack)
- 递归实现中,使用程序的“调用栈”(隐式栈);
- 迭代实现中,需手动维护一个栈(显式栈)。
栈的“后进先出(LIFO)”特性保证了每次优先探索最新发现的节点(即当前路径的最深节点)。
-
BFS:依赖队列(Queue)
必须手动维护一个队列(显式队列),队列的“先进先出(FIFO)”特性保证了先访问的节点其邻接节点也优先被访问(按层次顺序)。
三、遍历顺序对比(示例)
以如下无向图为例(节点0为起点):
0
/ \
1 2
/ \ \
3 4 5
-
DFS遍历顺序(可能结果):
0 → 1 → 3 → 4 → 2 → 5(先深入1的分支,回溯后再探索2的分支)。
注:顺序受邻接节点遍历顺序影响,若先访问2再访问1,则结果可能为0→2→5→1→3→4。 -
BFS遍历顺序(唯一结果):
0 → 1 → 2 → 3 → 4 → 5(先访问0的邻接节点1、2,再访问1的邻接节点3、4和2的邻接节点5)。
四、适用场景
-
DFS的典型应用:
- 连通性分析(如判断两点是否连通、找出所有连通分量);
- 拓扑排序(有向无环图DAG中,通过逆后序遍历实现);
- 路径搜索(如找出任意一条从起点到终点的路径,不保证最短);
- 解决回溯类问题(如N皇后、数独、迷宫求解);
- 检测图中的环(尤其是有向图)。
-
BFS的典型应用:
- 最短路径问题(在无权图中,BFS能找到起点到其他节点的最短路径);
- 层次遍历(如树的按层打印、社交网络中“一度好友”“二度好友”统计);
- 广度优先搜索树(用于网络爬虫按层次抓取页面);
- 求解无权图中节点的最短距离。
五、时间与空间复杂度
指标 | DFS | BFS |
---|---|---|
时间复杂度 | 均为O(V + E)(邻接表存储,V为节点数,E为边数);邻接矩阵存储时为O(V²)。 | 同DFS,与遍历所有节点和边的操作相关。 |
空间复杂度 | 最坏O(V)(如链状图,栈深度等于节点数)。 | 最坏O(V)(如完全图,队列需存储所有节点)。 |
六、关键区别总结
特性 | DFS | BFS |
---|---|---|
核心策略 | 深度优先,优先深入路径 | 广度优先,优先扩散层次 |
数据结构 | 栈(递归或手动维护) | 队列(手动维护) |
遍历顺序 | 非层次化,可能跳跃式访问 | 严格层次化,按距离递增访问 |
路径特性 | 不保证最短路径 | 无权图中可找到最短路径 |
空间依赖 | 依赖路径长度(栈深度) | 依赖当前层节点数(队列大小) |
递归限制 | 递归实现受栈深度限制(易栈溢出) | 迭代实现,无递归深度限制 |
通过以上对比可见,DFS和BFS虽同为图遍历算法,但因策略和数据结构的差异,适用场景各有侧重。实际应用中需根据问题需求(如是否需最短路径、是否涉及回溯等)选择合适的算法。