递归和尾递归的比较,斐波那契
相信如果一个人让我们求一个斐波那契数列,如果你学过c语言,你一定会说用递归法啊,很容易就实现了,但是如果人家让你求斐波那契的第50个数,而且你对递归了解的话,估计帮你不会说递归了,如果了解够深的话,其实你会说递归也可以求出来。
1、递归
首先我们来说说什么是递归,简单的来说,就是一个函数需要调用自己来完成某种功能,这种调用就叫做递归。
但我们需要清楚一点,递归在使用的时候,并不是一直调用自己,我们需要给他一个停下来的时机。就像打仗一样,要知道进攻的路线,但如果遇到突发状况也要能及时撤退。所以我们的递归也一样,你需要给他一条前进路径也要给他一条返回路径。所以在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。
但我们都清楚,递归有它致命的缺点,在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,因此递归次数过多容易造成栈溢出,正是因为如此它和循环比较效率是很低的,这也就是我前面所说的如果让你计算斐波那契的第五十个数你直接用递归法去求其实是不太好的。
下面我们来看看程序:
int Fib(int n) { if( n < 2) return n; return (Fib(n-1)+Fib(n-2)); }
这样写出来的代码很简洁,来分析一下它的执行过程,我们给n=5:
可能这样你还看不出问题,其实上面的图相当是一个树状结构:
红色的部分在之后又会被求到,如果我们给的数值不是5是一个更大的数,则被重复计算和调用的数和次数会变得更多。可见,在这样一个过程中,我们把某些值一直在重复计算,再加上重复的开辟栈空间,使得它的效率变得非常低,你们可以试着求一下第40 50个斐波那契额。
2、尾递归
尾部递归是一种编程技巧。如果在递归函数中,递归调用返回的结果总被直接返回,则称为尾部递归。尾部递归的函数有助将算法转化成函数编程语言,而且从编译器角度来说,亦容易优化成为普通循环。这是因为从电脑的基本面来说,所有的循环都是利用重复移跳到代码的开头来实现的。如果有尾部归递,就只需要叠套一个堆栈,因为电脑只需要将函数的参数改变再重新调用一次。
1 int Fib(int n, int ret1, int ret2)
2
3 {
4 if (n ==0 )
5 {
6 return ret1;
7 }
8 else
9 {
10 return Fib(n - 1, ret2, ret1 +ret2);
11 }
12 }
它的执行步骤如下,每次的ret1就是要求当前的返回值,当执行到n减到0的时候,此时的ret1就是我们要求的第n个数:
这里我们在传参的时候需要传ret=0,ret2=1; Fib(n – 1, ret2, ret1 +ret2)的使用,原本朴素的递归产生的栈的层次像二叉树一样,以指数级增长,但是现在栈的层次却像是数组,变成线性增长了,实在是奇妙,总结起来也很简单,原本栈是先扩展开,然后边收拢边计算结果,现在却变成在调用自身的同时通过参数来计算。
ret1就是第n个数,而ret2就是第n与第n+1个数的和,这其实和我们的“迭代”殊途同归。
我们可以看看迭代写出来的斐波那契数列求法。
3、迭代法
int Fib(int n) { int num1 = 1; int num2 = num1; int num3 = num1; while (n > 2) { num3 = num1 + num2; num1 = num2; num2 = num3; n--; } return num3; }
可以看出迭代法实现的方法其实和我们的尾递归法一个道理,但是迭代法比较通俗易懂,而且和尾递归比较起来,因为不用开辟栈空间,所以这三种方法比较起来迭代法是效率最高的。我们在解决实际问题的时候,根据实际要求选择合适的方法。
版权声明:本文为MrListening原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。