什么样的场景可以使用递归呢?
需要同时满足以下三个条件才可以使用递归:
1、一个问题的解可以分解为几个子问题
2、这个问题分解分解之后的子问题,除了数据规模不同,求解思路完成相同
3、递归存在终止条件
如何编写递归代码?
写递归代码的关键是如何找到将大问题分解为小问题的规律,并且基于此写出递归公式,然后再推导出终止条件,最后将递归公式和终止条件翻译成代码。
这里需要注意的是:当我们遇到递归,把它抽象为递归公式的时候,不要想着层层调用,不要试图用人脑去分解每一个步骤,否则你就会被绕进去。
示例:假如有n个台阶,每次你可以跨1个台阶和两个台阶,请问n个台阶有多少种走法?如果有7个台阶,你可以2、2、2、1这样子上去,也可以1、2、2、1、1这样子上去,总之,走法很多,那如何编写求得总共有多少种走法呢?
分析:走到第n个台阶有两种情况,第一种就是从n-1个台阶走上去,第二种情况,就是从n-2个台阶走上去,那么n-1个台阶呢,同样是两种情况,为(n-1)-1和(n-1)-2。同理n-2也有两种情况,为(n-2)-1和(n-2)-2。所以可以得到:当走到第n个的走法为 走到第n-1个台阶的走法和走到n-2个台阶的走法总和。分析到这里已经可以推到出递归公式了,就不要在推导下去了,否则你就很容易绕进入,走不出来
得到公式为:
f(n) = f(n-1)+f(n-2);
递归公式已经推到出来了。接下来就需要考虑边界问题 ,也就是我们的终止条件。因为它有两种走台阶的方式,一个是一次走一个台阶,一个是一次走两个台阶,所有我们要考虑只有一个台阶和有两个台阶的情况。一个台阶那很明显只有1种情况;两个台阶,则有两种走法为(1、1)和2的情况
最终得到递归函数:
int f(int n){
if(n==1) return 1;
if(n==2) return 2;
return f(n-1)+f(n-2);
}
编写递归代码还需要考虑一下几种情况?
一)、递归需要警惕栈溢出
熟悉JVM虚拟机的同学都知道,方法的调用就是栈帧压入内存栈,调用完之后,就是栈帧出栈的过程。如果递归求解的数据规模很大,调用层次很深,一直入栈,就会存在栈溢出的风险。
代码示例:
int f(int i){
if(i==1) return 1;
return f(i-1)+i;
}
@Test
public void test(){
f(20000);
}
运行结果:
使用递归实现累加,到20000就已经出现栈溢出的问题了。那如何解决呢?方案一:当递归超过一定的深度(比如:100)之后,我们就不在递归,直接返回报错。
改进后的代码示例:
int depth=0; //定义全局变量表示递归的深度
int f(int i){
depth++;
if(depth==100) throw new RuntimeException();
if(i==1) return 1;
return f(i-1)+i;
}
方案二:把递归代码改为非递归代码,这在后面会进行详解。
二)、递归需要警惕重复计算
还是以上面的台阶为例,如果把整个问题分解,那就如下图:
从上图种可以看出,计算f(5),需要先计算f(4)和f(3),计算f(4),也需要计算f(3)。其中f(3)被计算多次。为了避免重复计算的解决方案,可以通过数据结构(比如散列表)来保存已经求解过的f(k)。
优化上面台阶问题代码:
//定义一个全局变量map,来保存已经计算过的值
HashMap<Integer,Integer> map=new HashMap<>();
int f(int n){
if(n==1) return 1;
if(n==2) return 2;
if(map.containsKey(n)){ //判断是否存在计算过的值
return map.get(n);
}
int sum=f(n-1)+f(n-2);
map.put(n,sum); //保存以计算过的值
return sum;
}
三)、递归需要警惕递归函数在调用过程种出现闭环的情况
如何查找最终推荐人?现在很多App都有这样的功能,用户A推荐用户B注册,用户B推荐用户C注册。A->B->C
我们可以说,C的最终推荐人是A,B的最终推荐人是A。而A没有最终推荐人。。我们的代码实现可能是这样
long findRootReferrerId(long actorId) {
Long referrerId = select referrer_id from [table] where actor_id = actorId;
if (referrerId == null) return actorId;
return findRootReferrerId(referrerId);
}
在实际过程中,可能就会出现这种情况,测试同学在测试的时候,造了一条数据,表示用户C推荐了用户A。那递归就会形成闭环A->B->C->A。这种情况就会发生死循环,造成栈溢出。这种问题的解决方案有两种,
一)、限制递归的深度
二)、检测递归中是否出现环的情况
问题一:所有递归代码是否可以改写为非递归代码?
笼统的讲,是的,所有的递归都是借助栈来实现的。只要我们在内存堆上实现栈,手动模拟入栈和出栈的过程,就可以把递归代码改成看上去非递归代码的样子。
问题二:怎样将递归代码改写为非递归代码?
案例一:也就是上面使用递归累加求和的案例
案例二:也就是上面上台阶的案例
总结:递归的使用有利也有弊,利是递归的表达能力很强,写起来非常简洁,而弊端就是,空间复杂度高,有栈溢出的风险,存在重复计算,过多的调用函数等问题,所以在实际开发过程中,需要根据实际情况是使用递归的方式实现
递归就是一个不断调用自身的过程,从JVM机角度而言,递归就是一个不断入栈的过程,所以需要考虑边界条件,也就是终止条件,否则就会存在栈溢出的情况。同时也需要考虑递归调用层次问题,如果调用层次太深,也会存在栈溢出的风险。
参考:数据结构之美--王争