众所周知,函数调用例如A调用B,由于B执行结束后需要继续执行A,因此我们得把A在调用B时所处地址或者其他上下文信息进行保存。最常见的就是将地址保存在栈中。
若存在A->B->C,则此时栈内容依次存放函数A、B、C执行的信息。由于一个进程用于保存函数信息的栈容量有限,若类似的调用过多,则会导致栈溢出。
通常来说,正常的函数调用,其关系链并不会过长,因此通常不会出现这个问题。但对于递归函数这种情况,递归次数取决于问题规模,则很容易出现这个问题。
为了解决这个问题,聪明的程序员们提出了尾递归优化,在廖雪峰老师Python教程关于递归函数一篇中(这篇文章肯定不是最好的关于递归函数讲解的文章,只是刚好看到,所以拿来引用),有这么一句话
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
同时,廖雪峰老师还给出了相关的例子。对于求解某数阶乘,最普通的递归写法如下:
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
函数执行过程如下
===> fact(5)
===> 5 * fact(4)
===> 5 * (4 * fact(3))
===> 5 * (4 * (3 * fact(2)))
===> 5 * (4 * (3 * (2 * fact(1))))
===> 5 * (4 * (3 * (2 * 1)))
===> 5 * (4 * (3 * 2))
===> 5 * (4 * 6)
===> 5 * 24
===> 120
修改成尾递归的形式则如下:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
当然,你也可以把这两个函数融合成一个。函数执行过程大致如下(fact_iter函数):
===> fact_iter(5, 1)
===> fact_iter(4, 5)
===> fact_iter(3, 20)
===> fact_iter(2, 60)
===> fact_iter(1, 120)
===> 120
尾递归前后最大的变化就如同廖雪峰老师所言,函数返回仅包含一句自身调用,并不包含更多的表达式需要执行计算。因此,在这种情况下,实际上通过优化,我们可以让原来A->B->C 然后A<-B<-C的过程简化为A->B->C一次性。因为我们不需要拿着C函数给的信息回头继续执行B函数。放在递归函数中,即我们只需要一直向下执行即可,而不需要返回,因此我们也不需要保存沿途函数的栈帧。 这样自然不会栈溢出。
不过,以上只是理想状态。 通常我们把递归函数写成了可以被尾递归优化的形式,但具体能不能达到效果,还得看编译器。大多数编译器并不会因为我们的代码是可以被尾递归优化的,就进行优化,而是按照传统的函数调用方式,依旧去记录沿途经过的函数的栈帧,这样还是会栈溢出!
那么编译器是如何去优化这类代码,以实现真正的尾递归优化的呢?实际上这并不在本文的讨论当中,不过笔者看了一些文章,大致上就是将其转换为循环的形式,以减少栈的占用。 具体可参考《尾递归为啥能优化?》
// 全文完
因笔者能力有限,若文章内容存在错误或不恰当之处,欢迎留言、私信批评指正。
Email:YePeanut[at]foxmail.com