一文吃透 DFS 深度优先搜索:从原理到实战

在算法世界中,深度优先搜索(Depth-First Search,简称 DFS)是一种基础且核心的遍历与搜索算法。它以 “纵向挖掘” 为核心思想,沿着一条路径探索至终点后,回溯至前一节点继续探索其他路径,如同迷宫中沿着一条通道走到头,再折返寻找新通道。这种特性让 DFS 在图论遍历、迷宫求解、组合问题、拓扑排序等场景中广泛应用,也是面试与算法竞赛的高频考点。本文将从原理、实现、优化到实战,带你全面掌握 DFS。

一、DFS 核心原理:纵向探索与回溯

1. 核心思想

DFS 的本质是 “不撞南墙不回头”:从起始节点出发,优先探索当前节点的某一条邻接路径,直至无法继续前进(到达终点或已访问节点),再回溯到上一个未探索完所有邻接节点的节点,重复此过程,直至遍历所有可达节点。

2. 关键概念

  • 访问标记:为避免重复访问节点(陷入死循环),需用数组或哈希表记录节点是否已被访问。
  • 回溯:探索完一条路径后,撤销当前节点的访问标记(若需多次遍历),回到上一节点继续探索其他路径,是 DFS 区别于其他遍历算法的核心。
  • 递归与栈:DFS 可通过递归实现(利用函数调用栈隐式维护遍历路径),也可通过显式栈手动实现,两种方式本质一致。

3. 遍历流程示意图

以无向图 1-2-3-4,1-5 为例,DFS 遍历流程(起始节点为 1):

  1. 访问节点 1,标记为已访问;
  2. 选择邻接节点 2,访问并标记;
  3. 选择节点 2 的邻接节点 3,访问并标记;
  4. 选择节点 3 的邻接节点 4,访问并标记;
  5. 节点 4 无未访问邻接节点,回溯至 3;
  6. 节点 3 无其他未访问邻接节点,回溯至 2;
  7. 节点 2 无其他未访问邻接节点,回溯至 1;
  8. 选择节点 1 的邻接节点 5,访问并标记;
  9. 节点 5 无未访问邻接节点,回溯至 1;
  10. 所有节点遍历完成,流程结束。最终遍历顺序:1→2→3→4→5(路径不唯一,取决于邻接节点的遍历顺序)。

二、DFS基础框架(伪代码)

1. 递归实现(简洁直观)

递归是 DFS 最常用的实现方式,利用函数调用栈自动维护回溯路径,代码简洁易懂,适合大部分场景。

伪代码框架
bool visited[]; void dfs(当前节点 u) { visited[u] = true; 处理当前节点 u; 遍历 u 的所有邻接节点 v: 若 v 未访问,则递归调用 dfs(v); 可选:visited[u] = false; }

2. 栈实现(显式维护路径)

当递归深度过大时(如节点数超过 1e4),会导致栈溢出,此时需用显式栈手动实现 DFS,避免递归栈溢出问题。

伪代码框架
void dfs(起始节点 u) { 初始化栈,将 u 入栈,并标记为已访问; while 栈不为空: 弹出栈顶节点 curr; 处理 curr 节点; 遍历 curr 的所有邻接节点 v: 若 v 未访问,则标记为已访问,入栈; }

三、DFS 基础例题

例题 1:二叉树的最大深度

题目描述:给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

题目图片

题目思路:采用递归式 DFS 遍历二叉树,核心逻辑为:当前节点的最大深度 = 1(当前节点) + 左子树 / 右子树的最大深度(取较大值);递归终止条件为节点为空(深度为 0)。通过递归遍历左右子树,自底向上计算每个节点的深度,最终得到根节点的最大深度。

AC code

#include<bits/stdc++.h> using namespace std; const int MAX_NODE = 1005; struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x):val(x),left(NULL),right(NULL){} }; TreeNode* nodes[MAX_NODE]; int node_cnt=0; TreeNode* build(int val) { nodes[node_cnt++]=new TreeNode(val); return nodes[node_cnt-1]; } int maxDepth(TreeNode* root) { if (root==NULL) return 0; return 1+max(maxDepth(root->left),maxDepth(root->right)); } int main() { TreeNode* root=build(3); root->left=build(9); root->right=build(20); root->right->left=build(15); root->right->right=build(7); cout<<maxDepth(root)<<endl; return 0; }

例题 2:子集

题目描述:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

