浅谈尾递归
今天在围观大神博客时,看到尾递归这个名词,这里对自己看到的做个总结。
1. 递归
一个函数直接或间接的调用自身,这个函数就是一个递归函数。尾递归也是一种特殊的递归函数。
如计算一个阶乘函数:
public static int fac(int n) { if(n ==0 ) { return 1; } if(n==1) { return 1; } else { return n*fac(n-1); } }
这里fac(n)在计算过程中会不停的调用自身。
递归函数的特点:
(1)递归就是在过程或函数里调用自身。
(2)在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。(上面n==1,就是递归出口)
递归函数在调用自身的过程中,需要将每一层函数的returnAddress,局部变量等保存在栈存储中进行后面的运算,所以当递归深度过大时,递归函数会出现栈溢出的错误。
2. 尾递归
尾递归是一种特殊的递归,满足的要求是:函数的最后执行代码除了调用函数自身外,不再执行其他运算。
public static int facTail(int n,int m) { if(n==0) { return 1; } if(n==1) { return m; } else { return facTail(n-1,m*n); } }
上面的函数就是一个尾递归函数 ,因为最后执行代码是调用函数自身。
3.编译器是怎样优化尾递归的?
我们知道递归调用是通过栈来实现的,每调用一次函数,系统都将函数当前的变量、返回地址等信息保存为一个栈帧压入到栈中,那么一旦要处理的运算很大或者数据很多,有可能会导致很多函数调用或者很大的栈帧,这样不断的压栈,很容易导致栈的溢出。
我们回过头看一下尾递归的特性,函数在递归调用之前已经把所有的计算任务已经完毕了,他只要把得到的结果全交给子函数就可以了,无需保存什么,子函数其实可以不需要再去创建一个栈帧,直接把就着当前栈帧,把原先的数据覆盖即可。相对的,如果是普通的递归,函数在递归调用之前并没有完成全部计算,还需要调用递归函数完成后才能完成运算任务,比如return n * fact(n - 1);这句话,这个fact(n)在算完fact(n-1)之后才能得到n * fact(n - 1)的运算结果然后才能返回。
综上所述,编译器对尾递归的优化实际上就是当他发现你丫在做尾递归的时候,就不会去不断创建新的栈帧,而是就着当前的栈帧不断的去覆盖,一来防止栈溢出,二来节省了调用函数时创建栈帧的开销,用《算法精解》里面的原话就是:“When a compiler detects a call that is tail recursive, it overwrites the current activation record instead of pushing a new one onto the stack.”
目前编译器支持尾递归优化的语言有C,所以在其他语言下可以考虑将递归替换成迭代循环迭代来实现。
参考:
1. 浅谈尾递归
2. 递归与尾递归总结