Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >for循环、递归、回溯

for循环、递归、回溯

作者头像
vv彭
发布于 2020-10-27 03:13:12
发布于 2020-10-27 03:13:12
1.2K00
代码可运行
举报
文章被收录于专栏:c#学习笔记c#学习笔记
运行总次数:0
代码可运行

目录: 1.简单递归定义 2.递归与循环的区别与联系 3.递归的经典应用

1.简单递归定义

什么叫递归?(先定义一个比较简单的说法,为了理解,不一定对)

递归:无限调用自身这个函数,每次调用总会改动一个关键变量,直到这个关键变量达到边界的时候,不再调用。

比如说我要你先求一个N!的结果

你说我会用循环啊(没错,但是现在是学递归)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int factorial(int x,int ans)
{
    if(x==1)
       return  ans;
    factorial(x-1,ans*x);
}

怎么样,对于C基础如果掌握的还行的话,这段代码应该很好理解。递归,顾名思义就是“递”和“归”。也就是说,写每一个递归函数的时候,都应该在写之前考虑清楚,哪里体现了“递”,哪里体现了“归”。

但是常常递归函数会比较复杂, “递”和“归”看起来并不是那么明显,这就需要我们进一步来理解递归算法的思想。

比如说我现在要你用辗转相除法求出两个数的最大公约数,递归函数如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int gcd(int a,int b)
{
    return a%b==0?b:gcd(b,a%b);
}

这是一段很常用的代码,我们知道,在学习过程中不求甚解是最不应该的。因此现在来仔细看一看。这里的“递”和“归”放在同一行。首先进行判断a==b?(我们可以想象成“归”的内容,如果这个条件符合的话)。当然,如果不符合这个判断,那就继续“递”,也就是继续进行gcd(b,a%b);

看到这里,你就会发现,递归不就是循环的另一种方式么?

说对了一半,不过递归是一种思想,现在还暂时不能说透,需要大家先比较一下循环和递归的相同点和不同点(饭一口一口吃,别着急)

2.递归与循环的区别于联系

相同点: (1)都是通过控制一个变量的边界(或者多个),来改变多个变量为了得到所需要的值,而反复而执行的; (2)都是按照预先设计好的推断实现某一个值求取;(请注意,在这里循环要更注重过程,而递归偏结果一点)

不同点: (1)递归通常是逆向思维居多,“递”和“归”不一定容易发现(比较难以理解);而循环从开始条件到结束条件,包括中间循环变量,都需要表达出来(比较简洁明了)。

简单的来说就是:用循环能实现的,递归一般可以实现,但是能用递归实现的,循环不一定能。因为有些题目①只注重循环的结束条件和循环过程,而往往这个结束条件不易表达(也就是说用循环并不好写);②只注重循环的次数而不注重循环的开始条件和结束条件(这个循环更加无从下手了)。

3.递归的经典应用

来看看“汉诺塔问题”

如图,汉诺塔问题是指有三根杆子A,B,C。C杆上有若干碟子,把所有碟子从A杆上移到C杆上,每次只能移动一个碟子,大的碟子不能叠在小的碟子上面。求最少要移动多少次?

这是一个循环只注重循环次数的常见例子,我们知道,用循环有点无从下手(就目前作者水平来看),但是递归就很好写了。

汉诺塔,什么鬼,我不会啊?

别急,慢慢来。

我们首先需要一点思维:解决n块盘子从A移动到C,那么我只需要先把n-1块盘子从A移到B,然后把最下面的第n块盘子从A移到C,最后把n-1块盘子从B移到C(这就完成了)。

等等,那么如何把n-1块盘子从A移到B?

很好,这说明你已经开始递归入门了。

同样这样去想:解决n-1块盘子从A移动到B,那么我只需要先把n-2块盘子从A移动到C,然后把倒数第二块盘子从A移到B,最后把n-2块盘子从C移到B(这就完成了)。

这就是递归的“递”!

那么“归”呢?n==1的时候?Bingo

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int i;    //记录步数  
//i表示进行到的步数,将编号为n的盘子由from柱移动到to柱(目标柱)  
void move(int n,char from,char to){  
    printf("第%d步:将%d号盘子%c---->%c\n",i++,n,from,to);  
}  
  
