尾递归优化

2019-06-04  本文已影响0人  最尾一名

尾调用

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

function f(x) {
  return g(x);            // 这是尾调用
}

// 情况一
function f(x) {
  const y = g(x);       // 这不是尾调用
  return y;         
}

// 情况二
function f(x) {
  return g(x) + 1;      // 这也不是尾调用
}

尾调用不一定出现在函数尾部,只要是最后一步操作即可。

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

尾调用优化

函数在调用时会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内部变量等信息。如果在函数A的内部调用函数 B,那么在 A 的调用记录上方,还会形成一个 B 的调用记录。等到 B 运行结束,将结果返回到 A,B 的调用记录才会消失。如果函数 B 内部还调用函数 C,那就还有一个 C 的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"
由于尾调用是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。

function f() {
  const m = 1;
  const n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

上面代码中,如果函数 g 不是尾调用,函数f就需要保存内部变量 m 和 n 的值、g 的调用位置等信息。但由于调用 g 之后,函数 f 就结束了,所以执行到最后一步,完全可以删除 f() 的调用记录,只保留 g(3) 的调用记录。

这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。


尾递归

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

function fib(n) {
  if (n === 0) return 0;
  if (n === 1) return 1;
  return fib(n - 1) + fib(n - 2);
}

上述是一个关于斐波那契数列的函数,计算斐波那契数列中下标为 n 的数,最多需要保存 2^n 个调用记录,复杂度为 O(2^n)。
如果改写成尾递归,只需要保留一个调用记录。

function fib(n, a = 0, b = 1) {
  if (n === 0) return a;
  return fib(n - 1, b, a + b);
} 

举个例子,求 fib(5) 会经历以下过程:

fib(5)
fib(5, 0, 1)
fib(4, 1, 1)
fib(3, 1, 2)
fib(2, 2, 3)
fib(1, 3, 5)
fib(0, 5, 8)
5

尾递归的实现

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,函数 fib 需要用到两个中间变量 a、b ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来。

解决办法:

参考链接

http://www.ruanyifeng.com/blog/2015/04/tail-call.html

上一篇下一篇

猜你喜欢

热点阅读