递归的特征
如果看递归代码的结构,就像下面这个样子,前面的每一层都去一模一样地调下一层,不同的只是输入和输出的参数。
当然这个过程不能一直持续下去,一定要在满足某个要求之后返回结果的,否则的话,就会抛出“StackOverFLow”的问题。
所有的递归有两个基本的特征:
①执行时范围不断缩小,这样才能触底反弹。
②终止判断在调用递归的前面,这个终止条件,就是要触的底。
【1】执行范围不断缩小
递归就是数学里的递推,设计递归就是努力寻找数学里的递推公式,例如阶乘的递推公式就是f(n)=n*f(n-1),很明显一定是要触底之后才能反弹。再比如斐波那契数列的递归公式为f(n)=f(n-1)+f(n-2),n也在不断缩小。这条规律可以辅助我们检查自己写的递推公式对不对。
【2】终止条件判断在递归调用之前
递归之后可能还有终止条件,但是在执行递归之前,一定会有一个终止条件。这一条也可以帮助我们检查自己写的算法对不对。
任何递归方法在执行递归之前,一定会有一个终止条件。
实际一个方法里的递归调用可能不止一次,还会加一些逻辑处理,比如下面这样,但是终止的条件仍然在前面。
如何写递归
第一步:从小到大递推
递归该怎么写呢?递归源自数学里的归纳法,这个在高中数学中学过,大致就是你先猜测出存在递归关系,f(n)=δf(n-1),然后你只要证明当n增加1时,f(n+1)=δf(n)也是成立就说明你的猜测是对的。不过,在算法里,我们写递归一般不需要证明,先选几个较小的值验一下,再选择几个比较大的验一下即可。
很明显,大部分从n=1,2,3或者只有一两个元素开始写最简单。例如斐波那契序列为 1 1 2 3 5 8,...,从n=3开始都满足f(n)= f(n-1) + f(n-2),然后我们再选择某个比较大的n来验证即可。
我们仍然以阶乘和斐波那契数列为例来看。斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34….,即第一项 f(1) = 1,第二项 f(2) = 1…..,从第三项开始就满足:
f(n) = f(n-1) + f(n-2)
这就是我们要找的递归公式。
第二步:分情况讨论,明确结束条件
递归里终止条件一定是靠前的,而大部分递归的终止条件不过是n最小开始触底反弹时的几种情况。
有时候需要考虑的终止条件不止一个,例如斐波那契数列的递推公式f(n) = f(n-1) + f(n-2)里,如果n=2时会出现f(2)=f(1)+f(0),很明显这里是没有f(0)的,所以我们要将n==2也给限制住
有些情况不一定是触底才开始反弹,而是达到某种要求就要停止,这样需要考虑的情况会比较多。解决这类问题最直接的方式就是枚举,将可能的情况列举一下,再逐步优化。
确定终止条件对于递归至关重要,后面很多题目会花很大的篇幅来分析怎么判断终止条件,而一旦判断完毕,递推关系也就水到渠成了。
第三步:组合出完整方法
将递推公式和终止条件组合起来,变成完整的方法。
阶乘:
int f(int n){
if(n ==1){
return n;
}
return f(n-1) * n;
}
斐波那契数列
int fibonacci(int n){
// 1.先写递归结束条件
if(n <= 2){
return 1;
}
// 2.接着写等价关系式
return fibonacci(n-1) + fibonacci(n - 2);
}
怎么看懂递归的代码
我们先思考一个问题,上面的阶乘,如果n=4会调几次上面的f()方法呢?很明显应该是4次,递归的特征就是“不撞南墙不回头”,n=4,3和2时会继续递归,而n=1时发现满足退出条件了,就执行return 1,不再递归,而是不断返回上一层并计算。
接着再看返回时每层参数的问题,递归本质上仍然是方法调用,所以可以按照方法调用的方式来验证写的对不对。
下面这个图完整的表示了求阶乘的过程,你会发现递归不过是一个方法被调了好几次,每次n都在减小,这就是递进的过程。触底之后,也就是满足终止条件之后就开始返回了。
递进的时候当前层的n被系统给保存了,而返回的时候会自动设置回来,因此每层的n自然是不一样的,所以此时就是重新拿到当前这一层n的值完成计算即可。
例如我们将f(4)阶乘的过程如下: