递归与尾递归

来源:互联网 发布:字符串的全排列 java 编辑:程序博客网 时间:2024/05/22 06:29

对于递归,我们都不陌生,简单的来说就是一个函数直接或间接的调用自己,一般来说,我们在使用递归算法的时候都是有条件的:

1.首先问题的规模是在不断缩小的,我们可以通过解决子问题从而来解决我们的原问题,而且子问题和原问题的解决方法都是一样的;

2.我们使用递归算法,在问题不断演化的过程中,必须有一种情况我们是能够解决,也就是必须得有我们的递归出口;

而对于递归,我们一般用它来解决三类问题:
1.数据的定义是递归的(如:斐波那契数列,N的阶乘)

2.解决问题的方法是递归的(如:回溯法--迷宫算法,二分查找的递归算法)

3.数据结构形式是递归的(如:二叉树的遍历,单链表的逆序打印)

http://blog.csdn.net/chenkaixin_1024/article/details/70198936这篇博文中有几道典型的递归算法的例题。

以上就是我们的递归,但是我们这篇博文的重点是我们的尾递归:

尾递归是我们递归当中的一种比较特殊的,顾名思义,它的递归调用放在我们函数的最末尾。

对于一般递归来说,由于递归调用之后还有其他语句,也就是说这次递归调用返回之后,后面还有其他操作,所以我们就得在递归调用的时候,形成新的栈帧结构,同时原本调用者的栈帧信息得保存下来,因为我们在这次递归调用结束返回之后,还得返回到调用者中进行其他操作,因此对于一般的递归,随着递归深度的增加,由于不停的调用函数,这里的运行时间就会大大增加,同时栈帧的开销会越来越大,而由于栈空间是有限的,所以如果一直递归下去,递归深度太深的话,就很容易造成我们的栈溢出了。

但是对于我们的尾递归,由于我们的递归调用是这个函数体的最后一步,后续没有其他操作了,所以我们就不需要再返回调用者的栈帧结构中了,而它的栈帧的相关信息我们就不需要在保存了,所对应的栈空间就可以被我们用来作为我们下一层调用的栈帧空间了,而在返回的时候就会直接跳过调用者返回了。这样一来我们的栈空间中就不用维护那么一层又一层的栈帧结构了,所以即使递归调用次数增多,也不太会造成栈溢出了。

所以尾递归是极其重要的,因为不用尾递归话,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈,某些情况下回造成栈溢出的窘境。

下面给出对应的例子,我们熟悉的斐波那契数列:

int Fib(int n){if(n<3)return 1;return Fib(n-1)+Fib(n-2);}void funtest(){cout<<Fib(5)<<endl;}int main(){funtest();return 0;}
进入Fib函数,其汇编如下:


很明显能够看到函数对于自身的调用。

当递归深度增大,令n=50,虽说在这里不会看到栈溢出,但是我们一运行就会发现半天都出不来结果,这里面就在一直不停的递归,不停的开辟栈空间,维护一个又一个的栈帧结构。

而我们如果把斐波那契数列写成尾递归的形式的话:

int Fib(int n,int first,int second){if(n<3)return second;return Fib(n-1,second,first+second);}void funtest(){cout<<Fib(50,1,1)<<endl;}int main(){funtest();return 0;}

这里要将我们的编译器从DeBug切换成Release,而且将编译器的项目属性中进行如下修改:常规->调试信息格式换成程序数据库 (/Zi),优化->优化换成使大小最小化 (/O1),这里是对于VS2010编译器。

进入Fib函数,其汇编如下:


很明显,我们发现在底层并没有发现call指令,也就是说并没出现之前的函数调用,反而出现了jne这个跳转指令,而跳转的位置正是我们前面执行过的位置,也就说在这里类似于用了一个循环,而并非进行函数调用,而循环结束之后,也就直接返回了,所以说我们的尾递归并没有开辟新的栈帧,而是在原栈帧的基础上执行操作,类似于循环。

增大n 的值,我们发现运算结果马上就能出来(这里为了让结果不要溢出,最后将int换成long long)。


对比上面的一般递归和尾递归,我们发现确实尾递归有着一般递归无法相比的优点,但是当我们在使用尾递归的时候,要注意的是递归次数越多,所面对的不是越来越简单的问题,而是越来越复杂的问题,因为参数里带有前面若干步的运算路径或者是运算结果。

0 0
原创粉丝点击