Lecture_Notes:图论算法:DFS与BFS在路径搜索中的应用
图论算法是计算机科学中的重要基础,而深度优先搜索(DFS)和广度优先搜索(BFS)作为两种核心的图遍历算法,在路径搜索、拓扑排序、连通性分析等场景中有着广泛应用。本文将结合Lecture_Notes项目中的图论笔记,详细解析DFS与BFS的原理、实现及在路径搜索中的实际应用,并通过具体案例对比两者的适用场景。
图的基本概念与存储方式
在深入算法之前,我们需要先理解图(Graph)的基本定义和存储结构。图由顶点(Vertex)和边(Edge)组成,根据边的方向和权重可分为有向图、无向图、加权图等类型。在计算机中,图的存储主要有两种方式:
邻接矩阵(Adjacency Matrix)
邻接矩阵是一个二维数组,其中mat[i][j]表示顶点i和j之间是否存在边。对于无权图,存在边则值为1,否则为0;对于加权图,值为边的权重。
示例:
无向图的邻接矩阵表示如下:
// 伪代码:构建无向图邻接矩阵
int mat[N+1][M+1] = {0}; // 初始化N行M列矩阵
for each edge (u, v):
mat[u][v] = 1; // 无向图需同时更新两个方向
mat[v][u] = 1;
优缺点:
- 优点:判断两点间是否有边的时间复杂度为O(1),适合稠密图。
- 缺点:空间复杂度为O(V²),对于顶点数较多(如V>10³)的图会造成内存浪费。
邻接表(Adjacency List)
邻接表通过链表数组存储图,每个顶点对应一个链表,记录其所有相邻顶点。这种方式能有效节省空间,尤其适合稀疏图。
示例:
无向图的邻接表构建如下:
// 伪代码:构建无向图邻接表
let graph = new Array(N+1).fill().map(() => []); // 初始化N+1个空链表
for each edge (u, v):
graph[u].push(v); // 添加双向边
graph[v].push(u);
空间复杂度:O(V + E),其中V为顶点数,E为边数,是实际应用中更常用的存储方式。详细分析可参考图论基础笔记中的空间复杂度对比。
深度优先搜索(DFS):深入路径的递归遍历
DFS的核心思想
DFS(Depth-First Search)从起始顶点出发,沿着一条路径尽可能深入地搜索,直到无法继续时回溯,探索其他未访问的路径。其本质是利用递归或栈实现的“不撞南墙不回头”的遍历策略。
算法步骤
- 标记当前顶点为已访问。
- 递归访问当前顶点的所有未访问邻接顶点。
- 若所有邻接顶点均已访问,则回溯到上一顶点。
代码实现(递归版)
// 伪代码:DFS递归实现
let visited = new Array(N+1).fill(false); // 记录顶点访问状态
function dfs(u) {
visited[u] = true; // 标记当前顶点为已访问
for (let v of graph[u]) { // 遍历所有邻接顶点
if (!visited[v]) { // 若未访问,则递归访问
dfs(v);
}
}
}
应用场景:环检测与路径搜索
DFS在有向图的环检测中具有天然优势。通过维护一个“当前路径栈”,若遍历过程中遇到已在栈中的顶点,则说明存在环。例如,在课程安排问题中,若检测到环则表示存在循环依赖,无法完成所有课程。
示例:
有向图环检测的核心代码如下:
// 伪代码:有向图环检测(DFS)
let visited = new Array(N+1).fill(false);
let path = new Array(N+1).fill(false); // 记录当前递归路径
function hasCycle(u) {
visited[u] = true;
path[u] = true;
for (let v of graph[u]) {
if (path[v]) return true; // 发现环
if (!visited[v] && hasCycle(v)) return true;
}
path[u] = false; // 回溯时移出路径
return false;
}
可视化流程:
下图展示了DFS在有向图中检测环的过程(图片来源:图论进阶笔记):