题目思路:通过 DFS 回溯生成所有子集,核心逻辑为:递归过程中,每一步先将当前路径(已选元素)加入结果集,再遍历剩余元素,选择一个元素加入路径后递归,递归返回后撤销选择(回溯),确保遍历所有组合。用 start 控制遍历起点,避免生成重复子集(如 [1,2] 和 [2,1] 视为同一子集)。

AC code

#include<bits/stdc++.h> using namespace std; const int MAX_N=105; const int MAX_PATH=105; const int MAX_RES=1024; int nums[MAX_N]; int path[MAX_PATH]; int res[MAX_RES][MAX_PATH]; int res_size=0; int path_size=0; int n; void dfs(int start) { for(int i=0;i<path_size;++i) res[res_size][i]=path[i]; res_size++; for(int i=start;i<n;++i) { path[path_size++]=nums[i]; dfs(i+1); path_size--; } } int main() { cin>>n; for(int i=0;i<n;i++) cin>>a[i]; dfs(0); for(int i=0;i<res_size;++i){ for(int j=0;res[i][j]!=0;++j) { cout<<res[i][j]<<" "; } cout<<endl; } return 0; }

例题 3:电话号码的字母组合

题目描述:给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

题目图片

解题思路:用数组存储数字到字母的映射,通过 DFS 回溯生成所有组合:递归参数 index 表示当前处理的数字位置,每一步遍历当前数字对应的所有字母,将字母加入路径后递归处理下一个数字,递归终止条件为处理完所有数字(将当前路径加入结果集),递归返回后撤销字母选择(回溯),遍历所有可能的字母组合。

AC code

#include<bits/stdc++.h> using namespace std; const int MAX_RES=1024; const int MAX_STR=20; char res[MAX_RES][MAX_STR]; int res_size=0; char path[MAX_STR]; int path_len=0; char digits[MAX_STR]; char mapping[10][5]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"}; void dfs(int index) { if (index==strlen(digits)){ path[path_len]='\0'; strcpy(res[res_size++],path); return; } int num=digits[index]-'0'; for (int i=0;mapping[num][i]!='\0';++i){ path[path_len++]=mapping[num][i]; dfs(index+1); path_len--; } } int main() { cin>>digits; dfs(0); for (int i=0;i<res_size;++i){ cout<<res[i]<<endl; } return 0; }

例题 4:8 皇后

题目描述

8 皇后问题研究的是如何将 8 个皇后放置在 8×8 的棋盘上,并且使皇后彼此之间不能相互攻击。给定一个整数 8,返回所有不同的 8 皇后问题的解决方案。每一种解法包含一个明确的 8 皇后问题的棋子放置方案,该方案中 '1' 和 '0' 分别代表了皇后和空位。

#include<bits/stdc++.h> using namespace std; int a[9]; bool b[9],c[17],d[17]; int ans; void print(){ ans++; cout<<"No. "<<ans<<endl; for(int i=1;i<=8;i++){ for(int j=1;j<=8;j++){ if(a[j]==i) cout<<1<<' '; else cout<<0<<' '; } cout<<endl; } } int Dfs(int i){ if(i==9){print(); return 0;} for(int j=1;j<=8;j++){ if(!b[j]&&!c[i-j+7]&&!d[i+j]){ b[j]=1; //占领列 c[i-j+7]=1;//占领两条对角线 d[i+j]=1; a[i]=j;//把皇后的位置存放到a数组里。 Dfs(i+1);//搜索下一行 b[j]=0; //回溯,还原现场 c[i-j+7]=0; d[i+j]=0; } } } int main() { Dfs(1);//从第一行开始放置第一个皇后 }

四、DFS 常见误区与注意事项

  1. 忘记标记访问状态:导致节点重复访问,陷入死循环,最终超时或栈溢出。
  2. 回溯时未撤销状态:如组合问题中未弹出当前元素,导致组合错误。
  3. 递归深度过大:对于深度超过 1e4 的场景(如长链图),递归实现会栈溢出,需改用栈实现。
  4. 未剪枝导致效率低下:在组合、排列等问题中,未剪枝会导致时间复杂度爆炸,无法通过大数据测试。

五、总结

DFS 是一种 “纵向探索 + 回溯” 的搜索算法,核心在于 “不撞南墙不回头”,通过递归或栈实现,配合剪枝优化可大幅提升效率。它不仅是图论遍历的基础,更在组合问题、迷宫求解、拓扑排序等场景中发挥着重要作用,是算法学习中必须掌握的核心知识点。

