一、基本概念
深度优先搜索(Depth-First Search, DFS) 是一种用于遍历或搜索树、图等数据结构的算法。其核心思想是尽可能深地探索分支,直到无法继续前进时回溯到上一个分叉点,转而探索其他分支。
特点:
- 深度优先:优先沿一条路径深入到底,再回溯。
- 栈结构:递归或显式栈实现,后进先出(LIFO)。
- 空间复杂度:O(h),h为树的高度或图的深度。
二、算法流程
- 初始化:选择起点,标记为已访问。
- 深入探索:访问当前节点的第一个未访问邻接节点。
- 回溯机制:若当前节点无未访问邻接节点,回溯至上一节点。
- 终止条件:所有节点均被访问。
(递归实现):
#include <iostream>
#include <vector>
const int MAXN = 100;
std::vector<int> graph[MAXN];
bool visited[MAXN];//标记数组,来记录结点是否已被访问
void dfs(int node) {//传递正在访问的结点和当前结点的深度
visited[node] = true;
std::cout << "访问节点: " << node << std::endl;
for (int neighbor : graph[node]) {
if (!visited[neighbor]) {
dfs(neighbor);
}
}
}
int main() {
int n, m;
std::cin >> n >> m;
for (int i = 0; i < m; ++i) {
int u, v;
std::cin >> u >> v;
graph[u].push_back(v);
graph[v].push_back(u);
}
for (int i = 0; i < n; ++i) {
visited[i] = false;
}
dfs(0);
return 0;
}
三、应用场景
- 路径搜索:迷宫问题、是否存在路径。
- 连通性检测:判断图的连通分量。
- 拓扑排序:有向无环图(DAG)的排序。
- 环检测:判断图中是否存在环。
- 回溯算法:八皇后、数独等组合问题。
四、DFS与BFS对比
特性 | DFS | BFS |
---|---|---|
数据结构 | 栈(递归/显式栈) | 队列 |
遍历顺序 | 深度优先 | 广度优先(逐层扩展) |
空间复杂度 | O(h),h为树高或图深 | O(w),w为树宽或图广 |
适用场景 | 探索所有可能路径、连通性检测 | 最短路径、最小步数问题 |
五、代码示例
1. 树的DFS遍历(递归)
#include <iostream>
using namespace std;
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 前序遍历(根-左-右)
void dfs_preorder(TreeNode* root) {
if (!root) return;
cout << root->val << " "; // 处理当前节点
dfs_preorder(root->left);
dfs_preorder(root->right);
}
// 中序遍历(左-根-右)
void dfs_inorder(TreeNode* root) {
if (!root) return;
dfs_inorder(root->left);
cout << root->val << " "; // 处理当前节点
dfs_inorder(root->right);
}
int main() {
/* 构建示例树:
1
/ \
2 3
/ \ /
4 5 6
*/
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
root->right->left = new TreeNode(6);
cout << "前序遍历: ";
dfs_preorder(root); // 输出:1 2 4 5 3 6
cout << "\n中序遍历: ";
dfs_inorder(root); // 输出:4 2 5 1 6 3
return 0;
}
//运行结果:前序遍历: 1 2 4 5 3 6
//中序遍历: 4 2 5 1 6 3
2. 图的DFS遍历(显式栈)
#include <iostream>
#include <stack>
#include <vector>
#include <unordered_map>
using namespace std;
void dfs_graph(int start, const unordered_map<int, vector<int>>& graph) {
stack<int> st;
vector<bool> visited(graph.size() + 1, false); // 假设节点编号从1开始
st.push(start);
visited[start] = true;
while (!st.empty()) {
int node = st.top();
st.pop();
cout << node << " "; // 处理当前节点
// 逆序压栈保证遍历顺序与递归一致
for (auto it = graph.at(node).rbegin(); it != graph.at(node).rend(); ++it) {
if (!visited[*it]) {
visited[*it] = true;
st.push(*it);
}
}
}
}
int main() {
/* 构建图的邻接表:
1 → 2 → 5
↓ ↓
3 ← 4 → 6
*/
unordered_map<int, vector<int>> graph = {
{1, {2, 3}},
{2, {4, 5}},
{3, {}},
{4, {3, 6}},
{5, {}},
{6, {}}
};
cout << "DFS遍历结果: ";
dfs_graph(1, graph); // 输出:1 2 4 3 6 5
return 0;
}
//运行结果:1 2 4 3 6 5
六、优化技巧
- 剪枝(Pruning)
在搜索过程中提前终止无效分支,减少计算量。
示例:在组合问题中,若当前路径已不满足条件,停止深入。 - 记忆化(Memoization)
缓存已计算结果,避免重复计算。常用于动态规划与DFS结合的场景。 - 迭代深化(IDDFS)
结合BFS的层级限制,逐步增加深度限制,避免DFS陷入过深路径。
七、常见问题
- 栈溢出
解决方法:使用显式栈替代递归,或调整递归深度限制。 - 重复访问
解决方法:维护visited
集合,标记已访问节点(尤其在图遍历中)。 - 路径记录
技巧:使用栈或列表保存当前路径,回溯时弹出末尾节点。
八、总结
- 优势:代码简洁(递归实现)、内存占用低、适合探索所有可能性。
- 劣势:不保证最短路径、可能陷入深层无效搜索。
- 核心口诀:
“一路到底再回溯,栈中探索不忘记。剪枝优化效率高,应用场景要选对。”
掌握DFS的关键在于理解其深度探索与回溯机制,并结合实际问题灵活运用剪枝和记忆化等优化策略。
一、基本概念
广度优先搜索(Breadth-First Search, BFS) 是一种用于遍历或搜索树、图等数据结构的算法。其核心思想是逐层探索节点,先访问离起点最近的节点,再依次向外扩展。BFS 保证找到最短路径(当边权重相等时),常用于解决最短路径问题。
特点:
- 广度优先:逐层扩展,先近后远。
- 队列结构:先进先出(FIFO),用队列实现。
- 空间复杂度:O(w),w为树的最大宽度或图的广度。
二、算法流程
- 初始化:将起点加入队列,标记为已访问。
- 逐层扩展:
- 取出队首节点并处理。
- 将该节点的所有未访问邻接节点加入队列。
- 终止条件:队列为空(所有可达节点已访问)。
伪代码:
def bfs(start):
queue = deque([start])
visited = {start}
while queue:
node = queue.popleft()
process(node) # 处理当前节点
for neighbor in node.neighbors:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
三、应用场景
- 无权图的最短路径:迷宫最短路径、社交网络六度空间。
- 连通性检测:判断图的连通分量。
- 层级遍历:二叉树的层序遍历、图的层级分析。
- 状态转移问题:华容道、魔方最少步数。
四、BFS与DFS对比
特性 | BFS | DFS |
---|---|---|
数据结构 | 队列 | 栈(递归/显式栈) |
遍历顺序 | 层级遍历(广度优先) | 深度优先 |
空间复杂度 | O(w),w为最大宽度 | O(h),h为树高或图深 |
优势场景 | 最短路径、层级分析 | 探索所有路径、连通性检测 |
五、代码示例
1. 树的层序遍历(BFS)
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if (!root) return result;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int level_size = q.size();
vector<int> level;
for (int i = 0; i < level_size; ++i) {
TreeNode* node = q.front();
q.pop();
level.push_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
result.push_back(level);
}
return result;
}
int main() {
// 构建示例树:
// 3
// / \
// 9 20
// / \
// 15 7
TreeNode* root = new TreeNode(3);
root->left = new TreeNode(9);
root->right = new TreeNode(20);
root->right->left = new TreeNode(15);
root->right->right = new TreeNode(7);
vector<vector<int>> res = levelOrder(root);
// 输出结果:
// [3]
// [9,20]
// [15,7]
for (auto& level : res) {
for (int num : level) {
cout << num << " ";
}
cout << endl;
}
return 0;
}
2. 迷宫最短路径(BFS)
#include <iostream>
#include <queue>
#include <vector>
#include <utility> // for pair
using namespace std;
int shortestPath(vector<vector<int>>& maze, pair<int, int> start, pair<int, int> end) {
// 方向:上、下、左、右
vector<pair<int, int>> dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}};
int rows = maze.size();
int cols = maze[0].size();
// 访问标记数组
vector<vector<bool>> visited(rows, vector<bool>(cols, false));
queue<pair<pair<int, int>, int>> q; // ((x,y), steps)
q.push({start, 0});
visited[start.first][start.second] = true;
while (!q.empty()) {
auto [pos, steps] = q.front();
auto [x, y] = pos;
q.pop();
if (x == end.first && y == end.second) {
return steps;
}
for (auto [dx, dy] : dirs) {
int nx = x + dx;
int ny = y + dy;
// 边界检查:未越界、可通行、未访问
if (nx >= 0 && nx < rows && ny >= 0 && ny < cols
&& maze[nx][ny] == 0 && !visited[nx][ny]) {
visited[nx][ny] = true;
q.push({{nx, ny}, steps + 1});
}
}
}
return -1; // 不可达
}
int main() {
// 迷宫示例(0=可通行,1=障碍)
vector<vector<int>> maze = {
{0, 1, 0, 0},
{0, 0, 0, 1},
{1, 1, 0, 0},
{0, 0, 0, 0}
};
pair<int, int> start = {0, 0}; // (行,列)
pair<int, int> end = {3, 3};
int steps = shortestPath(maze, start, end);
cout << "Shortest path steps: " << steps << endl; // 应输出7
return 0;
}
六、优化技巧
- 双向BFS(Bidirectional BFS)
从起点和终点同时开始搜索,减少搜索空间。适用于已知终点的场景。 - 优先队列优化(Dijkstra)
当边权重不相等时,使用优先队列代替普通队列,演变为Dijkstra算法。 - 剪枝(Pruning)
提前终止无效路径的探索,例如在状态搜索中跳过重复状态。
七、常见问题
- 空间爆炸
问题:当图的广度极大时(如指数级增长的节点数),队列可能占用过多内存。
解决:结合DFS的迭代深化(IDDFS)或双向BFS。 - 重复访问
关键:必须在入队时标记访问状态,而非出队时。否则可能导致同一节点多次入队。 - 无权图与有权图
注意:BFS仅保证在无权图(或等权图)中找到最短路径,有权图需用Dijkstra或A*算法。
八、总结
- 优势:保证最短路径、层级分析直观。
- 劣势:空间复杂度较高,不适用于深度极大的场景。
- 核心口诀:
“队列逐层扫,先近再远跑。最短路径它最强,层级遍历是绝招。”
掌握BFS的关键在于理解其层级遍历特性,并熟练应用队列数据结构。在解决最短路径、状态转移等问题时,BFS往往是更优选择。