从斐波那契开始了解尾递归
尾递归(tail recursive)
,看名字就知道是某种形式的递归。简单的说递归就是函数自己调用自己。那尾递归和递归之间的差别
就只能体现在参数
上了。
尾递归wiki
解释如下:
尾部递归是一种编程技巧
。递归函数是指一些会在函数内调用自己的函数,如果在递归函数中,递归调用返回的结果总被直接返回,则称为尾部递归。尾部递归的函数有助将算法转化成函数编程语言,而且从编译器角度来说,亦容易优化成为普通循环
。这是因为从电脑的基本面来说,所有的循环都是利用重复移跳到代码的开头来实现的。如果有尾部归递
,就只需要叠套一个堆栈
,因为电脑只需要将函数的参数改变再重新调用
一次。利用尾部递归最主要的目的是要优化,例如在Scheme语言中,明确规定必须针对尾部递归作优化。可见尾部递归的作用,是非常依赖于具体实现的。
我们还是从简单的斐波那契
开始了解尾递归
吧。
用普通的递归计算Fibonacci数列:
#include "stdio.h"
#include "math.h"
int factorial(int n);
int main(void)
{
int i, n, rs;
printf("请输入斐波那契数n:");
scanf("%d",&n);
rs = factorial(n);
printf("%d \n", rs);
return 0;
}
// 递归
int factorial(int n)
{
if(n <= 2)
{
return 1;
}
else
{
return factorial(n-1) + factorial(n-2);
}
}
程序运行结果如下:
请输入斐波那契数n:20
6765
Process returned 0 (0x0) execution time : 3.502 s
Press any key to continue.
上边的递归需要花费3.502s
。
下面我们看看如何用尾递归
实现斐波那契数:
#include "stdio.h"
#include "math.h"
int factorial(int n);
int main(void)
{
int i, n, rs;
printf("请输入斐波那契数n:");
scanf("%d",&n);
rs = factorial_tail(n, 1, 1);
printf("%d ", rs);
return 0;
}
int factorial_tail(int n,int acc1,int acc2)
{
if (n < 2)
{
return acc1;
}
else
{
return factorial_tail(n-1,acc2,acc1+acc2);
}
}
程序运行结果如下:
请输入斐波那契数n:20
6765
Process returned 0 (0x0) execution time : 1.460 s
Press any key to continue.
从上边的结果可以看到尾递归的效率比一般递归的效率高很多
。
我们可以打印一下程序的执行过程,函数加入下面的打印语句:
int factorial_tail(int n,int acc1,int acc2)
{
if (n < 2)
{
return acc1;
}
else
{
printf("factorial_tail(%d, %d, %d) \n",n-1,acc2,acc1+acc2);
return factorial_tail(n-1,acc2,acc1+acc2);
}
}
程序的运行结构如下:
请输入斐波那契数n:10
factorial_tail(9, 1, 2)
factorial_tail(8, 2, 3)
factorial_tail(7, 3, 5)
factorial_tail(6, 5, 8)
factorial_tail(5, 8, 13)
factorial_tail(4, 13, 21)
factorial_tail(3, 21, 34)
factorial_tail(2, 34, 55)
factorial_tail(1, 55, 89)
55
Process returned 0 (0x0) execution time : 1.393 s
Press any key to continue.
从上面的调试就可以很清晰地看出尾递归的计算过程了。acc1
就是第n个数
,而acc2
就是第n与第n-1个数
的和,这就是我们前面讲到的“迭代”
的精髓,计算结果参与到下一次的计算,从而减少很多重复计算量。
fibonacci(n-1,acc2,acc1+acc2)
真是神来之笔,原本朴素的递归产生的栈的层次像二叉树一样
,以指数级增长
,但是现在栈的层次却像是数组
,变成线性增长
了,实在是奇妙,总结起来也很简单,原本栈是先扩展开,然后边收拢边计算结果,现在却变成在调用自身的同时通过参数来计算
。
小结
尾递归的本质是:将单次计算的结果缓存起来
,传递给下次调用,相当于自动累积
。
在Java
等命令式语言
中,尾递归使用非常少见
,因为我们可以直接用循环解决。而在函数式语言
中,尾递归
却是一种神器
,要实现循环就靠它了。
很多人可能会有疑问,为什么尾递归也是递归,却不会造成栈溢出
呢?因为编译器通常都会对尾递归进行优化
。编译器会发现根本没有必要存储栈信息
了,因而会在函数尾直接清空相关的栈
。