深度优先搜索(DFS)详解及C++实现

深度优先搜索(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. 递归实现(最常用)

递归实现的核心逻辑:

  1. 访问当前节点,标记为已访问;
  2. 遍历当前节点的所有邻接节点;
  3. 对每个未访问的邻接节点,递归调用DFS函数。

案例1:无向图的DFS遍历

假设我们有如下无向图:

0 → 1 → 2
0 → 3 → 4

用邻接表存储图(邻接表是图的常用存储方式,适合稀疏图),然后通过递归DFS遍历所有节点。

#include<iostream>#include<vector>usingnamespace std;// 邻接表存储图 vector<vector<int>> adj;// 标记节点是否被访问 vector<bool> visited;// 递归实现DFSvoiddfs(int u){// 标记当前节点为已访问 visited[u]=true;// 访问当前节点(此处为打印节点值) cout << u <<" ";// 遍历当前节点的所有邻接节点for(int v : adj[u]){// 如果邻接节点未被访问,递归调用DFSif(!visited[v]){dfs(v);}}}intmain(){// 图的节点数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;return0;}

输出结果:
DFS遍历结果(递归):0 1 2 3 4

说明:从节点0出发,先深入探索1→2分支,回溯后再探索3→4分支,最终遍历所有节点。

案例2:二叉树的前序遍历(DFS)

二叉树的前序遍历(根→左→右)是DFS的典型应用,递归实现非常简洁。

#include<iostream>usingnamespace std;// 二叉树节点定义structTreeNode{int val; TreeNode* left; TreeNode* right;TreeNode(int x):val(x),left(nullptr),right(nullptr){}};// 递归实现前序遍历(DFS)voidpreorderDFS(TreeNode* root){// 递归终止条件:节点为空if(root ==nullptr){return;}// 访问根节点 cout << root->val <<" ";// 递归遍历左子树preorderDFS(root->left);// 递归遍历右子树preorderDFS(root->right);}intmain(){// 构建一棵二叉树:// 1// \ // 2// /// 3 TreeNode* root =newTreeNode(1); root->right =newTreeNode(2); root->right->left =newTreeNode(3); cout <<"二叉树前序遍历(DFS):";preorderDFS(root); cout << endl;// 释放内存(简化处理,实际应手动遍历释放)delete root->right->left;delete root->right;delete root;return0;}

输出结果:
二叉树前序遍历(DFS):1 2 3

2. 非递归实现(手动栈)

当递归深度过大时(如遍历深度为1e4的树),会导致栈溢出(C++默认递归栈大小有限),此时需要用手动栈实现DFS。核心逻辑与递归一致,只是将递归调用栈替换为手动维护的栈。

非递归实现步骤:

  1. 将起始节点压入栈,标记为已访问;
  2. 弹出栈顶节点,访问该节点;
  3. 将该节点的所有未访问邻接节点压入栈(注意:为保证遍历顺序与递归一致,需逆序压入,因为栈是先进后出);
  4. 重复步骤2-3,直到栈为空。

案例1:无向图的DFS遍历(非递归)

#include<iostream>#include<vector>#include<stack>usingnamespace std; vector<vector<int>> adj; vector<bool> visited;// 非递归实现DFS(手动栈)voiddfsNonRecursive(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);}}}}intmain(){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;return0;}

输出结果:
DFS遍历结果(非递归):0 1 2 3 4

说明:这里对邻接节点逆序遍历(rbegin()和rend()),是因为栈是“先进后出”的。如果直接正序压入,遍历顺序会变成0→3→4→1→2,虽然也是DFS,但与递归实现的顺序不一致(不影响遍历完整性,仅影响顺序)。

案例2:二叉树的前序遍历(非递归DFS)

#include<iostream>#include<stack>usingnamespace std;structTreeNode{int val; TreeNode* left; TreeNode* right;TreeNode(int x):val(x),left(nullptr),right(nullptr){}};// 非递归实现前序遍历(DFS)voidpreorderDFSNonRecursive(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);}}}intmain(){// 构建与递归案例相同的二叉树 TreeNode* root =newTreeNode(1); root->right =newTreeNode(2); root->right->left =newTreeNode(3); cout <<"二叉树前序遍历(非递归DFS):";preorderDFSNonRecursive(root); cout << endl;// 释放内存delete root->right->left;delete root->right;delete root;return0;}

输出结果:
二叉树前序遍历(非递归DFS):1 2 3

四、DFS的经典应用:回溯法求解N皇后问题

DFS的核心是“探索-回溯”,而回溯法是DFS的一种延伸应用,常用于解决“选择-验证-回溯-再选择”的组合优化问题。N皇后问题是回溯法的经典案例:在n×n的棋盘上放置n个皇后,使得任意两个皇后不处于同一行、同一列或同一斜线上。

#include<iostream>#include<vector>#include<string>usingnamespace std; vector<vector<string>> result;// 存储所有合法解// 检查当前位置(row, col)是否可以放置皇后boolisValid(int n,int row,int col, vector<string>& board){// 检查同一列是否有皇后for(int i =0; i < row;++i){if(board[i][col]=='Q'){returnfalse;}}// 检查左上到右下的斜线(左上方向)for(int i = row -1, j = col -1; i >=0&& j >=0;--i,--j){if(board[i][j]=='Q'){returnfalse;}}// 检查右上到左下的斜线(右上方向)for(int i = row -1, j = col +1; i >=0&& j < n;--i,++j){if(board[i][j]=='Q'){returnfalse;}}returntrue;}// DFS回溯函数:当前处理第row行voidbacktrack(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;}// 打印所有解voidprintResult(const vector<vector<string>&> result){for(auto& solution : result){ cout <<"====================="<< endl;for(auto& row : solution){ cout << row << endl;}}}intmain(){int n =4;// 求解4皇后问题 vector<vector<string>> solutions =solveNQueens(n); cout << n <<"皇后问题共有 "<< solutions.size()<<" 个解:"<< endl;printResult(solutions);return0;}

输出结果(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的关键是理解“探索-回溯”的思想,以及如何通过标记和撤销操作控制遍历过程。

Read more

C++手撕红黑树:从0到200行,拿下STL map底层核心

C++手撕红黑树:从0到200行,拿下STL map底层核心

文章目录 * C++手撕红黑树:从0到200行,拿下STL map底层核心 * 1. 红黑树的概念 * 1.1 红黑树的规则 * 1.2 红黑树如何确保最长路径不超过最短路径的2倍? * 1.3 红黑树的效率 * 2. 红黑树的实现 * 2.1 红黑树的结构 * 2.2 红黑树的插入 * 2.2.1 插入的大概过程 * 2.2.2 情况1:变色 * 2.2.3 情况2:单旋 + 变色 * 2.2.4 情况3:双旋 + 变色 * 2.3 红黑树的插入代码实现 * 2.

By Ne0inhk
C++学习之旅【C++伸展树介绍以及红黑树的实现】

C++学习之旅【C++伸展树介绍以及红黑树的实现】

🔥承渊政道:个人主页 ❄️个人专栏: 《C语言基础语法知识》《数据结构与算法》 《C++知识内容》《Linux系统知识》 ✨逆境不吐心中苦,顺境不忘来时路!🎬 博主简介: 引言:前篇文章,小编已经介绍了关于C++AVL树的实现!相信大家应该有所收获!接下来我将带领大家继续深入学习C++的相关内容!本篇文章着重介绍关于C++伸展树介绍以及红黑树的实现!伸展树与红黑树是两类极具代表性的BBST,且在工程实践中各有不可替代的价值:伸展树摒弃了"严格平衡”的执念,通过“伸展”操作将最近访问的节点移至根节点,利用“局部性原理”优化频繁访问的场景,实现均摊O(logn)的时间复杂度,适合缓存、热点数据查询等场景;红黑树则通过给节点着色并遵守严格的颜色规则,确保树的最长路径不超过最短路径的两倍,以 “弱平衡” 换稳定的最坏O(logn)性能,是C++ STL 中 std::map、std:

By Ne0inhk
C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板

C++ 模板进阶:特化、萃取与可变参数模板 💡 学习目标:掌握模板进阶技术的核心用法,理解模板特化的深层应用、类型萃取的实现原理,以及可变参数模板的灵活使用,提升泛型编程的实战能力。 💡 学习重点:模板特化的进阶场景、类型萃取工具的设计与应用、可变参数模板的展开技巧、折叠表达式的使用方法。 一、模板特化进阶:处理复杂类型场景 💡 模板特化不只是针对单一类型的定制,还能处理指针、引用、数组等复杂类型,实现更精细的类型适配逻辑。 1.1 指针类型的模板特化 通用模板默认处理普通类型,我们可以为指针类型单独编写特化版本,实现指针专属的逻辑。 #include<iostream>#include<string>usingnamespace std;// 通用模板:处理普通类型template<typenameT>classTypeProcessor{public:staticvoidprocess(T data){ cout

By Ne0inhk

C++ 设计模式概述及常用模式

C++ 设计模式概述 本文介绍了C++中23种设计模式的分类及实现示例,主要分为三大类: 创建型模式(5个):单例模式(常用)、工厂方法模式(常用)、抽象工厂模式(常用)、建造者模式和原型模式。这些模式专注于对象的创建机制。 结构型模式(7个):适配器模式(常用)、桥接模式、组合模式和装饰器模式(常用)等。这些模式处理类和对象的组合方式。 行为型模式:未完整列出,但包含观察者模式等(未展示完整代码)。 文章通过简洁的C++代码示例展示了常用设计模式的实现方法,如单例模式通过私有构造函数和静态方法确保唯一实例,工厂方法模式通过抽象工厂类创建产品等。这些模式为解决特定设计问题提供了可重用的解决方案。 C++ 设计模式概述及常用模式 设计模式可分为三大类:创建型、结构型、行为型。以下是23个设计模式的分类及代码示例: 一、创建型模式(5个) 1. 单例模式(Singleton)⭐ 常用 classSingleton{private:static

By Ne0inhk