既然上一篇文章《Python实现斐波那契数列: 递归、尾递归、循环三种方法的比较》中提到了尾递归,那在这里就顺便研究一下尾递归;
那么什么是尾递归?如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
我来翻译一下,尾递归就是:1.递归调用必须在函数的尾部(最后执行);2.递归调用不能组成表达式语句;
那么首先我们看一下普通递归和尾递归,还以斐波那契数列为例:
# 方法一:直接用递归求斐波那契数列第n个数值
# 该种求法,n越大效率越低,因为每次求值,都会重新把前面的数算一遍
# 每次递归,都会重新创建栈,n较大时很容易爆栈
def feibona(n):
if n < 2:
return n
return feibona(n - 1) + feibona(n - 2)
# 方法二:使用尾递归求斐波那契数列第n个数值
# 该种求法,效率较高
def feibona_tial(n, a, b):
if n == 0:
return a
return feibona_tial(n - 1, b, a + b)
很明显,方法一普通递归最后一句,feibona(n - 1) + feibona(n - 2)两个递归组成了一个相加的表达式;
而方法二,则符合尾递归的定义;
那么尾递归相较于普通递归有哪些好处呢?
对于一般的递归而言,当递归层数太深的时候,很容易有爆栈的危险;
我们先看一下函数调用的过程:
1、调用开始前,调用方(或函数本身)会往栈上压相关的数据,参数,返回地址,局部变量等。
2、执行函数
3、清理栈中有关该函数的相关数据和返回;
因此对于普通递归,调用函数feibona(n),执行到最后一个表达式,调用函数feibona(n-1)和feibona(n-2);
feibona(n-1)又会调用到feibona((n-1)-1).....
在这个过程中,feibona(n)需要等待feibona(n-1)和feibona(n-2)的返回值才会释放资源,而feibona(n-1)又会等待feibona((n-1)-1)...;
因此,在普通递归过程中,所有层级的栈都会保留下来,直到最后一个层级返回数值后,才会一个层级一个层级释放资源;
尾递归的原理:
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。