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的算法模型就是一棵多叉树的话,那么通过递归来实现的难度就减小很多

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

H5自适应响应式代理记账与财政咨询服务类PbootCMS网站模板 – HTML5财务会计类网站源码下载

(H5自适应)响应式代理记账财政咨询服务类pbootcms网站模板 html5财务会计类网站源码下载 为了提升系统安全&#xff0c;请将后台文件admin.php的文件名修改一下。修改之后&#xff0c;后台登录地址就是&#xff1a;您的域名/您修改的文件名.php 模板特点&#xff1a; 1&#x…

嵌入式音视频开发(二)ffmpeg音视频同步

系列文章目录 嵌入式音视频开发&#xff08;零&#xff09;移植ffmpeg及推流测试 嵌入式音视频开发&#xff08;一&#xff09;ffmpeg框架及内核解析 嵌入式音视频开发&#xff08;二&#xff09;ffmpeg音视频同步 嵌入式音视频开发&#xff08;三&#xff09;直播协议及编码器…

工业自动化丨工业控制系统五层架构以及PLC、SCADA系统、DCS系统,从零基础到精通,收藏这篇就够了!

工业控制系统通常是几种类型控制系统的总称&#xff0c;包括监控和数据采集&#xff08;SCADA&#xff09;系统、分布式控制系统&#xff08;DCS&#xff09;和可编程逻辑控制器&#xff08;PLC&#xff09;以及其它控制系统。 【右下角**点赞、**转发、在看&#xff0c;为企业…

✨1.HTML、CSS 和 JavaScript 是什么?

✨✨ HTML、CSS 和 JavaScript 是构建网页的三大核心技术&#xff0c;它们相互协作&#xff0c;让网页呈现出丰富的内容、精美的样式和交互功能。以下为你详细介绍&#xff1a; &#x1f98b;1. HTML&#xff08;超文本标记语言&#xff09; 定义&#xff1a;HTML 是一种用于描…

MySQL基本操作——包含增删查改(环境为Ubuntu20.04,MySQL5.7.42)

1.库的操作 1.1 创建数据库 语法&#xff1a; 说明&#xff1a; 大写的表示关键字 [] 是可选项 CHARACTER SET: 指定数据库采用的字符集 COLLATE: 指定数据库字符集的校验规则 1.2 创建案例 创建一个使用utf8字符集的db1数据库 create database db1 charsetutf8; …

【项目】基于STM32F103C8T6的四足爬行机器人设计与实现(源码工程)

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;专__注&#x1f448;&#xff1a;专注主流机器人、人工智能等相关领域的开发、测试技术。 【项目】基于STM32F103C8T6的四足爬行机器人设计与…

IIS asp.net权限不足

检查应用程序池的权限 IIS 应用程序池默认使用一个低权限账户&#xff08;如 IIS_IUSRS&#xff09;&#xff0c;这可能导致无法删除某些文件或目录。可以通过以下方式提升权限&#xff1a; 方法 1&#xff1a;修改应用程序池的标识 打开 IIS 管理器。 在左侧导航树中&#x…

【数据结构】队列(Queue)

Queue 定义 Java中的队列(Queue)是一种先进先出(FIFO)的数据结构。队列只允许在一段进行插入数据操作&#xff0c;称为入队&#xff0c;在另一端进行删除数据操作&#xff0c;称为出队。我们可以把队列形象看作为排队。在最前面的进行出队&#xff0c;从最后面进行入队。 队列…

从零搭建微服务项目Base(第5章——SpringBoot项目LogBack日志配置+Feign使用)

前言&#xff1a; 本章主要在原有项目上添加了日志配置&#xff0c;对SpringBoot默认的logback的配置进行了自定义修改&#xff0c;并详细阐述了xml文件配置要点&#xff08;只对日志配置感兴趣的小伙伴可选择直接跳到第三节&#xff09;&#xff0c;并使用Feign代替原有RestT…

Linux 网络安全技巧

网络安全是一个非常重要的课题,基本上你运行的服务后台越多,你就可能打开更多的安全漏洞.如果配置的恰当的话,Linux本身是非常安全可靠的,假使在Linux系统中有某个安全缺陷,由于Linux的源码是开放的&#xff0c;有成千上万的志愿者会立刻发现并修补它。本文旨在介绍用来增强你的…

