深度优先搜索与广度优先搜索

什么是深度优先搜索与广度优先搜索
深度优先搜索(DFS)与广度优先搜索(BFS)是两种最基础、最经典的图遍历算法。它们的核心任务都是:从某个起点出发,按一定规则访问图中所有可达节点,确保每个节点恰好被访问一次。虽然目标相同,但两者的搜索策略截然不同:
- DFS 采用「纵深优先」策略:沿着一条路径不断深入,走到尽头才回溯换路,本质是栈(递归隐式使用系统栈)的思想。
- BFS 采用「广度优先」策略:依次访问距离起点为 1、2、3… 的节点,层层向外推进,本质是队列的思想。
两者广泛应用于寻路算法、连通性分析、拓扑排序、最短路径(无权图)、网页爬虫、迷宫求解等场景。理解 DFS 和 BFS 是学习更高级图算法(如 Dijkstra、A*、网络流等)必不可少的基础。
前情提要——图的存储
DFS 与 BFS 都是对图的遍历算法,而图的存储方式往往直接决定了遍历的实现和效率。假设图有 V 个顶点、E 条边,以下介绍两种最常用的图存储方式。
邻接表
邻接表用数组 + 链表(C++ 中常用 vector)表示每个顶点的所有出边邻居。graph[i] 是一个列表,存储了所有与顶点 i 直接相连的顶点编号。
邻接表的存储形式实例如下:
graph[1] = {2, 3, 4}graph[2] = {1, 6}graph[3] = {1, 7}graph[4] = {1}graph[5] = {6}graph[6] = {2, 5, 7}graph[7] = {3, 6}特点:
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 空间占用 | O(V + E) | 只存实际存在的边,稀疏图极省空间 |
| 判断 i 与 j 是否相邻 | O(degree(i)) | 需要遍历 i 的邻居列表查找 |
| 遍历 i 的所有邻居 | O(degree(i)) | 直接遍历列表,高效 |
| 添加边 | O(1) | 直接 push_back |
| 删除边 | O(degree(i)) | 需要在列表中查找并移除 |
适用场景: 图较稀疏(E ≪ V²),需要频繁遍历邻居。实际中大多数图(社交网络、道路网、网页链接)都是稀疏图,因此邻接表是最常用的存储方式。
邻接矩阵
邻接矩阵用一个 V × V 的二维数组存储图中所有边的信息。adj[i][j] = 1 表示顶点 i 到顶点 j 有边(无向图则同时有 adj[j][i] = 1),adj[i][j] = 0 表示无边。如果是有权图,值可以改为权重。
邻接矩阵的存储形式示例如下:
1 2 3 4 5 6 71 0 1 1 1 0 0 02 1 0 0 0 0 1 03 1 0 0 0 0 0 14 1 0 0 0 0 0 05 0 0 0 0 0 1 06 0 1 0 0 1 0 17 0 0 1 0 0 1 0特点:
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 空间占用 | O(V²) | 无论边多少都占满,稀疏图浪费严重 |
| 判断 i 与 j 是否相邻 | O(1) | 直接查数组,极快 |
| 遍历 i 的所有邻居 | O(V) | 需扫描整行,邻居少也扫 V 次 |
| 添加 / 删除边 | O(1) | 直接修改数组值 |
| 获取边权重 | O(1) | 直接读取 |
适用场景: 图较稠密(E 接近 V²),或需要频繁 O(1) 判断两点是否相邻、查询边权重。Floyd-Warshall 等全源最短路算法通常使用邻接矩阵。
对比总结
| 邻接表 | 邻接矩阵 | |
|---|---|---|
| 空间 | O(V + E),稀疏图省空间 | O(V²),稀疏图浪费 |
| 判断相邻 | O(degree(i)) | O(1) |
| 遍历邻居 | O(degree(i)) | O(V) |
| 适合场景 | 稀疏图、频繁遍历邻居 | 稠密图、频繁查询边 |
| 编码复杂度 | 略高 | 简单直观 |
下文 DFS 使用邻接表实现,BFS 使用邻接矩阵实现,方便对比两种存储方式在实际算法中的不同写法。
深度优先搜索
深度优先搜索(Depth-First Search,简称 DFS)基于回溯思想,核心可以概括为:不撞南墙不回头——一条路走到黑,走不通再回溯换路。
核心步骤
- 从起点出发,标记起点为已访问。
- 选择一个与当前节点相邻且未访问的节点,深入走下去。
- 重复步骤 2,直到当前节点无路可走(没有未访问的相邻节点)。
- 回溯到上一个节点,尝试其他未访问的分支。
- 重复步骤 2~4,直到所有可达节点均被访问完毕,或找到目标节点。
DFS 常见两种实现方式:
- 递归实现:代码简洁,利用系统调用栈自动完成回溯。
- 显式栈实现:手动维护一个栈模拟递归过程,避免递归过深导致栈溢出。
示例
假设我们有一个无向图 G,如下图所示。

