斐波那契数列:从朴素递归到记忆化优化

37 阅读4分钟

斐波那契数列是算法学习中最经典的入门问题之一:从 0 和 1 开始,后续每一项都是前两项之和。其数学定义简洁优美——F(0) = 0F(1) = 1F(n) = F(n-1) + F(n-2)。然而,正是这份简洁背后,隐藏着性能陷阱与优化智慧。初学者常直接用递归实现,却很快发现它在计算稍大的 n(如 50)时变得极其缓慢,甚至导致栈溢出。本文将剖析这一现象,并展示如何通过缓存闭包将其转化为高效解法。

朴素递归:优雅但低效

最直观的实现方式是直接翻译数学公式:

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

这段代码逻辑清晰,完美体现了“分而治之”的递归思想。但其时间复杂度高达 O(2ⁿ) 。原因在于存在大量重复计算:例如计算 fib(5) 时,fib(3) 会被分别从 fib(4)fib(3) 的分支中多次调用。随着 n 增大,函数调用呈指数级爆炸,不仅耗时极长,还可能因调用栈过深而崩溃。

记忆化:用空间换时间

观察递归过程不难发现,许多子问题被反复求解。若能将已计算的结果保存下来,后续直接复用,即可避免冗余。这就是**记忆化(Memoization)**的核心思想。

const cache = {};
function fib(n) {
  if (n in cache) return cache[n];
  if (n <= 1) {
    cache[n] = n;
    return n;
  }
  cache[n] = fib(n - 1) + fib(n - 2);
  return cache[n];
}

这里引入一个全局对象 cache 存储中间结果。每次调用先检查缓存,命中则直接返回;否则计算并存入。此举将时间复杂度降至 O(n) ,因为每个 fib(k)(k ≤ n)仅计算一次。虽然增加了 O(n) 的空间开销,但换来的是数量级的性能提升——原本需数小时的 fib(100) 现在瞬间完成。

闭包封装:避免全局污染

上述方案依赖全局变量 cache,在复杂项目中易引发命名冲突或状态污染。更好的做法是利用闭包将缓存私有化。

const fib = (function() {
  const cache = {};
  return function(n) {
    if (n in cache) return cache[n];
    if (n <= 1) {
      cache[n] = n;
      return n;
    }
    cache[n] = fib(n - 1) + fib(n - 2);
    return cache[n];
  };
})();

这是一个立即执行函数表达式(IIFE) :外层函数定义后立即执行,返回内部函数并赋值给 fib。由于 JavaScript 的词法作用域机制,返回的函数始终能访问外层的 cache,而外部无法直接修改它。这样既实现了缓存复用,又保证了数据封装性,是函数式编程中常见的模式。

值得注意的是,内部递归调用仍写作 fib(n-1),而非 arguments.callee,因为 fib 已指向闭包返回的函数本身,形成正确的递归链。

递归的本质与适用边界

递归之所以适用于斐波那契,是因为问题具有最优子结构重叠子问题特性:大问题可分解为相同形式的小问题,且小问题被多次重复求解。这种树形分解结构天然契合函数调用栈的运行机制。

然而,递归并非万能。当问题规模过大或递归深度不可控时,即使经过记忆化,仍可能面临栈溢出风险。此时可进一步改写为动态规划的迭代版本,用循环代替递归,彻底规避调用栈限制。但在多数实际场景中,记忆化递归已足够高效且代码简洁。

总结

斐波那契数列虽简单,却是理解算法优化思维的绝佳载体。从朴素递归的指数爆炸,到记忆化的线性提速,再到闭包封装的工程实践,每一步都体现了“识别重复”“缓存结果”“隔离状态”的核心原则。这些思想不仅适用于此题,也广泛应用于爬虫去重、API 请求缓存、组件渲染优化等真实开发场景。掌握它,意味着你已迈出高效编程的第一步。