尾调用优化和尾递归改写

Posted zerotensor

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了尾调用优化和尾递归改写相关的知识,希望对你有一定的参考价值。

1 尾调用

尾调用就是指某个函数的最后一步是调用另一个函数。

# 是尾调用
def f(x):
    return g(x)

# 不是尾调用,因为调用函数后还要执行加法,加法才是最后一步操作
def f(x):
    return 1+g(x)

2 尾调用优化

函数调用有一个调用栈,栈内保存了这个函数内部的变量信息。函数掉用就是切换不同的调用帧,从而保证每个函数有独立的运行环境。因为尾调用是函数的最后一步操作,所以在进入被尾调用函数之前并不需要保留外层函数的运行时环境,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。

尾递归

如果尾调用自身,就称为尾递归。递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。

def factorial(n) {
  if (n == 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

def factorial(n, total) {
  if (n == 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

4 递归函数的改写

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1?

两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。

def tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

def factorial(n) {
  return tailFactorial(n, 1);
}

factorial(5) // 120

上面代码通过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。

以上是关于尾调用优化和尾递归改写的主要内容,如果未能解决你的问题,请参考以下文章

递归和尾递归

JavaScript函数尾调用与尾递归

Swift 编程中的尾递归和蹦床译

如何看待以及理解Python的这种尾递归优化

学习了解递归和尾递归的区别

结合记忆和尾递归