深度优先搜索(DFS)详解及C++实现
一、什么是深度优先搜索(DFS)?
深度优先搜索(Depth-First Search,简称DFS)是一种用于遍历或搜索树或图的算法。其核心思想是:尽可能深地搜索图的分支,当某条分支搜索到尽头无法继续前进时,回溯到上一个节点,再选择另一条未探索的分支继续搜索,直到所有节点都被访问完毕。
可以用一个生动的比喻理解DFS:想象你走进一个迷宫,每次遇到岔路时,随机选择一条路一直走,直到走到死胡同(无法继续前进),然后沿原路返回上一个岔路,选择另一条未走过的路继续探索,直到找到出口或遍历完整个迷宫。
DFS的实现通常依赖栈(Stack)这种数据结构(手动实现时),或者直接利用递归函数调用栈(更简洁,也是最常用的方式)。递归实现的本质是将每次的节点访问和回溯过程交给函数栈来管理,无需手动维护栈结构。
二、DFS的核心特性与适用场景
1. 核心特性
-
不撞南墙不回头:优先深入探索当前分支,而非横向遍历同级节点;
-
回溯思想:探索到尽头后,返回上一节点继续探索其他分支,需要记录节点访问状态(避免重复访问);
-
空间复杂度:取决于递归深度或栈的大小,最坏情况下为O(n)(n为节点数);
-
时间复杂度:遍历图时,时间复杂度为O(V+E)(V为顶点数,E为边数);遍历树时,为O(n)(树中边数为n-1)。
2. 适用场景
-
图的遍历(连通分量查找、拓扑排序等);
-
树的遍历(前序、中序、后序遍历,路径搜索等);
-
迷宫问题(最短路径不适用,DFS不保证最短,需用BFS;但可用于判断是否存在路径);
-
排列组合问题(如全排列、子集生成等);
-
回溯法解决的经典问题(如N皇后、数独求解等)。
三、DFS的两种实现方式(C++)
DFS的实现分为递归实现 和非递归实现(手动栈)。递归实现代码简洁,易于理解;非递归实现则更灵活,可避免递归深度过大导致的栈溢出问题。下面以「无向图的遍历」和「树的前序遍历」为例,讲解两种实现方式。
1. 递归实现(最常用)
递归实现的核心逻辑:
-
访问当前节点,标记为已访问;
-
遍历当前节点的所有邻接节点;
-
对每个未访问的邻接节点,递归调用DFS函数。
案例1:无向图的DFS遍历
假设我们有如下无向图:
0 → 1 → 2
0 → 3 → 4
用邻接表存储图(邻接表是图的常用存储方式,适合稀疏图),然后通过递归DFS遍历所有节点。
cpp
#include <iostream>
#include <vector>
using namespace std;
// 邻接表存储图
vector<vector<int>> adj;
// 标记节点是否被访问
vector<bool> visited;
// 递归实现DFS
void dfs(int u) {
// 标记当前节点为已访问
visited[u] = true;
// 访问当前节点(此处为打印节点值)
cout << u << " ";
// 遍历当前节点的所有邻接节点
for (int v : adj[u]) {
// 如果邻接节点未被访问,递归调用DFS
if (!visited[v]) {
dfs(v);
}
}
}
int main() {
// 图的节点数
int n = 5;
// 初始化邻接表和访问标记数组
adj.resize(n);
visited.resize(n, false);
// 构建无向图
adj[0].push_back(1);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(1);
adj[0].push_back(3);
adj[3].push_back(0);
adj[3].push_back(4);
adj[4].push_back(3);
cout << "DFS遍历结果(递归):";
// 从节点0开始遍历(若图不连通,需遍历所有未访问节点)
dfs(0);
cout << endl;
return 0;
}
输出结果:
DFS遍历结果(递归):0 1 2 3 4
说明:从节点0出发,先深入探索1→2分支,回溯后再探索3→4分支,最终遍历所有节点。
案例2:二叉树的前序遍历(DFS)
二叉树的前序遍历(根→左→右)是DFS的典型应用,递归实现非常简洁。
cpp
#include <iostream>
using namespace std;
// 二叉树节点定义
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 递归实现前序遍历(DFS)
void preorderDFS(TreeNode* root) {
// 递归终止条件:节点为空
if (root == nullptr) {
return;
}
// 访问根节点
cout << root->val << " ";
// 递归遍历左子树
preorderDFS(root->left);
// 递归遍历右子树
preorderDFS(root->right);
}
int main() {
// 构建一棵二叉树:
// 1
// \
// 2
// /
// 3
TreeNode* root = new TreeNode(1);
root->right = new TreeNode(2);
root->right->left = new TreeNode(3);
cout << "二叉树前序遍历(DFS):";
preorderDFS(root);
cout << endl;
// 释放内存(简化处理,实际应手动遍历释放)
delete root->right->left;
delete root->right;
delete root;
return 0;
}
输出结果:
二叉树前序遍历(DFS):1 2 3
2. 非递归实现(手动栈)
当递归深度过大时(如遍历深度为1e4的树),会导致栈溢出(C++默认递归栈大小有限),此时需要用手动栈实现DFS。核心逻辑与递归一致,只是将递归调用栈替换为手动维护的栈。
非递归实现步骤:
-
将起始节点压入栈,标记为已访问;
-
弹出栈顶节点,访问该节点;
-
将该节点的所有未访问邻接节点压入栈(注意:为保证遍历顺序与递归一致,需逆序压入,因为栈是先进后出);
-
重复步骤2-3,直到栈为空。
案例1:无向图的DFS遍历(非递归)
cpp
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
vector<vector<int>> adj;
vector<bool> visited;
// 非递归实现DFS(手动栈)
void dfsNonRecursive(int start) {
stack<int> st;
// 压入起始节点,标记为已访问
st.push(start);
visited[start] = true;
while (!st.empty()) {
// 弹出栈顶节点
int u = st.top();
st.pop();
// 访问当前节点
cout << u << " ";
// 遍历邻接节点(逆序压入,保证遍历顺序与递归一致)
for (auto it = adj[u].rbegin(); it != adj[u].rend(); ++it) {
int v = *it;
if (!visited[v]) {
visited[v] = true;
st.push(v);
}
}
}
}
int main() {
int n = 5;
adj.resize(n);
visited.resize(n, false);
// 构建无向图(同递归案例)
adj[0].push_back(1);
adj[1].push_back(0);
adj[1].push_back(2);
adj[2].push_back(1);
adj[0].push_back(3);
adj[3].push_back(0);
adj[3].push_back(4);
adj[4].push_back(3);
cout << "DFS遍历结果(非递归):";
dfsNonRecursive(0);
cout << endl;
return 0;
}
输出结果:
DFS遍历结果(非递归):0 1 2 3 4
说明:这里对邻接节点逆序遍历(rbegin()和rend()),是因为栈是"先进后出"的。如果直接正序压入,遍历顺序会变成0→3→4→1→2,虽然也是DFS,但与递归实现的顺序不一致(不影响遍历完整性,仅影响顺序)。
案例2:二叉树的前序遍历(非递归DFS)
cpp
#include <iostream>
#include <stack>
using namespace std;
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 非递归实现前序遍历(DFS)
void preorderDFSNonRecursive(TreeNode* root) {
if (root == nullptr) {
return;
}
stack<TreeNode*> st;
// 压入根节点
st.push(root);
while (!st.empty()) {
// 弹出栈顶节点,访问
TreeNode* node = st.top();
st.pop();
cout << node->val << " ";
// 注意:先压右子树,再压左子树(栈先进后出,保证左子树先访问)
if (node->right != nullptr) {
st.push(node->right);
}
if (node->left != nullptr) {
st.push(node->left);
}
}
}
int main() {
// 构建与递归案例相同的二叉树
TreeNode* root = new TreeNode(1);
root->right = new TreeNode(2);
root->right->left = new TreeNode(3);
cout << "二叉树前序遍历(非递归DFS):";
preorderDFSNonRecursive(root);
cout << endl;
// 释放内存
delete root->right->left;
delete root->right;
delete root;
return 0;
}
输出结果:
二叉树前序遍历(非递归DFS):1 2 3
四、DFS的经典应用:回溯法求解N皇后问题
DFS的核心是"探索-回溯",而回溯法是DFS的一种延伸应用,常用于解决"选择-验证-回溯-再选择"的组合优化问题。N皇后问题是回溯法的经典案例:在n×n的棋盘上放置n个皇后,使得任意两个皇后不处于同一行、同一列或同一斜线上。
cpp
#include <iostream>
#include <vector>
#include <string>
using namespace std;
vector<vector<string>> result; // 存储所有合法解
// 检查当前位置(row, col)是否可以放置皇后
bool isValid(int n, int row, int col, vector<string>& board) {
// 检查同一列是否有皇后
for (int i = 0; i < row; ++i) {
if (board[i][col] == 'Q') {
return false;
}
}
// 检查左上到右下的斜线(左上方向)
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {
if (board[i][j] == 'Q') {
return false;
}
}
// 检查右上到左下的斜线(右上方向)
for (int i = row - 1, j = col + 1; i >= 0 && j < n; --i, ++j) {
if (board[i][j] == 'Q') {
return false;
}
}
return true;
}
// DFS回溯函数:当前处理第row行
void backtrack(int n, int row, vector<string>& board) {
// 递归终止条件:所有行都处理完毕(找到一个合法解)
if (row == n) {
result.push_back(board);
return;
}
// 遍历当前行的每一列,尝试放置皇后
for (int col = 0; col < n; ++col) {
if (isValid(n, row, col, board)) {
// 选择:在当前位置放置皇后
board[row][col] = 'Q';
// 探索:处理下一行
backtrack(n, row + 1, board);
// 回溯:撤销选择,恢复原状
board[row][col] = '.';
}
}
}
// 求解N皇后问题
vector<vector<string>> solveNQueens(int n) {
result.clear();
// 初始化棋盘:n行n列,全部为'.'
vector<string> board(n, string(n, '.'));
backtrack(n, 0, board);
return result;
}
// 打印所有解
void printResult(const vector<vector<string>&> result) {
for (auto& solution : result) {
cout << "=====================" << endl;
for (auto& row : solution) {
cout << row << endl;
}
}
}
int main() {
int n = 4; // 求解4皇后问题
vector<vector<string>> solutions = solveNQueens(n);
cout << n << "皇后问题共有 " << solutions.size() << " 个解:" << endl;
printResult(solutions);
return 0;
}
输出结果(4皇后问题的2个解):
4皇后问题共有 2 个解:
.Q...
...Q
Q...
...Q.
...Q.
Q...
...Q
.Q...
说明:该代码通过DFS回溯,逐行尝试放置皇后,每放置一个皇后就验证合法性,若合法则继续探索下一行,若不合法则回溯到上一行重新选择列。最终遍历出所有合法的放置方案。
五、DFS的常见注意事项
-
避免重复访问:必须使用visited数组(或其他标记方式)标记已访问的节点,否则会陷入无限循环(尤其是图中有环的情况);
-
递归深度控制:递归实现时,若问题规模过大(如n=1e4),会导致栈溢出,此时需改用非递归实现;
-
回溯的撤销操作:回溯法中,每次选择后必须撤销选择(如N皇后问题中恢复board[row][col]为'.'),否则会影响后续探索;
-
邻接表的构建:图的DFS遍历中,邻接表比邻接矩阵更高效(空间复杂度O(V+E) vs O(V²)),适合稀疏图;
-
多连通分量处理:若图不连通,需遍历所有节点,对每个未访问的节点调用DFS(如:for(int i=0; i<n; i++) if(!visited[i]) dfs(i);)。
六、总结
DFS是一种基于"深度优先、回溯探索"的遍历算法,核心依赖栈(递归栈或手动栈)实现。其代码简洁、思想直观,广泛应用于图遍历、树遍历、排列组合、回溯优化等问题。
在C++实现中,递归版本适合小规模问题,代码易写;非递归版本适合大规模问题,避免栈溢出。学习DFS的关键是理解"探索-回溯"的思想,以及如何通过标记和撤销操作控制遍历过程。