当前位置: 首页 > news >正文

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;// 遍历所有未访问节点

编辑分享

http://www.dtcms.com/a/430841.html

相关文章:

  • 如何通过企微SCRM实现高效的客户管理与营销策略?
  • 北京网站建设华网天下买送两年wordpress百度地图插件
  • Unity+Blender-03-输出制作Flipbook
  • SpringCloudGateway:像城市交通指挥系统一样的微服务网关
  • 【大模型评估】大模型评估框架 HELM(Holistic Evaluation of Language Models)全解析:原理、工具与实践
  • 做自媒体视频搬运网站网站做友链有行业要求吗
  • Jarvis 算法
  • [Linux基础——Lesson6.编译器gcc/g++以及动静态库的认识]
  • 【ROS2】快速创建一个包
  • Markdown——2.LaTeX数学公式
  • 网站系统建设合同范本网站建设相关技术
  • 做网站准备的资料竞猜世界杯
  • Python 开发工具,最新2025 PyCharm 使用
  • 新公司注册在哪个网站p2p网站建设教程
  • 2008 年真题配套词汇单词笔记(考研真相)
  • 增强版 bash “zsh“
  • 图数据库:基于历史学科的全球历史知识图谱构建,使用Neo4j图数据库实现中国历史与全球历史的关联查询。
  • conda建立虚拟环境,并在jupyter notebook中显示,查看与已安装包是否冲突
  • 美食网站策划书新网站如何让百度收录
  • [创业之路-642]:新质生产力、硬科技与数字经济深度融合
  • 今日分享 浮点数二分
  • DataTool.vip官网入口 - 多平台视频与音频免费下载器工具
  • 文心雕龙:DIFY 工作流驱动的Word自动化生成与规范排版方案
  • asp sql做学生信息网站中国建设银行演示网站
  • windows10 系统添加第二块硬盘(解决硬盘盘符丢失问题)
  • java-代码随想录第48天|739. 每日温度、496.下一个更大元素 I、503.下一个更大元素II
  • 在嘉立创的泰山派上也能运行Easysearch
  • JSP 点击量统计
  • 应用网站如何做外贸网站建设哪里做得好
  • Kubernetes证书管理实战:cert-manager部署与CRD导出