文章目录
一、什么是递归
递归是学习编程语言绕不开的一个话题,那么什么是递归呢?
递归我个人感觉更像是一种解决问题的方法,与算法类似,这里拿C语言举例,递归就是函数自己调用自己
展示一个史上最简单的C语言递归代码
可以发现程序陷入了死递归,最终停下来,这是什么原因呢?
这里需要配合这张图来说:
在每一次函数调用的时候,都会在内存的栈区申请空间,而上面反复调用main函数打印hehe就会陷入无限递归,停下来是因为把栈区空间撑爆了。
这里进入调试后也会发现,曝出栈溢出的问题
1.1 递归的思想:
把一个大型复杂问题层层转化为一个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆分,递归就结束了。所以递归的思考方式就是把大事化小的过程。
递归中的递就是递推的意思,归就是回归的意思。
1.2 递归的限制条件
递推在书写的时候,有两个必要条件:
- 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
接下里举例来体会这两个限制条件
二、递归的举例
2.1 求n的阶乘
这也是一个经典问题了,求n的阶乘,即求1-n的数字累积相乘
稍作分析我们就能得出这样图示一个结论。
从n!=(n-1)!*n可以看出这是将一个较大的问题,转换为另一个与原问题相似,但规模较小的问题来求解的。
n的阶乘与n-1的阶乘都是相似的问题,但是规模要少了n。有一种特殊情况就是:当n==0的时候,n的阶乘是1,而其余n的阶乘都是可以通过上面的公式计算。
这里我们写一个函数Fac()来求n的阶乘,Fac(n-1)即求n-1的阶乘,就会得到下面这个公式
代码实现
基于上面的分析,就有了这样一串代码,可以看出运行的结果是正确的,大家这时候看着这串代码可能还是有些疑惑,因为本人也一样。
再配合下推导过程图就清晰多了
红色线表示递推,蓝色线表回归。
还有一点在递推时每次都需要申请空间,但这些申请的空间在回归时候就会逐次销毁,如果只申请不销毁,那内存不就爆炸了吗?
2.2 顺序打印一个整数的每一位
输入一个整数n,按照顺序打印整数的每一位
输入:1234 输出:1 2 3 4
输入:520 输出:5 2 0
2.2.1 分析和代码实现
看到这个题目是不是很熟悉,之前发了一篇文章:怎么逆序打印每一位,逆序当时是这样打印的:
如果n是一位数,n的每一位就是n自己
n如果超过1位数的话,就得拆分每一位
1234%10就能得到4,然后1234/10得到123,这就相当于去掉了4
然后继续对123%10,就得到了3,再除10去掉3,以此类推
不断%10和/10操作,直到1234的每一位都得到
基于之前的经验,我们就有了灵感,可以发现一个数字的最低位是最容易得到的,通过%10就能得到
我们可以用函数Print来打印n的每一位:
Print(n)
如果n是1234,那表示为
Print(1234)//打印1234的每一位
其中1234中的4可以通过%10得到,那么
Print(1234)就可以拆分为两步
1. Print(1234/10) //打印123的每一位
2. printf(1234%10)//打印4
完成上述2步,就完成了1234每一位的打印
那么Print(123)又可以拆分为Print(123/10)+printf(123%10)
依次类推,就有
Print(1234)
-->Print(123) + printf(4)
-->Print(12) + printf(3)
-->Print(1) + printf(2)
-->printf(1)
直到被打印的数字变成一位数的时候,就不需要再拆分,递归结束,代码到这里也比较清楚了。
在这个解题过程中,就是使用了大事化小的思路。
把Print(1234)打印1234每一位,拆解为首先Print(123)打印123的每一位,再打印得到4
把Print(123)打印123每一位,拆解为首先Print(12)打印12的每一位,再打印得到3
直到Print打印的是一位数,直接打印就行。
再附以两张图加以理解,我个人认为函数递归这里还是很难的,主要很难想到和使用
红线递推,蓝线回归
灰线递推,黄线回归
递归的优点也是很明显的,能用很少的代码解决复杂的问题。
三、递归与迭代
说完了递归的优点,接下来就该说递归的缺点了,在C语言中每一次函数调用,都需要本次函数调用在内存的栈区,申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时的堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开闭属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。
3.1 求第n个斐波那契数
这就是一个递归缺点的极端例子,该问题就是使用递归的形式描述的,那么什么是斐波那契数呢?
斐波那契数列:
1 1 2 3 5 8 13 21 34 55.....
下标 1 2 3 4 5 6 7 8 9 10
斐波那契数的特点就是从第三个数开始,第三个数字是前两个数的相加和。
基于此特点,就推出了这样一个公式
代码展示
可以看出能正确输出答案,但是如果输入的数据偏大的时候:
貌似就不输出值了,其实这里不是不输出,而是计算量太大了,系统已经在疯狂的算了,只是计算任务太大了。
这是为什么呢?依旧简单分析一下
要算50的斐波那契数,就需要算49和48的,依次类推,不断二分,而且一些数据都需要重复算很多次,大大浪费了时间,就比如说图中的45就目前来看就要算6次
这里代码再做一点改进,如果我求第40个斐波那契数的时候,看看第三个斐波那契数会被重复计算多少次?
可以看到第三个斐波那契数被计算了3900多w次,这就是前面计算50的斐波那契数要算那么久的原因了。
迭代也就是正常的循环写法,只不过迭代的写法比较难以想到
这里蓝色是斐波那契数列,红色的是下标
这里前两个数是1,从第三个数开始,把第一个数设为a,第二个设为b,相加和为c,然后依次把a设为第二个数,b设为第三个数,计算的想加和c为第四个数,这样循环就有了,第三个数循环一次,第四个数循环两次,就有了下面这串代码:
因为int范围有限,这里的值是错误的,但其在一瞬间就算了出来,这里就彻底区分出递归与迭代的优缺点了:
递归
- 会占用用更多的内存空间,有可能导致栈溢出的问题
- 性能下降
- 有的复杂问题,使用递归描述非常简洁,写成代码也非常的方便
迭代
- 效率高
- 迭代的方式有时候不容易想到
事实上,我们看到的许多问题都是以递归的形式进行解释的,这只是因为它比非递归的形式个更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。
当一个问题非常复杂,难以使用迭代的方式实现时,此时递归实现的简洁性便可补偿它所带来的运行时的开销。
总结
这篇文章总结了函数递归迭代的优缺点以及其在经典案例上运用的讲解。各位观众姥爷如果对作者的文章还算满意的话,不要忘记一键三连给予支持哦~