DFS算法篇:理解递归,熟悉递归,成为递归

在这里插入图片描述

1.DFS原理

那么dfs就是大家熟知的一个深度优先搜索,那么听起来很高大尚的一个名字,但是实际上dfs的本质就是一个递归,而且是一个带路径的递归,那么递归大家一定很熟悉了,大学c语言课程里面就介绍过递归,我们知道就是定义一个函数,然后自己调用自己

那么我们要理解DFS,那么其实就是要理解递归,那么要理解好掌握好递归,那么我们就得明白递归的一个过程,那么递归的一个过程无非就两个字:

”递“和”归“

那么所谓的“递”,那么我们知道要实现递归,就是定义一个函数,然后在该函数内部再调用自己,那么我们知道当执行这个函数体的内的语句的时候,一旦执行到调用该函数本身的语句时,那么我们调用该函数就会为该函数在栈上开辟一份空间,那么开辟的这个空间我们有一个专有名词也就是栈帧来称呼,那么我们会在栈帧中存放该函数体内部所定义的各种局部变量,那么每次调用一个函数我们就会为函数创建一份栈帧,那么这里我们在函数体内部调用了该函数本身,那么这种套娃的行为,程序会为我们调用的该函数又开辟一份空间,只不过这个函数就是本身,然后我们就进入到该函数内部,又重新从函数体开头执行一行一行语句,然后又执行到调用自己的函数语句,又重复我们上述的行为,那么这整个过程,从第一个函数到之后调用自己的函数的一个个过程中,这里就是一个“递”过程,那么我们可以理解第一个函数一直到调用的最后一个函数的关系我们如果用一个层级关系来描述想象的话,那么我们从第一个函数到最后一个函数就是不断往下探索深入的过程,这就是为什么我们要取名为“深度”优先搜索,这里的deep是不是就很形象的描述了递归的一个”递“过程
在这里插入图片描述

而所谓的“归”,那么我们知道我们函数在自己调用自己的时候,我们一定会设置一个终止条件,如果不设置终止条件,那函数会无限的调用自己,反复套娃,而我们刚才说了调用函数会在栈上开辟空间,如果我们反复调用,那么最终会对空间的损耗极其大,所以一旦设置好终止条件之后,也就是我们在函数体内定义的一个return语句,那么我们就会回溯,那么我们上文说了,我们从第一个函数到调用自己创建的最后一个函数的过程,我们可以用层状关系来理解,那么我们结束该调用的函数,那么也就会返回调用我们该函数的上一级函数当中去,那么同理,我们上一级函数调用结束返回也是到调用它的上一级函数当中去,所以我们就这样从底层一直会回溯到顶层的第一个函数,那么第一个函数结束就是回到调用它的main主函数当中去了,那么这就是我们的归过程.


那么在我们知道了我们递归的一个过程,那么我们在理解递归的时候,如果想不通的话就可以通过画图,然后梳理函数之间的调用关系,得到一个层级的图

那么在我们上文中,我描述“递”与“归”过程中举了一个函数自己调用自己的例子来讲解,那么该函数之间的关系就是一个层级关系,但是一旦我们将我们这个例子给修改,也就是我们在一个函数体内部调用我们函数本身的语句不只有一个,而是有多个,那么这种场景就是我们的dfs的情况了

那么此时我们要理解这种场景的话,我们就可以不能用层状关系来描述了,而得是通过树状关系来描述,没错,这个树就是我们学的数据结构的那个树,那么该函数体内有着多个函数调用语句,那么我们就以该函数作为节点,那么其下有多个分支,多个子节点,那么这个子节点就是调用的函数,那么我们执行该递归函数的过程,那么我们可以将其形象的转换为遍历这棵二叉树,假设我们在函数体内部调用该函数本身两次,那么我们执行该递归函数的过程就是从顶部到底部的一个二叉树的前序遍历,也就是先遍历左子树再遍历右子树,而如果我们在该函数体内定义了多个调用函数语句,那么该递归的函数的过程就是遍历一棵多叉树,也是先遍历左子树然后再遍历右侧的子树
那么接下来我有一个问题,读者可以自行思考一下:
那么为什么要我们该递归函数的执行流程转换成多叉树的遍历是前序遍历也就是先遍历左子树再遍历右子树而不是中序或者后序遍历呢?

