递归有“去”和“来”的过程,去的过程叫“递”,回来的过程叫“归”。
递归需要满足的三个条件:
- 一个问题的解可以分解为多个子问题的解(子问题就是数据规模更小的问题)
- 该问题与分解之后的子问题,除了数据规模不同之外,求解思路完全一样
- 存在递归终止条件
关于递归的逻辑思考,如果某个问题只有一个子问题(可理解为只有一条线路,我们可以可以很简单的画出“去”和“来”的过程,例如 f(1)=1;return f(n)=f(n-1)+1;),但是如果该问题有多个子问题(每一步有多条线路可以走,人为就不好画出“去”和“来”的过程,例如 f(1)=1;f(2)=2;return f(n)=f(n-1)+f(n-2)),所以编写递归的关键在于,只要遇到递归,我们就把他抽象成一个递推公式,而不是用人脑去分解递归的每个步骤。
递归虽然可以简化代码量,但是也会有它存在的一些问题:
1.递归代码要警惕堆栈溢出。函数调用会使用栈来保存临时变量。每调用一个函数,都会将临时变量封装为栈帧压入内存栈,等函数执行完成之后才出栈。而系统栈或者虚拟栈空间一般都不大,如果递归求解的数据规模很大,调用层次很深,一直是入栈的操作,就会有堆栈溢出的风险。
解决办法:在代码中限制递归调用的最大深度(到达最多次数后,不在向下递归 而是直接报错)
当然这种方法并不能治本,因为最大允许的递归深度和当前线程剩余的栈空间大小有关,人为难以计算。如果人为判断该递归函数的递归次数不是很多(几百 几千),就可以使用这种方法,否则并不实用。
2.递归代码警惕重复计算。
例如 :
f(1)=1;
f(2)=2;
return f(n)=f(n-1)+f(n-2))
这个案例的小数据的递归分解如左图所示:
可以看到中间有大量的重复计算,浪费资源。
解决办法:可以通过一个散列表(或者其他数据结构)来保存已经求解过的f(k)。求解过的值 再次需要求解时直接从散列表中取值返回,不需要重复计算。
当然这里最好的办法就是 转换为非递归的循环操作来计算,后面会讲到。
3.复杂度分析消耗大。具体而言,在时间效率上,递归代码多个很多函数调用,当这些函数调用的数量较大时,就会积聚成比较大的时间成本。在空间复杂度上,因为递归调用一次就会在内存栈中保存一次现场数据,所以在分析递归代码空间复杂度时,需要额外考虑这部分的开销。
递归代码可以改为迭代循环的非递归代码吗?
一般而言,递归本身就是借助栈来实现的,只不过这里的栈是系统或者是虚拟机本身提供的。
举例:Fabincci Array问题
这个问题,我们都知道 数组:1,1,2,3,5,8,13,...
我们习惯性的使用来使用递归实现代码
public class FibonacciArray {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner in=new Scanner(System.in);
int n=in.nextInt();
System.out.println(fib(n));
}
public static int fib(int n){
if(n==0||n==1) return 1;
return fib(n-1)+fib(n-2);
}
}
其实这里 使用递归方法并不是最优的解,正如我们之前所言,这里会有大量的重复计算。这里使用递归方法的时间复杂度是O(2^n),方便理解 我们可以举一个例子n=5 n=4来进行判断,这里每一次递归有两个分支 递归过程画出来是一个典型的二叉树结构。或者根据我之前的博文白话代码中的复杂度分析-大O复杂度表示法 时间,空间复杂度分析 最好,最坏,平均复杂度
这里我们可以直接使用非递归的循环语句执行 得到结果
public class FibonacciArrayNotDigui {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner in=new Scanner(System.in);
int n=in.nextInt();
System.out.println(fib(n));
}
public static int fib(int n){
if(n==0||n==1) return 1;
int a=1,b=1;
int sum=0;
for(int i=2;i<=n;i++){
sum=a+b;
b=a;
a=sum;
}
return sum;
}
}
上面代码 我们可以得到相同的结果,但是时间复杂度却大大降低。这里的时间复杂度最小为O(1),最差为O(n),平均时间复杂度为O(n)
再举个例子:问题:n个台阶,每次可走一个1个台阶 或者2个台阶,n个台阶有多少种走法
递归方法:
public class Demo01 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner in=new Scanner(System.in);
int n=in.nextInt();
System.out.println(fun(n));
}
public static int fun(int n){
if(n==1) return 1;
if(n==2) return 2;
return fun(n-1)+fun(n-2); //递归解决
}
}
非递归方法:
public class Demo01ToNotDigui {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner in=new Scanner(System.in);
int n=in.nextInt();
System.out.println(fun(n));
}
//采用非递归的方式
//这里如果理解不了的话 可以采用举例画图的方式 例如f(4)=f(3)+f(2)=f(2)+f(1)+f(2)=5 有5种方式
public static int fun(int n){
if(n==1) return 1;
if(n==2) return 2;
int a=2,b=1;
int sum=0;
for(int i=3;i<=n;i++){
sum=a+b;
b=a;
a=sum;
}
return sum;
}
}
看完是不是觉得第二个例子和第一个例子中的Fib问题如出一辙了,是的。但是我们在日常的面试中,一般会问到的是第二个例子这样的形式。学会在看待新问题时联想我们之前用过的方法 时很重要的。 由我前段时间 面试小米的失败经验来看,对于面试官提到的某个具体问题,我们需要灵活的思考,并且对你所提出的方法进行时间复杂度分析,并能够不同方法的时间复杂度,以此能够得到某个问题的最优解。