从爬楼梯到记忆术:一文搞懂斐波那契与递归优化

117 阅读5分钟

从“爬楼梯”到“记忆术”:深入浅出理解斐波那契数列与递归优化

人生就像爬楼梯,每一步都建立在前两步的基础上。

你是否曾想过,一个看似简单的数学问题,竟能揭示程序设计中深刻的哲学?今天,我们就从一个耳熟能详的数列——509. 斐波那契数 - 力扣(LeetCode)出发,揭开递归重复计算缓存优化背后的秘密。无论你是编程新手,还是对算法心生畏惧的小白,这篇文章都会用生活中的比喻和清晰的逻辑,带你轻松穿越这道“算法之门”。


一、什么是斐波那契数列?

斐波那契数列(Fibonacci Sequence)是一个经典的整数序列:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

它的规则非常简单:

  • 第0项是 0
  • 第1项是 1
  • 从第2项开始,每一项都等于前两项之和

用数学公式表示就是:

f(0) = 0  
f(1) = 1  
f(n) = f(n-1) + f(n-2) (当 n ≥ 2

这个数列最早由意大利数学家斐波那契提出,用来描述兔子繁殖的理想模型。但如今,它早已超越生物学,成为计算机科学中最常被引用的例子之一。


二、最朴素的想法:递归实现

想象一下,你要计算第10级台阶的高度,而你只知道:每一级台阶的高度等于前两级之和。你会怎么做?

很自然地,你会想:“那我就先算第9级和第8级,再把它们加起来!”而要算第9级,又得先知道第8级和第7级……如此层层回溯,直到你回到最基础的第0级和第1级。

这种“自己调用自己”的思路,在编程中就叫递归

初学者的写法:

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

这段代码简洁优美,几乎就是数学公式的直接翻译。但问题来了:它太慢了!


三、为什么朴素递归会“爆炸”?

让我们以 fib(5) 为例,画出它的调用过程:

b6a3b5b60f4d12a9ed8cfaed9ffb2daa.png

你看,fib(3) 被算了两次,fib(2) 被算了三次!随着 n 增大,这种重复呈指数级增长。时间复杂度高达 O(2ⁿ) —— 这意味着计算 fib(100) 几乎永远等不到结果!

我们尝试着运行fib(100)

未命名的设计.gif

几乎不可能在合理时间内得到 fib(100) 的结果


四、优化之道:用“记忆术”避免重复劳动

人类聪明之处在于记住经验。程序员也一样——我们可以用一个“笔记本”(即缓存)把已经算过的值记下来,下次直接查,不用重算。

这种方法叫做记忆化(Memoization) ,核心思想是:用空间换时间

方法一:全局缓存

//创建一个空对象,用于存储 `{ n: fib(n) }` 的结果
const cache = {};

function fib(n) {
//查缓存:如果n已经算过,直接返回结果(避免重复工作)
  if (n in cache) {
    return cache[n];
  }
  if (n <= 1) {
    cache[n] = n;
    return n;
  }
  //递归计算:只在未缓存时计算,并把结果存起来供后续使用
  const result = fib(n - 1) + fib(n - 2);
  cache[n] = result;
  return result;
}

这里,cache 是一个对象,像一本小本子,记录着每个 n 对应的结果。第一次计算 fib(50) 很慢,但第二次调用几乎是瞬间完成!

此时我们再一次调用fib(100):

未命名的设计 (2).gif

瞬间完成!

就像你第一次做一道数学题花了1小时,但把解题步骤写在错题本上,下次考试直接翻本子,秒答!


五、更优雅的封装:闭包 + IIFE

但上面的 cache 是全局变量,容易被其他代码污染或误改。有没有办法把它“藏起来”,只让 fib 函数自己用?

答案是:闭包(Closure) + 立即执行函数(IIFE)

方法二:私有缓存(推荐写法)

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];
  };
})();

这段代码看起来有点“绕”,其实原理很简单:

  1. 外层 (function(){ ... })() 是一个立即执行函数,运行一次就创建了一个封闭的作用域。
  2. 在这个作用域里,cache 被定义为局部变量。
  3. 返回的内部函数(真正的 fib)能“记住”这个 cache,形成闭包
  4. 外部只能通过 fib(n) 调用,却看不到、改不了 cache

六、性能对比:天壤之别

方法时间复杂度能否计算 fib(100)特点
朴素递归O(2ⁿ)❌(卡死)简洁但低效
记忆化递归O(n)✅(毫秒级)高效、可读性强

记忆化递归将指数时间降为线性时间,这是算法优化的典型胜利!


七、总结:从小问题看大智慧

斐波那契数列虽小,却蕴含了计算机科学的核心思想:

  1. 分治思想:大问题拆成小问题(递归)。
  2. 避免重复:用缓存记住已解决的子问题(动态规划雏形)。
  3. 封装与安全:用闭包保护内部状态,提升代码健壮性。

当你下次看到 fib(100) 能瞬间输出 354224848179261915075 时,不要只惊叹数字之大,更要想到背后那套精巧的“记忆机制”——它不仅是代码的优化,更是人类智慧的缩影。

编程不是写代码,而是教机器如何聪明地思考。


附:完整可运行示例

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];
  };
})();

console.log(fib(100)); // 输出:354224848179261915075

现在,你也可以轻松驾驭“百级斐波那契”了!