递归求斐波那契的坑:重复计算如何让代码 “卡成 PPT”?

104 阅读6分钟

斐波那契数列:前端面试中的“常客”与“变身怪”

嘿,各位前端的程序猿们!👋 是不是在面试中经常遇到斐波那契数列这个“老朋友”?它就像一个“变身怪”,时而以递归的优雅姿态出现,时而又化身为迭代的效率高手。今天,咱们就来好好“盘盘”它,看看如何在前端场景中,用最通俗易懂、最风趣幽默的方式,把它拿下!

✨ 什么是斐波那契数列?

在聊实现之前,咱们先来回顾一下这个神奇的数列。斐波那契数列(Fibonacci sequence),又称黄金分割数列,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34……从第三项开始,每一项都等于前两项之和。用数学公式表示就是:

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

这个数列可不仅仅是数学家的“玩具”,它在大自然中无处不在,比如向日葵的螺旋排列、鹦鹉螺的生长曲线、树枝的分叉等等,都隐藏着斐波那契数列的影子。是不是很神奇?

斐波那契数列在自然界中的体现

🔄 递归实现:优雅,但要小心“坑”!

首先,我们来看看最直观、最符合数学定义的实现方式——递归。它的代码简洁得就像一首诗,完美地诠释了斐波那契数列的定义。

// 递归
function fn(n) {
  if (n === 0) return 0;
  if (n === 1) return 1;
  return fn(n - 2) + fn(n - 1);
}

代码解读:

  • if (n === 0) return 0;if (n === 1) return 1; 是递归的终止条件,就像给递归函数设置了“安全出口”,避免无限循环。
  • return fn(n - 2) + fn(n - 1); 则是递归的核心,它直接将问题分解为两个更小的子问题,然后将子问题的结果相加。

优点:

  • 代码简洁,易于理解: 几乎是斐波那契数列数学定义的“翻译版”。

缺点:

  • 性能问题: 递归实现会存在大量的重复计算。比如,计算 fn(5) 需要计算 fn(4)fn(3);而 fn(4) 又需要计算 fn(3)fn(2)。你会发现 fn(3) 被计算了两次!当 n 越大时,重复计算的次数呈指数级增长,导致性能急剧下降,甚至可能栈溢出。这就像你为了算一道题,反复去问同一个“笨蛋”同学好几遍,效率自然就低了。

递归重复计算示意图

别急,既然发现了“坑”,那咱们就得想办法“填坑”!下一节,我们来看看如何优化它。

🔧 优化递归:记忆化搜索,让“笨蛋”变“聪明”!

为了解决递归的重复计算问题,我们可以引入“记忆化搜索”或者说是“动态规划”的思想。简单来说,就是把已经计算过的结果保存起来,下次再需要的时候直接取用,避免重复计算。这就像给你的“笨蛋”同学配了个“小本本”,让他把算过的题都记下来,下次直接查本本就行了。

// 优化
function fibonacci2(n) {
  const arr = [0, 1]; // 注意:这里我把图片中的 [1, 1, 2] 改成了 [0, 1],更符合斐波那契数列的定义 F(0)=0, F(1)=1
  const arrLen = arr.length;

  if (n <= arrLen - 1) { // 对应 n=0 或 n=1 的情况
    return arr[n];
  }

  for (let i = arrLen; i <= n; i++) {
    arr.push(arr[i - 1] + arr[i - 2]);
  }

  return arr[n]; // 直接返回 arr[n] 即可,不需要 arr.length - 1
}

代码解读:

  • const arr = [0, 1];:我们用一个数组 arr 来存储已经计算过的斐波那契数。初始值为 F(0)=0F(1)=1
  • if (n <= arrLen - 1) { return arr[n]; }:如果 n 已经存在于 arr 中(即 n 为 0 或 1),直接返回对应的值,避免重复计算。
  • for (let i = arrLen; i <= n; i++) { arr.push(arr[i - 1] + arr[i - 2]); }:从 arr 已经有的长度开始,循环计算直到 n,并将结果依次存入 arr 中。这样,每个斐波那契数都只计算一次。
  • return arr[n];:最后直接返回 arr[n],就是我们想要的结果。

优点:

  • 性能大幅提升: 避免了重复计算,时间复杂度从指数级降低到线性级(O(n))。
  • 空间换时间: 用额外的空间(数组 arr)来存储中间结果,换取了更快的计算速度。 斐波那契数列总结

🚀 非递归实现:迭代,效率的“王者”!

除了递归和记忆化搜索,我们还有一种更直接、更高效的方式来实现斐波那契数列,那就是——迭代!它不需要额外的数组来存储所有中间结果,只需要维护几个变量即可,空间复杂度更低。

// 非递归
function fn(n) {
  let pre1 = 0; // F(0)
  let pre2 = 1; // F(1)
  let current = 0; // 当前斐波那契数

  if (n === 0) return 0;
  if (n === 1) return 1;

  for (let i = 2; i <= n; i++) {
    current = pre1 + pre2;
    pre1 = pre2;
    pre2 = current;
  }

  return current;
}

代码解读:

  • let pre1 = 0;let pre2 = 1;:初始化 pre1F(0)pre2F(1)
  • if (n === 0) return 0; if (n === 1) return 1;:处理 n 为 0 或 1 的特殊情况。
  • for (let i = 2; i <= n; i++) { ... }:从 i=2 开始循环,因为 F(0)F(1) 已经初始化。
  • current = pre1 + pre2;:计算当前的斐波那契数,它是前两个数的和。
  • pre1 = pre2;pre2 = current;:更新 pre1pre2,为下一次循环做准备。pre1 变成原来的 pre2pre2 变成当前的 current

优点:

  • 性能极佳: 时间复杂度为 O(n),空间复杂度为 O(1),是效率最高的实现方式。
  • 避免栈溢出: 不存在递归调用,自然也就没有栈溢出的风险。

总结:面试官,我全都要!

斐波那契数列虽然简单,但它却能很好地考察你对递归、迭代、动态规划以及性能优化的理解。在面试中,如果你能清晰地讲出这几种实现方式的优缺点,并能手写出代码,那绝对能让面试官眼前一亮!

  • 递归: 优雅简洁,但性能堪忧,适用于 n 较小的情况。
  • 优化递归(记忆化搜索/动态规划): 空间换时间,性能大幅提升,解决了重复计算问题。
  • 非递归(迭代): 效率最高,空间复杂度最低,是处理大 n 值的最佳选择。

希望这篇博客能帮助大家更好地理解斐波那契数列,并在面试中轻松应对!如果你有其他有趣的实现方式或者对斐波那契数列的独到见解,欢迎在评论区留言分享哦!