假设我们从起点 1 出发,按照「节点编号较小优先」的规则进行深度优先搜索:
- 从起点
1出发,首先访问1。 - 与
1相邻的节点有2、3和4,按编号较小优先选择2。 - 与
2相邻的未访问节点只有6,访问6。 - 与
6相邻的未访问节点有5和7,按编号较小优先选择5。 5没有未访问的相邻节点,回溯到6,继续访问6的另一个相邻节点7。- 与
7相邻的未访问节点只有3,访问3。 3没有未访问的相邻节点,一路回溯到1,起点1还有一个未访问节点4,访问4。
由此,深度优先搜索的遍历顺序为:1 -> 2 -> 6 -> 5 -> 7 -> 3 -> 4。
算法实现
#include <iostream>#include <vector>using namespace std;
/** * 深度优先搜索(递归实现) * @param node 当前访问的节点编号 * @param graph 图的邻接表表示,graph[i] 存储节点 i 的所有邻居 * @param visited 访问标记数组,visited[i] 为 true 表示节点 i 已访问 */void dfs(int node, const vector<vector<int>> &graph, vector<bool> &visited) { // 标记当前节点已访问,并输出 visited[node] = true; cout << node << " ";
// 遍历当前节点的所有邻居 for (int neighbor : graph[node]) { // 如果邻居未被访问,则递归深入 if (!visited[neighbor]) { dfs(neighbor, graph, visited); } }}
int main() { int n = 7; // 图中节点总数(节点编号 1~7) // 使用邻接表存储图,下标从 0 开始,多开一个位置方便直接按编号索引 vector<vector<int>> graph(n + 1);
// 根据示例图片构建无向图 graph[1] = {2, 3, 4}; graph[2] = {1, 6}; graph[3] = {1, 7}; graph[4] = {1}; graph[5] = {6}; graph[6] = {2, 5, 7}; graph[7] = {3, 6};
// visited[i] 标记节点 i 是否已被访问 vector<bool> visited(n + 1, false);
cout << "DFS 遍历顺序:"; dfs(1, graph, visited); // 从节点 1 开始 DFS cout << endl;
return 0;}运行输出为:DFS 遍历顺序:1 2 6 5 7 3 4,与前文分析的遍历顺序一致。
广度优先搜索
广度优先搜索(Breadth-First Search,简称 BFS)是一种「按层遍历」的算法,核心是:层层推进——先访问所有距离起点为 1 的节点,再访问距离为 2 的节点,以此类推,逐步向外扩展。
BFS 的一个重要性质是:在无权图中,首次访问某个节点时经过的路径就是起点到该节点的最短路径。因此 BFS 常用于求无权图的最短路径问题。
核心步骤
- 将起点加入队列,并标记为已访问。
- 从队首取出一个节点,遍历其所有邻居:对于每个未访问的邻居,标记已访问并加入队列。
- 重复步骤 2,直到队列为空。
- 此时所有从起点可达的节点均已按距离远近顺序被访问完毕。
示例
以上述无向图为例,从起点 1 开始进行广度优先搜索:
- 起点
1入队,队列:[1]。 - 取出
1,访问其相邻节点2、3、4,依次入队,队列:[2, 3, 4]。 - 取出
2,其唯一的未访问邻居是6,将6入队,队列:[3, 4, 6]。 - 取出
3,其唯一的未访问邻居是7,将7入队,队列:[4, 6, 7]。 - 取出
4,没有未访问的邻居,队列:[6, 7]。 - 取出
6,其未访问的邻居是5,将5入队,队列:[7, 5]。 - 取出
7,没有未访问的邻居,队列:[5]。 - 取出
5,没有未访问的邻居,队列为空,搜索结束。
广度优先搜索的遍历顺序为:1 -> 2 -> 3 -> 4 -> 6 -> 7 -> 5。
按层来看,第 0 层为 {1},第 1 层为 {2, 3, 4},第 2 层为 {6, 7},第 3 层为 {5},符合 BFS 层层推进的特点。
算法实现
#include <iostream>#include <vector>#include <queue>using namespace std;
/** * 广度优先搜索(队列实现,使用邻接矩阵存储图) * @param n 节点总数 * @param start 搜索起点 * @param adj 邻接矩阵,adj[i][j] == 1 表示节点 i 和 j 之间有边 */void bfs(int n, int start, const vector<vector<int>> &adj) { vector<bool> visited(n + 1, false); // 访问标记数组 queue<int> q; // BFS 用到的队列
visited[start] = true; // 标记起点已访问 q.push(start); // 起点入队
cout << "BFS 遍历顺序:"; while (!q.empty()) { int node = q.front(); // 取出队首节点 q.pop(); // 队首出队 cout << node << " ";
// 遍历邻接矩阵第 node 行,找出所有邻居 for (int j = 1; j <= n; j++) { if (adj[node][j] && !visited[j]) { visited[j] = true; // 标记为已访问 q.push(j); // 邻居入队 } } } cout << endl;}
int main() { int n = 7; // 图中节点总数(节点编号 1~7) // 使用邻接矩阵存储图,adj[i][j] == 1 表示 i 和 j 之间有边 vector<vector<int>> adj(n + 1, vector<int>(n + 1, 0));
// 根据示例图片构建无向图(无向图需双向标记) adj[1][2] = adj[2][1] = 1; adj[1][3] = adj[3][1] = 1; adj[1][4] = adj[4][1] = 1; adj[2][6] = adj[6][2] = 1; adj[3][7] = adj[7][3] = 1; adj[5][6] = adj[6][5] = 1; adj[6][7] = adj[7][6] = 1;
bfs(n, 1, adj); // 从节点 1 开始 BFS
return 0;}运行输出为:BFS 遍历顺序:1 2 3 4 6 7 5,与前文分析的遍历顺序一致。
总结
| 特性 | DFS(深度优先搜索) | BFS(广度优先搜索) |
|---|---|---|
| 核心策略 | 纵深优先,一条路走到黑 | 广度优先,层层推进 |
| 数据结构 | 栈(递归 / 显式栈) | 队列 |
| 空间复杂度 | O(h),h 为递归深度 | O(w),w 为最大宽度 |
| 最短路径(无权图) | 不保证 | 保证 |
| 适用场景 | 连通性判断、拓扑排序、回溯剪枝 | 最短路径、层序遍历、扩散问题 |
| 实现难度 | 递归简洁,栈稍复杂 | 较直观 |
两种算法时间复杂度均为 O(V + E),其中 V 为顶点数,E 为边数。实际应用中,选择 DFS 还是 BFS 取决于问题的具体需求:需要求最短路径时选 BFS,需要深度探索或使用回溯思想时选 DFS。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