Next.js【详解】获取数据(访问接口)

Next.js 中分为 服务端组件 和 客户端组件&#xff0c;内置的获取数据各不相同 服务端组件 方式1 – 使用 fetch export default async function Page() {const data await fetch(https://api.vercel.app/blog)const posts await data.json()return (<ul>{posts.map((…

个人shell脚本分享

在周一到周五做增量备份&#xff0c;在周六周日做完全备份 #!/bin/bash定义变量 SRC“/path/to/source” # 源目录 BKUP“/backup” # 备份主目录 FUL“KaTeX parse error: Expected EOF, got # at position 22: …ull" #̲ 完全备份目录 INC"BKUP/inc” # 增量备份…

小胡说技书博客分类(部分目录):服务治理、数据治理与安全治理对比表格

文章目录 一、对比表格二、目录2.1 服务2.2 数据2.3 安全 一、对比表格 下表从多个维度对服务治理、数据治理和安全治理进行详细对比&#xff0c;为读者提供一个直观而全面的参考框架。 维度服务治理数据治理安全治理定义对软件开发全流程、应用交付及API和接口管理进行规范化…

冒险岛079 V8 整合版源码搭建教程+IDEA启动

今天教大家来部署下一款超级怀旧游戏冒险岛&#xff0c;冒险岛源码是开源的&#xff0c;但是开源的代码会有各种&#xff0c;本人进行了加工整合&#xff0c;并且用idea进行了启动测试&#xff0c;经过修改后没有任何问题。 启动截图 后端控制台 前端游戏界面 声明 冒险岛源码…

Ubuntu 24.04.1 LTS 本地部署 DeepSeek 私有化知识库

文章目录 前言工具介绍与作用工具的关联与协同工作必要性分析 1、DeepSeek 简介1.1、DeepSeek-R1 硬件要求 2、Linux 环境说明2.1、最小部署&#xff08;Ollama DeepSeek&#xff09;2.1.1、扩展&#xff08;非必须&#xff09; - Ollama 后台运行、开机自启&#xff1a; 2.2、…

进阶数据结构——树状数组

前言 看这篇文章前我建议你们先看这个视频还有这个视频&#xff0c;不然你们可能看不懂。 一、树状数组的核心思想与本质 核心思想&#xff1a;树状数组&#xff08;Fenwick Tree&#xff09;是一种用于高效处理前缀和查询和单点更新的数据结构。 本质&#xff1a;通过二进…

vue3 + thinkphp 接入 七牛云 DeepSeek-R1/V3 流式调用和非流式调用

示例 如何获取七牛云 Token API 密钥 https://eastern-squash-d44.notion.site/Token-API-1932c3f43aee80fa8bfafeb25f1163d8 后端 // 七牛云 DeepSeek API 地址private $deepseekUrl https://api.qnaigc.com/v1/chat/completions;private $deepseekKey 秘钥;// 流式调用pub…

「软件设计模式」桥接模式(Bridge Pattern)

深入解析桥接模式&#xff1a;解耦抽象与实现的艺术 一、模式思想&#xff1a;正交维度的优雅解耦 桥接模式&#xff08;Bridge Pattern&#xff09;通过分离抽象&#xff08;Abstraction&#xff09;与实现&#xff08;Implementation&#xff09;&#xff0c;使二者可以独立…

Vue2项目,商城系统

Vue2商城系统项目 商城系统 包含功能: 下单平台&#xff0c;登录&#xff0c;购物车 纯前端无后台、无数据库 &#xff01;&#xff01; 纯前端无后台、无数据库 &#xff01;&#xff01; vue2 setup语法糖写法 本项目主要使用技术&#xff1a; - 基于vue2的项目框…

百度千帆平台对接DeepSeek官方文档

目录 第一步&#xff1a;注册账号&#xff0c;开通千帆服务 第二步&#xff1a;创建应用&#xff0c;获取调用秘钥 第三步&#xff1a;调用模型&#xff0c;开启AI对话 方式一&#xff1a;通过API直接调用 方式二&#xff1a;使用SDK快速调用 方式三&#xff1a;在千帆大模…