//汉诺塔递归函数  
//n表示要将多少个"圆盘"从起始柱子移动至目标柱子  
//start_pos表示起始柱子,tran_pos表示过渡柱子,end_pos表示目标柱子  

void Hanio(int n,char start_pos,char tran_pos,char end_pos)
{  
    if(n==1)      //很明显,当n==1的时候,我们只需要直接将圆盘从起始柱子移至目标柱子即可.  
        move(n,start_pos,end_pos);   
    else
    {  
        Hanio(n-1,start_pos,end_pos,tran_pos);   //递归处理,一开始的时候,先将n-1个盘子移至过渡柱上  
        move(n,start_pos,end_pos);                //然后再将底下的大盘子直接移至目标柱子即可  
        Hanio(n-1,tran_pos,start_pos,end_pos);    //然后重复以上步骤,递归处理放在过渡柱上的n-1个盘子  
                                                  //此时借助原来的起始柱作为过渡柱(因为起始柱已经空了)  
    }  
}  

实际上这里面已经使用到了一点点栈的思想(即最上面的最先考虑变化),但其实递归有的时候就是真的可以理解为栈!

到了这一步,相信大家应该已经有所明白。循环其实就是一个控制变量从开始条件走到结束条件的过程(在循环的过程顺带把其他变量也改变一下),因此需要控制变量,开始条件,结束条件(缺一不可)。但是递归只要告诉你“归”是什么,如何去“递”,不管过程如何,只要计算结果即可。

(2)递归可以是多个“递”,也可以是多个“归”;而循环由始至终都只由一个变量控制(就算有几个变量同时控制)也只有一个出口,每次循环也只是一个“递”。

再看一个例子

用二分思想建立二叉树(通常的是递归实现),比如说线段树

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//root   节点序号  
//left   节点维护的左边界  
//right  节点维护的右边界
void build(int root,int left,int right)
{
    if(left==right)
      return ;
    int mid=(left+right)/2;
    build(root*2,left,mid);
    build(root*2+1,mid+1,right);
}

如果你是新手看不太懂也没关系,现在最主要的是明白:在这个程序里面只有一个“归”,但是有两个“递”。

那么如果学过一点但是对这一块还不明白的怎么办呢?别急,听我来解释:

实际上,这两个“递”是按照先后分别进行的,等到第一个“递”执行完(也就是到了“归”的条件之后),才开始执行第二个“递”。也就是说,通常在建树的时候,都不是一层一层同时建的,而是先建一棵子树,等到这棵子树全部建完之后,才开始建立另外一棵子树。

那就会问了,一棵子树建完了之后root值不会变么,root值变了之后还怎么建另外一棵子树呢?

root值不会变!大家请注意,这里root*2是写在递归函数里面的,实际上并没有赋值?为什么要这样写?因为如果不这样写,你直接写在外边的话,一棵子节点到达叶子节点之后,需要一层一层往上回溯(在这里提到了回溯的思想),而回溯就会无故产生很多不必要的时间复杂度,降低了递归效率(实际上递归的时间效率本来就有一点偏低)。

所以到目前为止,我只是介绍一些很常见的简单的递归,但是在接下来,我就需要说一些比较深层一点的知识了。

首先要理解一下什么是回溯(写的不好,大佬勿喷)

回溯:在递归的过程中由于改变的量需要倒退到某一个位置而执行的步骤。

先来看一个简单的素数环问题:

给出1到n的n个连续的正整数(这里n暂时等于6),并且把这n个数填写在如下图的n个圆圈里面(当然是不重复不遗漏了)。要求是每一个位置上面的数跟他相邻的数之和都为一个素数,打印并输出最后满足条件的情况。

首先明白,开始条件是1,把1填写在第一个位置,然后在剩下的n-1个数字里找到一个满足与1的和是一个素数的数(当然如果有多个,先靠前的先考虑)。接下来再继续从剩下n-2个数字里找到一个与这个数的和又是一个素数的数(当然如果有多个,同上。)。。。最后的一个数只要满足与最开始的数1之和是一个素数的话,这个情况就满足了(就可以打印输出这样一个例子了)