掌握 DFS 的关键在于:理解回溯的本质、熟练运用访问标记、学会针对性剪枝。多做实战题目,才能真正吃透 DFS 的思想与应用场景,在面试与竞赛中灵活应对。

Read more

SpringBoot详解

文章目录 * 概览 * 与Spring的区别 * 创建SpringBoot项目 * SpringBoot常用注解 * SpringBoot自动配置 * @SpringBootConfiguration * @EnableAutoConfiguration * SpringBoot配置管理 * SpringBoot嵌入式服务器 * SpringBoot测试 概览 SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。SpringBoot提供了一种新的编程范式,可以更加快速便捷地开发Spring项目,在开发过程当中可以专注于应用程序本身的功能开发,而无需在Spring配置上花太大的工夫。 SpringBoot基于Sring4进行设计,继承了原有Spring框架的优秀基因。SpringBoot准确的说并不是一个框架,而是一些类库的集合。maven或者gradle项目导入相应依赖即可使用 SpringBoot,而无需自行管

By Ne0inhk
告别小白!吃透 MySQL 基本查询,看这一篇就够了

告别小白!吃透 MySQL 基本查询,看这一篇就够了

🔥海棠蚀omo:个人主页                 ❄️个人专栏:《初识数据结构》,《C++:从入门到实践》,《Linux:从零基础到实践》,《Linux网络:从不懂到不会》,《MySQL:新手入门指南》                 ✨追光的人,终会光芒万丈 博主简介: 目录 一.Create 1.1替换 二.Retrieve 2.1SELECT列 2.1.1全列查询 2.1.2指定列查询 2.1.3查询字段为表达式 2.1.4为查询结果指定别名 2.1.5结果去重 2.2WHERE条件 2.2.1英语不及格的同学及英语成绩 2.2.2语文成绩在[80,90]分的同学及语文成绩

By Ne0inhk
Flutter 组件 okay 的适配 鸿蒙Harmony 深度进阶 - 驾驭异步结果链式融合、实现鸿蒙端分布式业务逻辑解耦与精密审计方案

Flutter 组件 okay 的适配 鸿蒙Harmony 深度进阶 - 驾驭异步结果链式融合、实现鸿蒙端分布式业务逻辑解耦与精密审计方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 okay 的适配 鸿蒙Harmony 深度进阶 - 驾驭异步结果链式融合、实现鸿蒙端分布式业务逻辑解耦与精密审计方案 前言 在前文中,我们探讨了 okay 在鸿蒙(OpenHarmony)端实现基础 Result 模式包装的实战。但在真正的“分布式微服务聚合”、“高并发资产对账”以及“具备自愈能力的 IoT 指令链”场景中。简单的 ok() 与 err() 判定往往不足以支撑起复杂的业务全景。面对需要同时并行发起 3 个 API 请求,并要求在“所有请求均成功时执行合并、任一请求失败时执行局部逻辑路由”的高阶需求。如果缺乏一套完善的异步结果映射与多级逻辑聚合机制。不仅会导致异步回调地狱(Callback Hell)在

By Ne0inhk
MySQL 大数据处理优化与分布式架构探索

MySQL 大数据处理优化与分布式架构探索

MySQL 大数据处理优化与分布式架构探索 在数据爆炸式增长的时代,MySQL 作为一款流行的开源关系型数据库管理系统,如何在大数据处理场景下保持高效与稳定,成为了众多开发者和数据库管理员关注的焦点。本文将深入探讨 MySQL 大数据处理优化与分布式架构的实现与应用,帮助读者更好地应对高并发和大数据量的挑战。 一、MySQL 大数据处理面临的挑战 随着业务的发展和用户数量的增长,MySQL 数据库面临的数据量急剧增加,这对数据库的性能和扩展性提出了更高要求。传统的单机 MySQL 数据库在处理大规模数据时,往往会遇到性能瓶颈,如查询速度慢、写入压力大、存储能力不足等问题。因此,如何优化 MySQL 大数据处理,成为了一个亟待解决的问题。 二、MySQL 大数据处理优化策略 1. 索引优化 索引是 MySQL 查询优化的关键。合理的索引设计可以显著提高查询速度。在大数据量场景下,应重点关注以下几点: * 选择合适的索引类型:根据查询需求选择合适的索引类型,如主键索引、唯一索引、普通索引、复合索引等。[9] * 避免索引失效:注意查询条件中的数据类型匹配、

By Ne0inhk