DFS 详解(C++版)
DFS 深度优先搜索详解(C++ 实现版)
深度优先搜索(Depth-First Search,简称 DFS)是一种核心的图与树遍历算法,其核心思想是 “不撞南墙不回头”—— 从起点出发,优先沿着一条路径深入探索,直到无法继续前进(遇到边界或已访问节点),再回溯到上一个分叉点,选择另一条未探索的路径,重复此过程直至遍历所有可达节点。
DFS 不仅是算法竞赛和工程开发中的基础工具,还广泛应用于拓扑排序、连通性分析、迷宫求解、子集生成等场景。下面从原理、C++ 实现、应用三个维度展开详解。
一、DFS 核心原理
1.1 本质:“深度优先 + 回溯”
DFS 的执行过程依赖 “深度探索” 与 “回溯” 的配合:
- 深度优先探索:优先选择当前节点的一个未访问邻接节点,通过递归(依赖系统调用栈)或迭代(手动维护栈)深入,直到无未访问节点。
- 回溯:路径探索完毕后返回上一分叉点,根据场景决定是否撤销 “已访问” 标记(如子集生成需撤销,图遍历无需撤销),再探索其他路径。
C++ 中实现时,递归依赖编译器的调用栈,迭代需手动使用 std::stack
容器,二者均需通过 “访问标记”(如 std::unordered_set
或数组)避免重复遍历。
1.2 与 BFS 的核心区别(C++ 视角补充)
特性 | DFS(深度优先搜索) | BFS(广度优先搜索) |
---|---|---|
数据结构 | 递归用调用栈,迭代用 std::stack | 用 std::queue |
探索顺序 | 纵向深入(一条路走到头) | 横向扩散(先访问当前层所有节点) |
适用场景 | 迷宫求解、拓扑排序、子集生成 | 最短路径(无权图)、层序遍历 |
空间复杂度 | O (h)(h 为搜索树深度,最坏 O (n)) | O (w)(w 为搜索树最大宽度,最坏 O (n)) |
C++ 实现注意点 | 递归需避免栈溢出,迭代需逆序压栈 | 队列需按顺序入队,无需逆序操作 |
二、DFS 的两种 C++ 实现方式
以下以 “无向图遍历” 为例,用 邻接表(std::vector<std::vector<int>>
)存储图,分别实现递归与迭代版本。
2.1 递归实现(推荐入门)
递归实现逻辑简洁,与 DFS 思想高度一致,适合处理深度不大的场景。
代码实现:无向图递归 DFS
cpp
运行
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;// 递归 DFS 函数:graph-邻接表,start-当前节点,visited-访问标记(引用传递)
void dfsRecursive(const vector<vector<int>>& graph, int start, unordered_set<int>& visited) {// 1. 标记当前节点为已访问visited.insert(start);cout << "访问节点: " << start << " ";// 2. 遍历当前节点的所有邻接节点for (int neighbor : graph[start]) {// 3. 邻接节点未访问则递归if (visited.find(neighbor) == visited.end()) {dfsRecursive(graph, neighbor, visited);}}
}int main() {// 构建无向图邻接表(与 Python 示例一致:0-1-3,0-2-4)vector<vector<int>> graph = {{1, 2}, // 节点 0 的邻接节点{0, 3}, // 节点 1 的邻接节点{0, 4}, // 节点 2 的邻接节点{1}, // 节点 3 的邻接节点{2} // 节点 4 的邻接节点};unordered_set<int> visited; // 存储已访问节点(支持快速查找)cout << "递归 DFS 访问顺序: ";dfsRecursive(graph, 0, visited); // 从节点 0 开始遍历return 0;
}
输出结果
plaintext
递归 DFS 访问顺序: 访问节点: 0 访问节点: 1 访问节点: 3 访问节点: 2 访问节点: 4
关键说明
- 访问标记:用
std::unordered_set<int>
存储已访问节点,find()
方法时间复杂度为 O (1),比数组更灵活(节点编号无需连续)。 - 递归边界:当邻接节点均已访问时,递归自动回溯,无需手动处理。
- 注意事项:若图深度超过 1e4(如链状图),会触发栈溢出(C++ 递归栈默认大小较小),需改用迭代实现。
2.2 迭代实现(手动维护栈)
迭代实现通过 std::stack
手动管理待访问节点,避免递归栈溢出,适合深度极大的场景。需注意:邻接节点需逆序压栈,确保与递归访问顺序一致。
代码实现:无向图迭代 DFS
cpp
运行
#include <iostream>
#include <vector>
#include <unordered_set>
#include <stack>
#include <algorithm> // 用于 reverse() 函数
using namespace std;// 迭代 DFS 函数:graph-邻接表,start-起点
void dfsIterative(const vector<vector<int>>& graph, int start) {unordered_set<int> visited; // 访问标记stack<int> nodeStack; // 手动维护的栈nodeStack.push(start); // 起点压栈cout << "迭代 DFS 访问顺序: ";while (!nodeStack.empty()) {// 1. 弹出栈顶节点(当前节点)int current = nodeStack.top();nodeStack.pop();// 2. 若未访问,标记并处理if (visited.find(current) == visited.end()) {visited.insert(current);cout << "访问节点: " << current << " ";// 3. 邻接节点逆序压栈(保证与递归顺序一致)// 原因:栈是 LIFO,逆序后弹出顺序与递归的“正序遍历邻接表”相同vector<int> reversedNeighbors = graph[current];reverse(reversedNeighbors.begin(), reversedNeighbors.end());for (int neighbor : reversedNeighbors) {if (visited.find(neighbor) == visited.end()) {nodeStack.push(neighbor);}}}}
}int main() {// 同递归示例的图结构vector<vector<int>> graph = {{1, 2}, {0, 3}, {0, 4}, {1}, {2}};dfsIterative(graph, 0); // 从节点 0 开始遍历return 0;
}
输出结果
plaintext
迭代 DFS 访问顺序: 访问节点: 0 访问节点: 1 访问节点: 3 访问节点: 2 访问节点: 4
关键说明
- 逆序压栈:通过
reverse()
函数将邻接节点逆序,例如节点 0 的邻接节点[1,2]
逆序为[2,1]
,压栈后弹出顺序为1→2
,与递归一致。 - 栈操作:弹出节点后先判断是否已访问,避免重复处理(因同一节点可能被多次压栈)。
三、DFS 的经典 C++ 应用场景
3.1 二叉树遍历(前序、中序、后序)
树是无环的图,DFS 是二叉树遍历的核心方式。以下以前序遍历为例(根→左→右),用递归实现。
代码实现:二叉树前序 DFS
cpp
运行
#include <iostream>
#include <vector>
using namespace std;// 二叉树节点结构
struct TreeNode {int val;TreeNode* left;TreeNode* right;// 构造函数TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};// 前序遍历:根→左→右
void preorderDFS(TreeNode* root, vector<int>& result) {if (root == nullptr) {return; // 递归边界:空节点返回}// 1. 处理根节点result.push_back(root->val);// 2. 递归左子树preorderDFS(root->left, result);// 3. 递归右子树preorderDFS(root->right, result);
}// 释放二叉树内存(避免内存泄漏)
void freeTree(TreeNode* root) {if (root == nullptr) return;freeTree(root->left);freeTree(root->right);delete root;
}int main() {// 构建二叉树:[1, null, 2, 3]TreeNode* root = new TreeNode(1);root->right = new TreeNode(2);root->right->left = new TreeNode(3);vector<int> result;preorderDFS(root, result);// 输出结果cout << "前序遍历结果: ";for (int val : result) {cout << val << " ";}// 释放内存freeTree(root);return 0;
}
输出结果
plaintext
前序遍历结果: 1 2 3
3.2 子集生成(组合问题)
子集问题的核心是 “选或不选某个元素”,DFS 通过回溯(撤销选择)生成所有子集,避免重复。
代码实现:生成数组 [1,2,3] 的所有子集
cpp
运行
#include <iostream>
#include <vector>
using namespace std;// 递归 DFS:index-当前处理的元素下标,path-当前子集,result-所有子集
void subsetDFS(const vector<int>& nums, int index, vector<int>& path, vector<vector<int>>& result) {// 每次递归都将当前子集加入结果(空集也会被加入)result.push_back(path);// 遍历从 index 开始的元素(避免重复子集,如 [1,2] 和 [2,1])for (int i = index; i < nums.size(); ++i) {// 1. 选择当前元素path.push_back(nums[i]);// 2. 递归处理下一个元素(i+1:避免重复选择同一元素)subsetDFS(nums, i + 1, path, result);// 3. 回溯:撤销选择path.pop_back();}
}// 生成所有子集的入口函数
vector<vector<int>> subsets(const vector<int>& nums) {vector<vector<int>> result;vector<int> path; // 存储当前子集subsetDFS(nums, 0, path, result);return result;
}// 打印所有子集
void printSubsets(const vector<vector<int>>& subsets) {cout << "所有子集: " << endl;for (const auto& subset : subsets) {cout << "[";for (int i = 0; i < subset.size(); ++i) {if (i > 0) cout << ", ";cout << subset[i];}cout << "]" << endl;}
}int main() {vector<int> nums = {1, 2, 3};vector<vector<int>> result = subsets(nums);printSubsets(result);return 0;
}
输出结果
plaintext
所有子集:
[]
[1]
[1, 2]
[1, 2, 3]
[1, 3]
[2]
[2, 3]
[3]
3.3 迷宫求解(寻找任意路径)
迷宫用二维数组表示(0
可走,1
障碍,2
终点),DFS 通过回溯探索四个方向(上、下、左、右),找到从 (0,0)
到终点的任意路径。
代码实现:迷宫求解 DFS
cpp
运行
#include <iostream>
#include <vector>
#include <unordered_set>
using namespace std;// 辅助函数:将 (x,y) 转换为字符串,用于 unordered_set 存储
string getKey(int x, int y) {return to_string(x) + "," + to_string(y);
}// 递归 DFS:maze-迷宫,x/y-当前坐标,path-当前路径,visited-访问标记
bool mazeDFS(const vector<vector<int>>& maze, int x, int y, vector<pair<int, int>>& path, unordered_set<string>& visited) {// 边界条件:1. 越界;2. 障碍;3. 已访问if (x < 0 || x >= maze.size() || y < 0 || y >= maze[0].size() ||maze[x][y] == 1 || visited.count(getKey(x, y))) {return false;}// 标记当前位置为已访问,加入路径visited.insert(getKey(x, y));path.emplace_back(x, y);// 终止条件:到达终点(值为 2)if (maze[x][y] == 2) {return true;}// 探索四个方向:上、下、左、右vector<pair<int, int>> directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};for (const auto& dir : directions) {int newX = x + dir.first;int newY = y + dir.second;// 找到路径则直接返回if (mazeDFS(maze, newX, newY, path, visited)) {return true;}}// 回溯:当前路径走不通,移除当前位置,撤销访问标记path.pop_back();visited.erase(getKey(x, y));return false;
}// 打印路径
void printPath(const vector<pair<int, int>>& path) {cout << "找到路径: ";for (int i = 0; i < path.size(); ++i) {if (i > 0) cout << " → ";cout << "(" << path[i].first << "," << path[i].second << ")";}cout << endl;
}int main() {// 迷宫:[[0,1,0],[0,0,0],[1,1,2]]vector<vector<int>> maze = {{0, 1, 0},{0, 0, 0},{1, 1, 2}};vector<pair<int, int>> path; // 存储路径(坐标对)unordered_set<string> visited; // 访问标记(用字符串标识坐标)if (mazeDFS(maze, 0, 0, path, visited)) {printPath(path);} else {cout << "无有效路径" << endl;}return 0;
}
输出结果
plaintext
找到路径: (0,0) → (1,0) → (1,1) → (1,2) → (2,2)
3.4 拓扑排序(有向无环图 DAG)
拓扑排序对 DAG 节点排序,确保所有有向边 u→v
中 u
在 v
之前。DFS 通过 “后序遍历 + 逆序输出” 实现。
代码实现:DAG 拓扑排序 DFS
cpp
运行
#include <iostream>
#include <vector>
#include <unordered_set>
#include <algorithm> // 用于 reverse()
using namespace std;// 递归 DFS:node-当前节点,graph-DAG,visited-访问标记,result-后序遍历结果
void topoDFS(int node, const vector<vector<int>>& graph, unordered_set<int>& visited, vector<int>& result) {visited.insert(node);// 先遍历所有后续节点(出边指向的节点)for (int neighbor : graph[node]) {if (visited.find(neighbor) == visited.end()) {topoDFS(neighbor, graph, visited, result);}}// 所有后续节点遍历完,加入结果(后序)result.push_back(node);
}// 拓扑排序入口函数
vector<int> topologicalSort(const vector<vector<int>>& graph) {unordered_set<int> visited;vector<int> result;// 遍历所有未访问节点
编辑分享