广度优先搜索(BFS):层次遍历的最短路径算法
BFS的核心思想
BFS(Breadth-First Search)从起始顶点出发,优先访问所有距离为1的邻接顶点,再访问距离为2的顶点,以此类推。BFS利用队列(Queue)实现,保证了“层次遍历”的特性,因此天然适用于求解无权图的最短路径问题。
算法步骤
- 将起始顶点入队,并标记为已访问。
- 队首顶点出队,遍历其所有未访问邻接顶点,依次入队并标记为已访问。
- 重复步骤2,直到队列为空。
代码实现(队列版)
// 伪代码:BFS队列实现
function bfs(start) {
let visited = new Array(N+1).fill(false);
let q = new Queue(); // 初始化队列
q.enqueue(start);
visited[start] = true;
while (!q.isEmpty()) {
let u = q.dequeue(); // 出队并处理当前顶点
for (let v of graph[u]) { // 遍历邻接顶点
if (!visited[v]) {
visited[v] = true;
q.enqueue(v); // 入队下一层顶点
}
}
}
}
应用场景:多源最短路径与矩阵搜索
BFS的层次遍历特性使其在多源最短路径问题中表现突出。例如,“腐烂的橘子”问题中,多个腐烂橘子同时向四周扩散,BFS可计算所有橘子腐烂的最短时间。
示例:腐烂橘子问题的BFS解法
// 伪代码:多源BFS求解腐烂橘子问题
int orangesRotting(int[][] grid) {
Queue<int[]> q = new LinkedList<>();
int fresh = 0; // 记录新鲜橘子数量
// 初始化:将所有腐烂橘子入队
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 2) q.offer(new int[]{i, j, 0});
else if (grid[i][j] == 1) fresh++;
}
}
// 方向数组:上下左右
int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
int time = 0;
while (!q.isEmpty()) {
int[] curr = q.poll();
int x = curr[0], y = curr[1], t = curr[2];
time = t; // 更新当前时间
for (int[] d : dirs) {
int nx = x + d[0], ny = y + d[1];
if (nx >= 0 && nx < grid.length && ny >=0 && ny < grid[0].length
&& grid[nx][ny] == 1) {
grid[nx][ny] = 2; // 标记为腐烂
fresh--;
q.offer(new int[]{nx, ny, t+1});
}
}
}
return fresh == 0 ? time : -1; // 若有剩余新鲜橘子,返回-1
}
案例效果:
下图展示了BFS解决“腐烂橘子”问题的扩散过程(图片来源:图论应用笔记):

DFS与BFS的对比及适用场景
核心差异
| 特性 | DFS | BFS |
|---|---|---|
| 数据结构 | 栈(递归本质是系统栈) | 队列 |
| 遍历顺序 | 深度优先,可能跳过深层节点 | 广度优先,按层次逐层扩散 |
| 空间复杂度 | O(h),h为图的深度(最坏O(V)) | O(w),w为图的宽度(最坏O(V)) |
| 典型应用 | 环检测、拓扑排序、迷宫路径搜索 | 最短路径(无权图)、层序遍历 |
路径搜索场景对比
-
迷宫问题:
- DFS适合探索所有可能路径,但无法保证最短路径。
- BFS可找到最短路径,但需要存储更多中间状态。
-
网格类问题:
在矩阵中搜索连通区域(如“岛屿数量”问题),DFS和BFS均可实现,但BFS更适合求解最短距离。例如,在8方向移动的网格中,BFS能确保找到从起点到终点的最短步数:// 伪代码:BFS求解网格最短路径(8方向移动) int shortestPathBinaryMatrix(vector<vector<int>>& grid) { if (grid[0][0] == 1) return -1; int n = grid.size(); queue<tuple<int, int, int>> q; // (x, y, steps) q.emplace(0, 0, 1); grid[0][0] = 1; // 标记为已访问 vector<pair<int, int>> dirs = {{-1,-1}, {-1,0}, {-1,1}, {0,-1}, {0,1}, {1,-1}, {1,0}, {1,1}}; while (!q.empty()) { auto [x, y, s] = q.front(); q.pop(); if (x == n-1 && y == n-1) return s; for (auto [dx, dy] : dirs) { int nx = x + dx, ny = y + dy; if (nx >=0 && nx <n && ny >=0 && ny <n && grid[nx][ny] == 0) { grid[nx][ny] = 1; q.emplace(nx, ny, s+1); } } } return -1; }
总结与扩展
DFS和BFS作为图论的基础算法,在路径搜索中各具优势。DFS通过递归深入探索,适合处理环检测和拓扑排序;BFS借助队列逐层扩散,是求解无权图最短路径的利器。在实际应用中,需根据问题特性选择合适的算法:
- 若需最短路径或层序遍历,优先使用BFS;
- 若需探索所有路径或检测环,优先使用DFS。
更多图论算法细节可参考Lecture_Notes项目中的图论进阶笔记和BFS应用案例,其中包含完整代码实现和复杂度分析。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