那是因为我们的递归函数调用,一旦我们执行到该函数调用语句之后,我们会该调用的函数创建一份新的空间,然后就直接进入到该调用函数体的内部,而不会往下执行该调用函数之后的语句,那么也就是在二叉树中最左侧的孩子就是先被调用被创建执行的函数,而右边的函数还没有被调用,也就还没创建出它的空间,所以我们递归的执行只能是一个前序遍历的一个过程
在这里插入图片描述

那么在此模型下,也就是多叉树的遍历模型,我们在加入一个元素那就是我们的DFS了,也就是记录路径信息,那么什么是记录路径信息呢?

我们直到我们DFS的模型就是遍历一棵多叉树,那么该多叉树的第一个节点也就是根节点就是我们的第一个函数,而底部的叶子节点就是我们调用的最后一个函数,那么我们从顶部到达底部的这个路径中,我们沿途通过一个数组或者表等结构来记录我们沿途遍历的各个节点的内容,那么这就是我们的DFS,也就是我开篇所提到的带路径的递归

而不带路径的递归则是我们不记录沿途的各个节点的内容,只是接受节点往上回溯的返回值,例如斐波那契数列,那么这个不带路径的递归,就是我们的动态规划,那么动态规划也可谓是我们算法的一个重量级人物,那么我在后文会更新该内容。

那么彻底知道了我们DFS的一个原理以及模型,那么我们DFS的应用题目就是需要暴力枚举的题目,而大家经常听到的蓝桥杯的暴力方法,也就是本文的主角DFS,

那么我们接下来就看怎么用我们这DFS,那么我在下文准备了几道题目

2.应用

题目一:字符串的全部子序列 难度:EZ

题目:现在我们有一个字符串a,我们要得到该字符串的全部子序列。

例:abc的全部子序列有:"",“a”,“b”,“c”,“ab”,“ac”,“bc”,“abc”

题目思路:那么这里我们要得到该字符串的全部子序列,我们怎么得到我们一个字符串的全部子序列呢,我们就是可以通过枚举每一个位置的两种可能性,也就是该位置的字符在我们的子序列中存在还是不存在,那么我们的一个子序列的确定就是枚举每种位置的这两种可能性然后组合就可以得到我们的子序列,那么我们的枚举过程可以用一个决策树来描述,那么我们每一个位置就是该决策树的一个节点,那么该节点就有两个分支,分别对应我们该位置是否存在在子序列当中还是不存在在子序列当中,那么在每一个分支下有同理也有着两个分支,那么我们这棵决策树就是一棵完全展开的二叉树,那么我们从二叉树的顶部也就是根节点到底部的叶子节点的每一个路径就是我们一个子序列,那么我们就从根节点往下遍历,那么在遍历的过程中同时用一个变量来记录我们遍历的节点的内容,那么一旦到达底部,我们从根节点沿途记录的内容就是我们的其中一个答案

所以只要理解了DFS的一个多叉树遍历模型,那么这题的理解以及DFS的递归的实现就很轻松

代码实现:

