浅谈尾递归

今天在围观大神博客时,看到尾递归这个名词,这里对自己看到的做个总结。

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. 递归与尾递归总结

  

posted @ 2016-06-22 11:21  黎明露珠  阅读(582)  评论(0编辑  收藏  举报