递归:recursion,尾递归:tail recursion
- 普通递归会不断地累积占用栈空间,当到达一个峰值之后,再不断减小;
1. 从普通递归到尾递归
尾递归和一般的递归不同在对内存的占用,普通递归创建 stack 累积而后计算收缩,尾递归只会占用恒量的内存(和迭代一样)。SICP 中描述了一个内存占用曲线,以 Python 代码为例(普通递归),求解前 N 个自然数的和:
def recursum(N):
if N == 1:
return N
return N + recursum(N-1)
如果在客户端调用recursum(5)
函数,Python 解释器将会按如下形式执行:
recursum(5)
5 + recursum(4)
5 + (4 + recursum(3))
5 + (4 + (3 + recursum(2)))
5 + (4 + (3 + (2 + recursum(1))))
5 + (4 + (3 + (2 + 1)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15
这个曲线就代表内存占用大小的峰值,从左到右,达到顶峰,再从右到左收缩。而我们通常不希望这样的事情发生(过于占用内存,但初值过大,递归调用层数够深时),所以使用迭代,只占据常量stack space(更新这个栈!而非扩展他)。
我们可以将该函数改造为尾递归版本:
def tail_recursum(n, running_total=0):
if n == 0:
return running_total
return tail_recursum(n-1, running_total + n)
客户端调用如下:
tail_recsum(5, 0)
tail_recsum(4, 5)
tail_recsum(3, 9)
tail_recsum(2, 12)
tail_recsum(1, 14)
tail_recsum(0, 15)
15
2. 尾递归的含义
尾递归就是从最后开始计算,每递归一次就算出相应的结果,也就是说,函数调用出现在调用者函数的尾部,因为是尾部, 所以根本没有必要去保存任何局部变量. 直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去。尾递归就是把当前的运算结果(或路径)放在参数里传给下层函数,深层函数所面对的不是越来越简单的问题,而是越来越复杂的问题,因为参数里带有前面若干步的运算路径。
尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。比如f(n, sum) = f(n-1) + value(n) + sum; 会保存n个函数调用堆栈,而使用尾递归f(n, sum) = f(n-1, sum+value(n)); 这样则只保留后一个函数堆栈即可,之前的可优化删去。
3. 将斐波那契改造为尾递归版
def tail_recur_fib(a, b, n):
if n <= 1: return b
return tail_recur_fib(b, a+b, n-1)