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

2704 字
14 分钟
深度优先搜索与广度优先搜索

什么是深度优先搜索与广度优先搜索#

深度优先搜索(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 7
1 0 1 1 1 0 0 0
2 1 0 0 0 0 1 0
3 1 0 0 0 0 0 1
4 1 0 0 0 0 0 0
5 0 0 0 0 0 1 0
6 0 1 0 0 1 0 1
7 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)基于回溯思想,核心可以概括为:不撞南墙不回头——一条路走到黑,走不通再回溯换路。

核心步骤#

  1. 从起点出发,标记起点为已访问。
  2. 选择一个与当前节点相邻且未访问的节点,深入走下去。
  3. 重复步骤 2,直到当前节点无路可走(没有未访问的相邻节点)。
  4. 回溯到上一个节点,尝试其他未访问的分支。
  5. 重复步骤 2~4,直到所有可达节点均被访问完毕,或找到目标节点。

DFS 常见两种实现方式:

  • 递归实现:代码简洁,利用系统调用栈自动完成回溯。
  • 显式栈实现:手动维护一个栈模拟递归过程,避免递归过深导致栈溢出。

示例#

假设我们有一个无向图 G,如下图所示。

无向图示例
无向图示例

假设我们从起点 1 出发,按照「节点编号较小优先」的规则进行深度优先搜索:

  1. 从起点 1 出发,首先访问 1
  2. 1 相邻的节点有 234,按编号较小优先选择 2
  3. 2 相邻的未访问节点只有 6,访问 6
  4. 6 相邻的未访问节点有 57,按编号较小优先选择 5
  5. 5 没有未访问的相邻节点,回溯到 6,继续访问 6 的另一个相邻节点 7
  6. 7 相邻的未访问节点只有 3,访问 3
  7. 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 常用于求无权图的最短路径问题。

核心步骤#

  1. 将起点加入队列,并标记为已访问。
  2. 从队首取出一个节点,遍历其所有邻居:对于每个未访问的邻居,标记已访问并加入队列。
  3. 重复步骤 2,直到队列为空。
  4. 此时所有从起点可达的节点均已按距离远近顺序被访问完毕。

示例#

以上述无向图为例,从起点 1 开始进行广度优先搜索:

  1. 起点 1 入队,队列:[1]
  2. 取出 1,访问其相邻节点 234,依次入队,队列:[2, 3, 4]
  3. 取出 2,其唯一的未访问邻居是 6,将 6 入队,队列:[3, 4, 6]
  4. 取出 3,其唯一的未访问邻居是 7,将 7 入队,队列:[4, 6, 7]
  5. 取出 4,没有未访问的邻居,队列:[6, 7]
  6. 取出 6,其未访问的邻居是 5,将 5 入队,队列:[7, 5]
  7. 取出 7,没有未访问的邻居,队列:[5]
  8. 取出 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。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
深度优先搜索与广度优先搜索
https://blog.myqfeng.top/posts/dfs-and-bfs/
作者
明月清风
发布于
2026-04-02
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
明月清风
苦逼大学生一枚
公告
欢迎来到我的博客!转载请标明出处:https://blog.myqfeng.top
分类
标签
最新动态
站点统计
文章
6
分类
3
标签
14
总字数
16,915
运行时长
0
最后活动
0 天前
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v6.15.6
文章许可
CC BY-NC-SA 4.0