怎么解决递归 爆栈(尾递归和蹦床函数)

122 阅读2分钟

递归函数

当我们想计算n的阶乘的时候 我们想到的最简单的方法就是递归(空间复杂度和时间复杂度都是O(n)), 但是下面的解决方案 如果n很大的时候 函数调用栈就会爆满,从而报错

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

factorial(5) // 120

尾递归优化

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

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

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

// 等同于
g(3);

使用尾尾递归优化 优化阶乘函数, 此时空间复杂度O(1) 时间复杂度O(n) , 目前所有浏览器只有 Safari 有支持, V8不支持尾递归优化

function factorial(n, total) {
    "use strict"
   if (n === 1) return total;
   return factorial(n - 1, n * total);
}
factorial(5, 1);

蹦床函数

递归之所以导致函数调用栈爆满 主要原因就是 函数嵌套过多 导致第一层的函数无法释放,所以蹦床函数的思想就是 将嵌套的函数扁平化使得可以释放掉第一层的函数

function partial(fun, arg1) {
  return fun.bind(undefined, arg1);
}

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

// 所以我们手动调用第二次的时候 第一个函数在调用栈中就被删除了
factorial(3)
// function () { return factorial(3 - 1, 3) }
factorial(3)();
// function () { return factorial(2 - 1, 3 * 2) }
factorial(3)()();
// function () { return factorial(1 - 1, 1 * 3 * 2) }

上面我们手动调用factorial实现了扁平化 我们可以用代码实现它


function trampoline(f) {
  return function trampolined(...args) {
    let result = f.bind(null, ...args);

    while (typeof result === 'function') result = result();

    return result;
  };
}

const factorial = trampoline(function _factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return () => _factorial(n - 1, n * acc);
});

console.log(factorial(5));

参考文章

[译] 使用JavaScript中的蹦床函数实现安全递归

尾调用优化

蹦床(trampoline)原理