但事情并没有想象的那么简单。。。(告诉我如果在中途寻找的过程中从剩下的数里找不到与当前数的的和是一个素数的情况出现怎么办?在线等)

这就表明这样一条路终归是一条思路,你要往回走了!这就很符合我们给回溯的定义了,此时这个改变的量需要倒退的前面一步从另外一个方向寻找了。(还是举栗子吧)

比如说:

1->2->3->4 突然发现5和6都不满足要求了

那么就倒退,准备找另外满足要求的数

1->2->3 又发现除了4以外3跟5或者3跟6也不满足要求

那就继续倒退,继续准备找另外满足要求的数

1->2->5->6 接下来发现6跟3或者6跟4不满足要求

…(还想继续下去?你是要玩死我?别这样,我也累啊,看一两个就行了,啊!) 最后发现满足条件的一个是

1->4->3->2->5->6

大家应该已经懂了,上面的倒退,实际上就是回溯。(暂时这样简单的理解吧,错了也不能怪你们)

实际上,递归+回溯就已经是dfs(深度优先搜索)的内容范畴了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void dfs(int x)
{
    if(x==n+1&&prime(a[x-1]+a[1]))    //如果满足条件就可以打印输出数据了,这里就是“归”
    {
        for(int i=1;i<n;i++)
          cout<<a[i]<<" ";
        cout<<a[n]<<endl;
    }
    else                        //否则就继续“递”
    {
        for(int i=2;i<=n;i++)
        {
            if(!vis[i]&&prime(i+a[x-1]))
            {
                vis[i]=1;            //vis[]是一个标记数组,表示当前的数字已经被使用过了
                a[x]=i;
                dfs(x+1);   //“递”的入口
                vis[i]=0;          //请注意,回溯点在这里
            }
        }
    }
}

大家可能前面都看懂了,比如说“递”和“归”,vis[]标记数组什么的。但是最后一个vis[i]=0是啥意思?难道不多余么?

不多余!前面我已经拿建树给大家讲过递归的“工作原理”,它是先无限递归,然后到达某个条件之后,回溯到上面一个位置,继续向其他方向递归。而这个vis[i]=0就是清楚当前数字的标记,表示从当前节点开始,之后递归过的内容统统清空(也就是回溯)。然后根据循环,进行下面一个方向的继续递归。

这也是dfs的经典思想所在!

因此,讲到这里,不说说dfs似乎也是吊大家胃口了。所以接下来,就聊一聊dfs中的递归。

比如说hdu上面的1312 http://acm.hdu.edu.cn/showproblem.php?pid=1312

我简单说一下意思,如下图,判断一个图内包括@符号在内的所有‘.’和‘@’的个数。有个限制条件,如果‘.’被‘#’封死,则‘.’不可访问。

6 9 (分别表示行和列)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
....#.
.....#
......
......
......
......
......
#@...#
.#..#.

45

比如说这一个数据就有三个‘.’被边界和‘#’困死而不可访问,因此只有45个点满足要求。

本题的思路就是从’@'点开始,bfs搜索每一个点,分成上下左右四个方向分别递归搜索。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int cnt,a[4]={-1,0,1,0},b[4]={0,1,0,-1},n,m,vis[22][22];
char s[22][22];      
void dfs(int x,int y)
{
	for(int i=0;i<4;i++)      //四个方向循环搜索
	{
		int p=x+a[i];
		int q=y+b[i];
		if(p>=0&&p<m&&q>=0&&q<n&&!vis[p][q]&&s[p][q]=='.')     //判断递归条件,包括在数组边界之内,该点未被标记 
		{
            vis[p][q]=1;    //标记该点 
		  	cnt++;      //计数变量加一 
		    dfs(p,q);     //递归搜索 
		}
	}
}

请注意:本题中因为可以提前判断下一个要搜索的点是否为‘#’而免于回溯,降低时间复杂度。

