图的遍历:深度优先搜索(DFS)和广度优先搜索(BFS)
1. 什么是图?
在计算机科学中,图(Graph)是一种数据结构,由节点(也称为顶点)和连接这些节点的边组成。图可以用来表示各种关系,比如社交网络中的朋友关系、地图中的路径、网络中的设备连接等。
图分为以下几种类型:
- 无向图:图中的边没有方向,表示节点之间的关系是对称的。
- 有向图:图中的边有方向,表示节点之间的关系是单向的。
- 加权图:图中的边带有权重,表示连接两个节点的代价、距离或其他度量。
- 无环图:图中没有形成环路。
- 连通图:图中的任意两个节点之间都有路径相连。
2. 如何判断是图?
图是由节点和连接这些节点的边组成的数据结构。要判断一个结构是否是图,我们可以检查以下特征:
- 节点集合:图由一组节点(顶点)组成,通常使用
V
表示。 - 边集合:图还包含一组边,边是连接两个节点的关系,通常使用
E
表示。 - 连接性:在图中,节点之间的连接性由边来表示,这些连接可以是单向或双向的。
在图的表示中,通常使用邻接表(Adjacency List)或邻接矩阵(Adjacency Matrix)来表示图的结构。
3. 图的遍历
图的遍历指的是从图中的某一个节点开始,访问所有与之相连的节点。常见的图遍历算法有两种:深度优先搜索(DFS) 和 广度优先搜索(BFS)。
3.1 深度优先搜索(DFS)
深度优先搜索(Depth-First Search, DFS)是一种从图的起始节点出发,尽可能深的访问图的搜索算法。它优先访问未访问的子节点,直到无法再深入为止,然后回溯到前一个节点继续搜索未访问的其他节点。
DFS 可以使用 递归 和 迭代 两种方式实现。
3.1.1 递归实现 DFS
递归实现 DFS 通过递归函数的调用栈来模拟对图的深度优先遍历。
递归代码示例:
#include <vector>
#include <iostream>
void dfs(int node, std::vector<bool>& visited, const std::vector<std::vector<int>>& graph) {
visited[node] = true;
std::cout << node << " "; // 访问节点
// 访问相邻节点
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
dfs(neighbor, visited, graph);
}
}
}
int main() {
// 示例图(无向图)
std::vector<std::vector<int>> graph = {
{1, 2}, // 0 -> 1, 2
{0, 3, 4}, // 1 -> 0, 3, 4
{0, 5, 6}, // 2 -> 0, 5, 6
{1}, // 3 -> 1
{1}, // 4 -> 1
{2}, // 5 -> 2
{2} // 6 -> 2
};
std::vector<bool> visited(graph.size(), false);
dfs(0, visited, graph); // 从节点0开始DFS
return 0;
}
递归 DFS 的执行过程:
- 从节点
0
开始,访问节点0
,标记为已访问。 - 访问节点
0
的邻居1
,递归调用进入节点1
。 - 访问节点
1
的邻居0
(已访问,跳过),然后访问3
和4
。 - 递归完成后,回溯到节点
0
,访问节点2
,然后访问5
和6
。
输出结果:0 1 3 4 2 5 6
3.1.2 迭代实现 DFS
迭代实现 DFS 使用显式的栈来模拟递归调用栈的行为。
迭代代码示例:
#include <vector>
#include <stack>
#include <iostream>
void dfs_iterative(int start, const std::vector<std::vector<int>>& graph) {
std::vector<bool> visited(graph.size(), false);
std::stack<int> stack;
stack.push(start);
while (!stack.empty()) {
int node = stack.top();
stack.pop();
if (!visited[node]) {
visited[node] = true;
std::cout << node << " "; // 访问节点
// 将邻居节点压入栈中(逆序保证DFS的顺序性)
for (auto it = graph[node].rbegin(); it != graph[node].rend(); ++it) {
if (!visited[*it]) {
stack.push(*it);
}
}
}
}
}
int main() {
// 示例图(无向图)
std::vector<std::vector<int>> graph = {
{1, 2}, // 0 -> 1, 2
{0, 3, 4}, // 1 -> 0, 3, 4
{0, 5, 6}, // 2 -> 0, 5, 6
{1}, // 3 -> 1
{1}, // 4 -> 1
{2}, // 5 -> 2
{2} // 6 -> 2
};
dfs_iterative(0, graph); // 从节点0开始DFS
return 0;
}
迭代 DFS 的执行过程:
- 从节点
0
开始,将0
压入栈中。 - 弹出栈顶节点
0
并访问,压入其邻居2
和1
。 - 弹出栈顶节点
1
并访问,压入其邻居4
和3
。 - 继续弹出并访问,直至所有节点访问完毕。
输出结果:0 1 3 4 2 5 6
3.2 广度优先搜索(BFS)
广度优先搜索(Breadth-First Search, BFS)是一种从图的起始节点出发,优先访问距离起始节点最近的节点的搜索算法。它使用队列来维护访问顺序,逐层扩展,直到访问完所有节点。
BFS 一般使用 迭代 的方式实现。
3.2.1 迭代实现 BFS
迭代代码示例:
#include <vector>
#include <queue>
#include <iostream>
void bfs(int start, const std::vector<std::vector<int>>& graph) {
std::vector<bool> visited(graph.size(), false);
std::queue<int> queue;
queue.push(start);
visited[start] = true;
while (!queue.empty()) {
int node = queue.front();
queue.pop();
std::cout << node << " "; // 访问节点
// 将所有未访问的邻居节点加入队列
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.push(neighbor);
}
}
}
}
int main() {
// 示例图(无向图)
std::vector<std::vector<int>> graph = {
{1, 2}, // 0 -> 1, 2
{0, 3, 4}, // 1 -> 0, 3, 4
{0, 5, 6}, // 2 -> 0, 5, 6
{1}, // 3 -> 1
{1}, // 4 -> 1
{2}, // 5 -> 2
{2} // 6 -> 2
};
bfs(0, graph); // 从节点0开始BFS
return 0;
}
BFS 的执行过程:
- 从节点
0
开始,将0
压入队列。 - 弹出队列前端的节点
0
并访问,压入其邻居1
和2
。 - 继续弹出并访问队列前端的节点,直至所有节点访问完毕。
输出结果:0 1 2 3 4 5 6
4. DFS 和 BFS 的区别
- DFS:
- 优先访问深层节点。
- 使用栈(递归调用栈或显式
栈)实现。
-
适合用于搜索路径、检查连通性、解决迷宫等问题。
-
BFS:
- 优先访问同一层的节点。
- 使用队列实现。
- 适合用于寻找最短路径、层次遍历等问题。
5. 示例分析
5.1 图结构
以以下无向图为例,来解释 DFS 和 BFS 的过程:
0
/ \
1 2
/ \ / \
3 4 5
- 节点:
0, 1, 2, 3, 4, 5
- 边:
0
连接1
和2
1
连接3
和4
2
连接5
5.2 深度优先搜索(DFS)
-
DFS 递归:
- 从
0
出发,访问0
->1
->3
(回溯) ->4
(回溯) ->2
->5
。
- 从
-
DFS 迭代:
- 从
0
出发,访问0
->2
->5
(回溯) ->1
->4
(回溯) ->3
。
- 从
5.3 广度优先搜索(BFS)
- BFS:
- 从
0
出发,访问0
->1
->2
->3
->4
->5
。
- 从
6. 总结
在解决图遍历问题时,DFS 和 BFS 是两种非常有用的搜索策略。DFS 更适合用于需要深度探索的场景,如路径查找或连通性问题;BFS 则更适合用于层次遍历或寻找最短路径的场景。理解这两种算法及其实现方式,对于解决复杂的图问题是非常重要的。
程中更上一层楼。