一、递归时出现的错误
现使用单路递归的方法进行n到一的求和,用Java代码实现如下:
//递归求和 n + (n-1) + ... + 1
public class E06Sum {
public static void main(String[] args) {
long s = sum(15000);
System.out.println(s);
}
//f(n) = f(n - 1) + 1
//简单的单路递归
public static long sum(long n){
if(n == 1){
return 1;
}
return sum(n-1) + n;
}
}
当n较小时没有问题可以正常跑,但是当我这里设置n = 15000 的时候报错了,Exception in thread "main" java.lang.StackOverflowError StackOverflowError栈内存耗尽,俗称爆栈。
二、对错误进行分析
用伪代码来分析错误:
/*
用伪代码说明以下执行过程:
long sum(long n = 15000){
return 15000 + long sum(long n = 14999){
return 14999 + sum(14998){
。。。。。。。。。。。。。。
return 2 + long sum(long n = 1){
if(n == 1){
return 1;
}
}
}
};
}
*/
其中,若最内层的函数(如下所示)sum(long n = 1)没有完成,其他的函数是不会执行的,只有最内层的函数得到返回值了才能一层一层的向外归,也就是递的过程必须递到最深处才能归。
/*
return 2 + long sum(long n = 1){
if(n == 1){
return 1;
}
}
*/
每个方法调用时需要消耗一定的内存,因为方法调用时需要存储方法相关的信息比如方法的参数信息,局部变量的信息,方法的返回地址等信息,这些信息都要储存到栈内存中。最内层sum(n = 1)这个方法没有结束之前,前面的14999个sum方法都要等待,它们占用的内存也不能得到释放,只有当方法执行完毕时这个方法占用的内存才能释放,故我们占用得内存是一点一点释放的。
所以我们刚刚出现StackOverflowError栈内存耗尽 这个错误是因为方法调用得太多了,层级太深,导致栈内存耗尽。
三、爆栈问题解决方法
1.尾调用的概念
如果函数的最后一步是调用一个函数,那么就称为尾调用,例如:
function a(){
return b();//函数a的最后一步是调用函数b
}
下面三段代码不能称之为尾调用:
- 最后一步并虽然调用了函数,但是又进行了运算:
function a(){
return b() + 1;
}
- 最后一步并非调用函数:
function a(){
const c = b();
return c;
}
- 最后一步虽然调用了函数,但又用到了外层函数的变量x:
function a(x){
return b() + x;
}
2.尾调用的用途
一些语言的编译器能够对尾调用做优化,例如:
没优化之前的代码:
//有以下三个函数a\b\c
//函数a尾调用了函数b
function a(){
//前面的处理
return b();
}
//函数b尾调用了函数c
function b(){
//前面的处理
return c();
}
function c(){
return 1000;
}
//首先调用函数a
a();
当我们先调用函数a时的过程的伪代码:
function a(){
return function b(){
return function c(){
return 1000;
}
}
}
可以看到,这种执行的过程是一种嵌套的关系。当函数c没有执行完之前,a、b都要等待c执行完,故c执行完之前a、b所占的内存并不能得到释放。
优化之后的代码:
a();
b();
c();
优化后的代码执行过程就从嵌套的关系变成一种平级的调用关系,内存也能够得到及时地释放。这就是某一些编译器能够对尾调用进行的优化。那么具体优化方法就是,例如在a函数前面的处理执行完了之后我们就不需要用到a了,就将a占用的内存释放释放,因为最后a的结果是由调用b来提供的,b函数也同理。这也是为什么尾调用才可以做这种优化的原因,若不是尾调用,例如最后的操作是 return b() + 1; 那么我们要先得到b返回的值,再进行运算,最后才能得到a的结果,所以此时无法将a释放,这种情况就不能做尾调用的优化了。
3.尾调用的一种特殊情况-尾递归
尾递归指的是函数最后调用的函数是他本身:
function a(){
//前面的处理
return a();
}
能对尾调用做优化的编译器肯定能对尾递归做优化。
4.什么语言能够对尾调用做优化
- c++
- scala
这里具体讲一下scala.