并且大家可以看出,上面的代码实际上是稍微复杂一点的递归算法(把从‘@’出发的每一个方向看成一条线段,而这条线段的另外一个终点就是边界或者’#’),因此这就是可以看成循环了四次的递归算法,而每一次递归调用的过程,每一方向又看成从当前点出发的一条线段。如此反复。(中间的cnt用来计数)

请注意,cnt就是就是递归的次数(因为没有回溯,如果有回溯,计数的话不一定等于递归的次数)

到此,基本知识点已经全部讲完,下面给出一点个人关于写递归算法的建议吧:

(1)把递归当成复杂的循环来写,如果不明白过程,多模拟几遍数据;

(2)把递归逆向写的时候当做一个栈来实现(即符合后进先出的思想);

(3)当递归和回溯结合在一起的时候需要明白递归次数和统计次数之间的练习和区别;

(4)但递归有多个“递”和“归”的时候,选择一个重点的“递”和“归”作为匹配,即时题目即时分析,注意随机应变即可。

本文转载于:https://blog.csdn.net/feizaosyuacm/article/details/54919389

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-09-23 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
递归专题BFS
先来说一下有关递归的几个算法;深度优先搜索,深度优先遍历,广度优先搜索,广度优先遍历以及回溯,剪枝,记忆化搜索;
啊QQQQQ
2024/11/19
810
递归专题BFS
【基础算法】递归算法
递归算法是一种直接或间接调用原算法的算法,一个使用函数自身给出定义的函数被称为递归函数。利用递归算法可以将规模庞大的问题拆分成规模较小的问题,从而使问题简化。无论是递归算法还是递归函数,最大的特点都是“自己调用自己”。
WuShF
2023/07/08
3880
【基础算法】递归算法
【C语言】函数递归例子1汉诺塔问题
汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
用户11290673
2024/09/25
840
【C语言】函数递归例子1汉诺塔问题
【递归回溯与搜索算法篇】算法的镜花水月:在无尽的自我倒影中,递归步步生花
在经典汉诺塔问题中,有 3 根柱子和 N 个不同大小的穿孔圆盘。圆盘可以滑入任意一根柱子。初始状态下,所有圆盘自上而下按升序叠在第一根柱子上,任务是将所有圆盘从第一根柱子移到最后一根柱子,满足以下限制:
半截诗
2024/11/21
1060
【递归回溯与搜索算法篇】算法的镜花水月:在无尽的自我倒影中,递归步步生花
《JavaSE-习题篇二》之七个题目,十六张图,让你不惧递归。
学习方法后,我们来学习一种特殊调用方法的方式,即递归。本篇文章将介绍什么是递归,以及递归的使用规则和注意事项,最后通过几道经典的题目来加深对递归的理解。
用户10517932
2023/10/07
2160
《JavaSE-习题篇二》之七个题目,十六张图,让你不惧递归。
【C语言篇】递归详细介绍(基础概念习题及汉诺塔等进阶问题)
递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。 写⼀个史上最简单的C语⾔递归代码:
半截诗
2024/10/09
1190
【C语言篇】递归详细介绍(基础概念习题及汉诺塔等进阶问题)
递归求解汉诺塔问题
博主之前有写过关于递归问题的思维模式: 递归的思路 下面将用这种思维模式来求解经典汉诺塔问题。
VIBE
2022/12/02
4470
经典递归问题--汉诺塔(java实现)
这里就是在fac()函数内部 不断调用 fac函数 ;通过简单的代码来实现复杂过程。
黎鹤舞
2024/03/19
1700
经典递归问题--汉诺塔(java实现)
用例子理解递归
在说什么是递归之前,我想正在阅读的你应该会使用循环来解决一些问题了。那循环又是什么呢?循环是指在程序中需要反复执行某个功能而设置的一种程序结构。它由循环体中的条件,判断继续执行某个功能还是退出循环。
花狗Fdog
2020/11/09
1.1K0
多柱汉诺塔最优算法设计探究
多柱汉诺塔最优算法设计探究 引言 汉诺塔算法一直是算法设计科目的最具代表性的研究问题,本文关注于如何设计多柱汉诺塔最优算法的探究。最简单的汉诺塔是三个柱子(A、B、C),因此多柱汉诺塔的柱子个数M≥3。下面从三柱汉诺塔说起,慢慢深入我们要关心的问题。 1. 三柱汉诺塔 三柱汉诺塔是经典的汉诺塔问题,在算法设计中是递归算法的典型问题。其算法是这样的: 首先把A 柱上面的n- 1 个碟子通过C 柱移到B 柱上【T(n-1)步】,然后把A 柱剩下的一个碟子移到C 柱上【1步】, 最后把B 柱上所有的碟子通过A 柱
Florian
2018/02/05
2.2K0
算法基础:递归
递归的基本思想就是把规模大的问题转化为规模小的相同的子问题来解决。在函数实现时,因为大问题和小问题是一样的问题,因此大问题的解决方法和小问题的解决方法也是同一个方法。这就产生了函数调用它自身的情况,而这个函数必须有明确的结束条件,否则就会导致无限递归的情况。
RendaZhang
2020/09/08
4510
算法基础:递归
深入浅出递归算法
递归就是将一个很大的问题拆分成子问题,然后再将子问题继续拆分,拆分成更小的子问题,最后直到不能拆分为止。 递归一共分为三个步骤,首先,我们要将一个问题拆为一些子问题,然后去看这些子问题是否有相同的方法可以继续拆分,所以递归的关键就是一个大问题是否能转换成相同类型的子问题,然后子问题是否又能继续转换成相同类型的子问题,注意这里我们就需要搞定我们这个递归的函数传递的参数具体需要什么,也就是(函数头),当我们确立了子问题之后,我们就需要进行函数体的书写了,书写函数体主要围绕单个子问题进行,因为,我们的一个大的问题都可以拆分为一个个的小的子问题,所以这些子问题都可以通过一个方法来处理,所以只需要对一个子问题进行书写函数体就行了,最后,我们需要防止无限递归,也就是递归的终止条件,向上归的过程。
用户11305458
2024/10/09
1280
深入浅出递归算法
数据结构与算法——DFS(深度优先搜索)
深度优先搜索(Depth-First Search,简称DFS)是一种用于遍历或搜索树或图的算法。这种算法会尽可能深地搜索图的分支,直到找到目标节点或达到叶节点(没有子节点的节点),然后回溯到上一个分支继续搜索。DFS可以用于许多问题,比如路径寻找、连通性验证、拓扑排序等。
摆烂小白敲代码
2024/09/23
4010
数据结构与算法——DFS(深度优先搜索)
递归算法专题一>汉诺塔问题
处理N(盘子数)为1时候,其他数量少的情况比如 N=2,都可以由  N=1  的方式得来,就好像套娃:这个N=3 问题 可以分为,N=2来解决。
用户11305962
2024/11/21
1140
递归算法专题一>汉诺塔问题
汉诺塔递归太难理解了_函数定义时可以用递归吗
记得我第一次做汉诺塔这道题时,是2017年11月。当时,我坐在山大青岛校区图书馆3楼,不知怎么地,看到了这个题。
全栈程序员站长
2022/11/17
7640
【算法专题】递归算法
题目:在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制: (1) 每次只能移动一个盘子; (2) 盘子只能从柱子顶端滑出移到下一根柱子; (3) 盘子只能叠在比它大的盘子上。
YoungMLet
2024/03/01
1130
【汉诺塔】经典递归问题(Java实现)图文并茂讲解
汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
爱敲代码的小杨.
2024/05/07
6440
【汉诺塔】经典递归问题(Java实现)图文并茂讲解
C 语言函数递归探秘:从基础概念到复杂问题求解的进阶之路
递归可以理解为一种分而治之的思想:将复杂问题拆分为若干规模更小但相似的子问题,直到可以直接解决。
学无止尽5
2024/11/29
1810
C 语言函数递归探秘:从基础概念到复杂问题求解的进阶之路
递归-汉诺塔问题
汉诺塔传说:汉诺塔问题,是源于印度一个古老的益智玩具;大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
西湖醋鱼
2020/12/30
8900
递归-汉诺塔问题
C语言:函数递归
递归的思想: 把⼀个⼤型复杂问题层层转化为⼀个与原问题相似,但规模较小的⼦问题来求解;直到⼦问题不能再被拆分,递归就结束了。所以递归的思考⽅式就是把⼤事化小的过程。
小陈在拼命
2024/02/17
1680
C语言:函数递归
相关推荐
递归专题BFS
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档