递归
什么是递归:非常广泛的编程方法
下面有一个小场景解释递归:
去看电影的时候你不知道自己坐在第几排,在没办法数的情况下,通常会问前面的人是第几排,然后他的排数我们在进行+1。但是你前面的也不知道自己是第几排就去问他的前面的人,就这样一直问到第一排,然后一排排的在把座位排数传回来。在这个过程中去的过程称为‘递’,回来的过程称为‘归’,我们可以把上面例子用递推公式表示
f(n)=f(n-1)+1 其中f(1)=1 用代码表示就是
int f(int n){
if (n==1)
return 1;
return f(n-1)+1;
}
递归需要满足的条件
1.待求解问题的解可以分解为几个子问题的解
2.待求解问题与分解之后的子问题,只有数据规模不同,求解思路完全相同
3.存在递归终止条件
递归的堆栈溢出
函数调用会使用栈来保存临时变量。如果递归求解的数据规模很大,调用层次很深一直往函数栈里添加数据就有可能塞满函数栈倒置堆栈溢出。
int f(int n){
if (n==1)
return 1;
return f(n-1)+1;
}
如果将JVM设置成1KB,在求解f(19999)时,便会出现错误 StackOverflowError
我们可以简单的通过设置递归调用深度来去解决,实际上但是不能完全解决,因为允许的最大递归深度与当前线程剩余的栈空间大小有关无法事先计算。
int dept=0;
int f(int n){
++dept;
if(dept>1000)
throw exception;
if (n==1)
return 1;
return f(n-1)+1;
}
递归的重复计算
int f(int n){
if (n==1)
return 1;
if (n==2)
return 2;
return f(n-1)+f(n-2);
}
计算f(5)需要算出f(4)和f(3)
计算f(4)需要算出f(3)和f(2)
f(3)被多次计算这就是重复计算问题 解决办法一般是
保存已经计算过的f(k),当计算到f(k)的时候先看一下是否求解过。
一般记录我们可以使用hash表
int f(int n){
if (n=1)
return 1;
if (n==2)
return 2;
if (hasSolvedList.containKey(n)){ //可以理解成一个Map
return hasSolvedList.get(n);
}
int ret =f(n-1)+f(n-2);
hasSolvedList.put(n,ret);
return ret;
}
递归除了堆栈溢和重复计算这两个常见问题,在执行效率上当函数调用较多时,函数调用本身就比较耗时。在空间复杂度上需要额外的开销,因为递归调用一次就会在内存栈中保存一次现场数据。
将递归代码改写成非递归代码
int f(int n){
if (n==1)
return 1;
return f(f-1)+1;
}
int f(int n){
int ret=1;
for (int i=2;2<=n;++i){
ret =ret+1;
}
return ret;
}
从理论上来说,递归本身就是借助栈来实现的,只不过使用的栈是系统或虚拟机提供的函数调用栈,不需要在编程的时候显示定义。如果我们自己模拟函数调用栈,手动模拟入栈出栈的过程,那么任何递归代码都可以写成看上去不是递归的代码,但是本质没有变。
为什么递归会产生堆栈溢出
函数调用采用函数调用栈来保存现场(局部变量、返回地址等)函数调用栈是在内存中开辟的一块存储空间,它被组织成栈这种数据结构,数据先进后出
递归过程包含大量的函数调用,如果递归要求解的规模很大,函数调用层次很深,那么函数调用栈的数据(栈帧)会越来越多,如果函数调用栈的空间一般不大,就容易相有堆栈溢出的风险。
尾递归:并不是所有的代码都可以改写成尾递归,只有递归调用出现在函数的最后一行,并且没有任何局部变量参与最后一行代码的计算。就可以改成尾递归
int f(int n,int ret){
if(n<=1)
return res;
return f(n-1,n*res);
}
从理论上讲尾递归是有可能解决堆栈溢出问题的