斐波那契数列:前端面试中的“常客”与“变身怪”
嘿,各位前端的程序猿们!👋 是不是在面试中经常遇到斐波那契数列这个“老朋友”?它就像一个“变身怪”,时而以递归的优雅姿态出现,时而又化身为迭代的效率高手。今天,咱们就来好好“盘盘”它,看看如何在前端场景中,用最通俗易懂、最风趣幽默的方式,把它拿下!
✨ 什么是斐波那契数列?
在聊实现之前,咱们先来回顾一下这个神奇的数列。斐波那契数列(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)=0和F(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;:初始化pre1为F(0),pre2为F(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;:更新pre1和pre2,为下一次循环做准备。pre1变成原来的pre2,pre2变成当前的current。
优点:
- 性能极佳: 时间复杂度为 O(n),空间复杂度为 O(1),是效率最高的实现方式。
- 避免栈溢出: 不存在递归调用,自然也就没有栈溢出的风险。
总结:面试官,我全都要!
斐波那契数列虽然简单,但它却能很好地考察你对递归、迭代、动态规划以及性能优化的理解。在面试中,如果你能清晰地讲出这几种实现方式的优缺点,并能手写出代码,那绝对能让面试官眼前一亮!
- 递归: 优雅简洁,但性能堪忧,适用于
n较小的情况。 - 优化递归(记忆化搜索/动态规划): 空间换时间,性能大幅提升,解决了重复计算问题。
- 非递归(迭代): 效率最高,空间复杂度最低,是处理大
n值的最佳选择。
希望这篇博客能帮助大家更好地理解斐波那契数列,并在面试中轻松应对!如果你有其他有趣的实现方式或者对斐波那契数列的独到见解,欢迎在评论区留言分享哦!