class solution
{public:void dfs(string& a,vector<string>& ans,string temp,int i){if(i==a.size()){ans.push_back(temp);return;}dfs(a,ans,temp+a[i],i+1)//当前字符存在在子序列中dfs(a,ans,temp,i+1);//当前字符不存在在子序列当中return;}void allsubstring(string& a,vector<string>& ans){string temp;dfs(a,ans,temp,0);return;}
}

题目二:字符串的全部不重复的子序列 难度:EZ

题目:现在我们有一个字符串a,我们要求该字符串的全部且不重复的子序列

例:abcc的全部不重复子序列:"",“a”,“b”,“c”,“ab”,“ac”,“bc”,"“cc”,“abc”,“abcc”

这里的c以及abc会有重复

题目思路:那么这个题我们就得在上一个题的思路下进行一个优化,那么我们知道我们上一个题就是暴力枚举出所有的全部子序列,那么我们这个枚举的过程就是一棵二叉树,我们从顶部到底部进行遍历,而这里我们只不过在添加一个哈希表的结构,然后我们会将从顶部到底部的沿途的所有节点的记录加入到哈希表中去重即可.

代码实现:

class solution
{public:void dfs(string& a,vector<string>& ans,string temp,int i,unordered_map<string,int>& value){if(i==a.size()){if(value.find(temp)==value.end()){ans.push_back(temp);value[temp]++;}return ;}dfs(a,ans,temp+a[i],i+1)dfs(a,ans,temp,i+1);return;}void allsubstring(string& a,vector<string>& ans){string temp;unordered_map<string,int> value;dfs(a,ans,temp,0,value);return;}
}

题目三:子集 难度:MID

题目:现在有一个数组,那么数组中每个元素可以组成一个集合,请返回所有且不重复的集合。

例:[1,2,3,4]的子集有:[1],[2],[3],[4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4],[1,2,3],[1,2,4],

[1,3,4],[2,3,4],[1,2,3,4]

注意:[1,2,3]和[3,2,1]以及[2,1,3]是一个集合

题目思路:那么这里我们解决该题的一个思路还是一个枚举,那么我们按照每个位置是否在这个集合中存在来作为枚举,那么每一个位置都有两种可能,也就是存在在这个集合当中或者不存在在这个集合当中,那么这里我们就枚举每一个位置的可能性,然后每个位置的可能性所组合就是我们的子集了,那么我们枚举的过程就是一棵决策树,那么在当前每个位置的当前所做的决策就是二叉树的一个节点,那么我们现在有两种决策,要么存在在集合中要么不存在在集合中,那么在二叉树的该节点中就对应就会有两个分支,那么同理,这两个分支下有对应着两个分支,那么我们从顶部到达底部的路径的沿途的各个节点组合就是我们的子集,那么我们就遍历我们这棵二叉树,然后我们定义一个变量来记录沿途的路径上的节点内容即可。

class solution
{public:void dfs(vector<int>& a,vector<vector<int>>& ans,int index,vector<int>& current,unordered_map<string,int>& value){if(index==a.size()){for(int i=0;i<current.size();i++){a+=to_string(current[i]);}if(value.find(a)==value.end()){ans.push_back(current);value[a]++;}return;}current.push_back(a[index]);dfs(a,ans,index+1,current);current.pop_back();dfs(a,ans,index+1,current);}void all(vector<int>& a,vector<vector<int>>& ans){vector<int> current;unordered_map<string,int> value;dfs(a,ans,0,current,value);return;}
}

但是我们可以优化一下,因为我们知道这题存在重复的子集,那么所谓的重复的子集就是集合的各个值的数量一样,但是顺序不一样,那么就是重复的子集,那么我们就可以按照集合的特定的值的数量来进行一个枚举,那么首先我们可以先对数组进行一个排序,这样相同大小的数会在相邻位置,然后我们按照这样的方式来枚举,那么我们枚举集合中值为k的数在集合的数量为0的子集,然后再枚举为值为k数量为1的子集有几个,那么同理对于其他值也是这样,那么我们将每个值在当前集合的数量的可能给组合就是我们的子集,那么按照这样枚举的话我们就能够优化很多重复子集的枚举,实现剪枝

而所谓的剪枝就跟园丁修建树木的花花草草一样,我们这里的剪枝则是要修剪掉重复计算过的分支,这样减少树的遍历从而优化时间复杂度,并且也优化了空间复杂度因为减少了递归函数的调用从而不用为函数创建栈帧,而在上一个题中,我们用哈希表的方式来去重注意那个不是剪枝,我们还是会从树的顶部走到底部,然后将记录的结果加进哈希表中去重,树的分支其实并没有减少

代码实现:

class Solution {
public:void dfs(const vector<int>& nums, vector<vector<int>>& subsets, vector<int>& current, int index) {if(index==nums.size()){ans.push_back(current);return;}int i=index+1;while(i<nums.size()&&nums[i]==nums[i-1]){i++;}for(int j=0;j<i-index;j++){current.push_back(nums[index]);dfs(nums,subsets.current,i);}}vector<vector<int>> subsets(vector<int>& nums) {vector<vector<int>> result;vector<int> current;sort(nums.begin(), nums.end());  // 对数组进行排序dfs(nums, result, current, 0);return result;}
};

题目四:全排列 难度:MID

题目:那么现在有一个数组,我们要得到该数组的所有全排列

例:[1,2,3]的全排列有[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,2,1],[3,1,2]

题目思路:那么这里我们要枚举我们的所有全排列,那么这里我们的枚举思路就是我们假设我们的数组长度为n,那么意味着有n个数,那么我们分别来枚举每一个位置的数的情况,比如第一个位置有n种情况,那么第二个位置有n-1种情况,以此类推,那么我们每个位置的情况组合就得到了我们的全排列,那么我们这个枚举过程就是每一个数的决策就是该多叉树的一个节点,那么我们其中从顶部到底部的各个路径就是我们其中一个全排列,那么我们定义一个变量来记录我们从顶部到达底部的路径沿途的各个节点内容,然后该记录就是我们的一个答案

代码实现:

class solution
{public:void dfs(vector<int>& a,vector<vector<int>>& ans,vetcor<int>& temp,vector<int>& check){if(temp.size()==a.size()){ans.push_back(temp);return;}for(int i=0;i<a.size();i++){if(check[i]==0){check[i]=1;temp.push_back(a[i]);dfs(a,ans,temp,check);check[i]=0;temp.pop_back();}}}void all(vector<int>& a,vector<vector<int>>& ans){vector<int> temp;vector<int> check(a.size(),0);dfs(a,ans,temp,check);return;}
}

3.结语

那么这就是我们DFS的全部内容,那么我们DFS的应用场景就是那些需要我们去暴力枚举尝试的题目,那么每一次枚举也就是每一次所做的要做的决策就是我们树当中的节点,而其中枚举的可能性就是这个节点所对应的分支,那么我们就沿途遍历这些所有的从顶部到达底部的所有分支,并将沿途的节点内容给记录即可,那么这就是我们的DFS,本质上就是带路径的递归,那么要掌握DFS只要熟悉我们的递归,其实DFS的算法原理与思想其实很简单,挺无脑的,并且知道了DFS的算法模型就是一棵多叉树的话,那么通过递归来实现的难度就减小很多

那么最后感谢你能够耐心的看完本文,那么希望本文能够让你有所收获,我会持续更新,希望你能够多多关注,那么如果我的文章有帮组到你,那还请你多多三连支持哦,你的支持就是我创作的最大的动力!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/70125.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HepG2细胞复苏实验以及六孔板种植细胞实验

一、细胞复苏实验&#xff1a; 首先先用紫外照射复苏细胞的新培养皿&#xff0c;然后预热要用到的1640培养基&#xff08;控制在30mins以内&#xff0c;否则会发生蛋白质结构转变等&#xff09;&#xff0c;等待培养基预热完毕后。取出冻存的HepG2细胞&#xff0c;手拿头部在水…

springboot021-基于协同过滤算法的个性化音乐推荐系统

&#x1f495;&#x1f495;作者&#xff1a; 小九学姐 &#x1f495;&#x1f495;个人简介&#xff1a;十年Java&#xff0c;Python美女程序员一枚&#xff0c;精通计算机专业前后端各类框架。 &#x1f495;&#x1f495;各类成品Java毕设 。javaweb&#xff0c;ssm&#xf…

Vue 3最新组件解析与实践指南:提升开发效率的利器

目录 引言 一、Vue 3核心组件特性解析 1. Composition API与组件逻辑复用 2. 内置组件与生命周期优化 3. 新一代UI组件库推荐 二、高级组件开发技巧 1. 插件化架构设计 2. 跨层级组件通信 三、性能优化实战 1. 惰性计算与缓存策略 2. 虚拟滚动与列表优化 3. Tree S…

github用户名密码登陆失效了

问题&#xff1a; git push突然推代码需要登陆&#xff0c;但是用户名和密码正确输入后&#xff0c;却提示403 git push# Username for https://github.com: **** #Password for https://gyp-programmergithub.com: #remote: Permission to gyp-programmer/my-app.git denie…

js考核第三题

题三&#xff1a;随机点名 要求&#xff1a; 分为上下两个部分&#xff0c;上方为显示区域&#xff0c;下方为控制区域。显示区域显示五十位群成员的学号和姓名&#xff0c;控制区域由开始和结束两个按钮 组成。点击开始按钮&#xff0c;显示区域里的内容开始滚动&#xff0c;…

如何在Flask中处理静态文件

哈喽,大家好,我是木头左! 本文将详细介绍如何在Flask中处理静态文件,包括如何配置静态文件夹、如何访问静态文件以及如何处理静态文件的缓存问题。 配置静态文件夹 在Flask中,你可以通过static_folder参数来指定静态文件夹。默认情况下,Flask会在项目的根目录下寻找名为…

Deep seek学习日记1

Deepseek最强大的就是它的深度思考&#xff0c;并且展现了它的思考过程。 五种可使用Deep seek的方式&#xff08;应该不限于这五种&#xff0c;后续嵌入deepseek的应该更多&#xff0c;多了解一点因为官网容易崩~~&#xff09;&#xff1a; 1.deep seek官网 2.硅基流动silicon…

电子电气架构 --- OEM对软件自研的期待

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 简单,单纯,喜欢独处,独来独往,不易合同频过着接地气的生活,除了生存温饱问题之外,没有什么过多的欲望,表面看起来很高冷,内心热情,如果你身…

grep如何排除多个目录?

在使用 grep 进行文本搜索时&#xff0c;有时候需要排除多个目录&#xff0c;避免在这些目录下进行搜索。下面介绍几种不同的实现方式。 目录 1.使用 -r 和 --exclude-dir 选项&#xff08;GNU grep&#xff09; 2.使用扩展正则表达式和 -P 选项&#xff08;GNU grep&#x…

deepseek_各个版本django特性

以下是 Django 2.0 至 5.0 的主要区别总结&#xff0c;按版本特性分类说明&#xff1a; 1. Django 2.0 的主要变化 Python 支持 仅支持 Python 3.4&#xff0c;不再兼容 Python 2.x。路由系统 弃用 url()&#xff0c;引入 path() 和 re_path() 替代&#xff0c;path() 默认不支…

微信小程序中缓存数据全方位解惑

微信小程序中缓存数据全方位解惑 微信小程序中的数据缓存是提升用户体验和优化性能的重要手段&#xff0c;跟电脑浏览器中的Local Storage的性质一样。以下是关于微信小程序数据缓存的相关知识点和示例的详细介绍&#xff1a; 1. 数据缓存的类型 微信小程序提供了两种数据缓…

IP 路由基础 | 路由条目生成 / 路由表内信息获取

注&#xff1a;本文为 “IP 路由” 相关文章合辑。 未整理去重。 IP 路由基础 秦同学学学已于 2022-04-09 18:44:20 修改 一. IP 路由产生背景 我们都知道 IP 地址可以标识网络中的一个节点&#xff0c;并且每个 IP 地址都有自己的网段&#xff0c;各个网段并不相同&#xf…

kong身份认证插件详解之Basic Auth插件

1.3、Basic Authentication 支持基于用户名和密码的基本认证&#xff0c;通常用于简单的身份验证场景。 1.3.1、环境准备 1.3.1.1、创建一个服务&#xff0c;basic-auth-service curl -i -s -X POST http://localhost:8001/services \--data namebasic-auth-service \--dat…

计算机性能与网络体系结构探讨 —— 基于《计算机网络》谢希仁第八版

(꒪ꇴ꒪ )&#xff0c;Hello我是祐言QAQ我的博客主页&#xff1a;C/C语言&#xff0c;数据结构&#xff0c;Linux基础&#xff0c;ARM开发板&#xff0c;网络编程等领域UP&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff0c;让我们成为一个强大的攻城狮&#xff0…

spring boot知识点1

1.什么是spring boot spring boot是spring框架的子项目&#xff0c;主要特点是自动配置&#xff0c;以及内置的tomcat服务器&#xff0c;适合快速开发web与微服务架构 2.spring boot和spring cloud俩者之间的联系 spring boot可单独运行&#xff0c; spring cloud则是用于多…

thingboard告警信息格式美化

原始报警json内容&#xff1a; { "severity": "CRITICAL","acknowledged": false,"cleared": false,"assigneeId": null,"startTs": 1739801102349,"endTs": 1739801102349,"ackTs": 0,&quo…

【个人开发】deepspeed+Llama-factory 本地数据多卡Lora微调

文章目录 1.背景2.微调方式2.1 关键环境版本信息2.2 步骤2.2.1 下载llama-factory2.2.2 准备数据集2.2.3 微调模式2.2.3.1 zero-3微调2.2.3.2 zero-2微调2.2.3.3 单卡Lora微调 2.3 踩坑经验2.3.1 问题一&#xff1a;ValueError: Undefined dataset xxxx in dataset_info.json.2…

Python函数的函数名250217

函数名其实就是一个变量&#xff0c;这个变量就是代指函数而已函数也可以被哈希&#xff0c;所以函数名也可以当作集合中的元素&#xff0c;也可作为字典的key值 # 将函数作为字典中的值&#xff0c;可以避免写大量的if...else语句 def fun1():return 123 def fun2():return 4…

apache artemis安装

安装apache artemis https://xxzkid.github.io/2025/apache-artemis-install

VoIP之音视频会议中的混音技术

在VoIP音视频会议中&#xff0c;需要将多路参会方音频流混合成一路音频流再发送给各参会方&#xff0c;以达到参会方可以听到每个与会人声音的目的&#xff0c;这种技术叫混音。 一、混音基础原理 在实际生活中&#xff0c;我们所处的生活和工作环境就是一个自然的混音场&…