斐波那契数列是算法学习中的经典案例,也是前端面试中的高频考点。本文将从基础递归实现开始,逐步优化,带你深入理解递归、缓存、闭包等核心概念。
什么是斐波那契数列?
斐波那契数列是一个从 0 和 1 开始,后续每一项都等于前面两项之和的整数序列。
数学定义:
ini
f(0) = 0
f(1) = 1
f(n) = f(n-1) + f(n-2) (n ≥ 2)
方法一:基础递归实现
代码实现
javascript
// 递归
// 时间复杂度 O(2^n)
function fib(n) {
// 退出条件,若没有会爆栈
if(n <= 1) return n;
return fib(n-1) + fib(n-2);
}
console.log(fib(10)); // 55
核心思想
递归的本质:
- 大的问题可以分解为多个小的问题(类似)
- 自顶向下,思路清晰,代码简洁
- 靠函数入栈来实现,调用栈会占用栈内存
时间复杂度分析
O(2^n) - 指数级时间复杂度
为什么是指数级?
ini
fib(5)
/ \
fib(4) fib(3)
/ \ / \
fib(3) fib(2) fib(2) fib(1)
/ \ / \ / \
fib(2) fib(1) ...
可以看到,fib(3) 被计算了多次,fib(2) 被计算了更多次。每一层都会产生两个子问题,所以总的时间复杂度是指数级的。
问题分析
-
重复计算:同一个值被计算多次
fib(3)在计算fib(5)时被计算了 2 次fib(2)被计算了 3 次fib(1)被计算了 5 次
-
栈溢出风险:当 n 较大时,调用栈过深会导致爆栈
javascriptfib(100); // 可能会栈溢出 -
性能问题 :计算
fib(40)就需要几秒钟,fib(50)可能需要几分钟
方法二:缓存优化(记忆化)
代码实现
javascript
const cache = {}; // 用空间换时间
function fib(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;
}
console.log(fib(100)); // 354224848179262000000
优化原理
核心思想:用空间换时间
- 缓存已计算的结果 :使用
cache对象存储已经计算过的值 - 避免重复计算:如果计算过,直接在缓存中取,不用入栈那么多函数
- 时间复杂度优化:从 O(2^n) 降低到 O(n)
时间复杂度分析
O(n) - 线性时间复杂度
每个 fib(i) 只计算一次,然后存储在缓存中。后续需要时直接从缓存读取。
空间复杂度
O(n) - 需要存储 n 个计算结果
存在的问题
- 全局变量污染 :
cache是全局变量,可能被其他代码修改 - 封装性差:缓存逻辑暴露在外部
- 无法重置:一旦计算过,缓存会一直存在
方法三:闭包封装(推荐)
代码实现
javascript
// cache 闭合到函数中?
const fib = (function() {
// 闭包
// IIFE (Immediately Invoked Function Expression)
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)); // 354224848179262000000
核心概念解析
1. 闭包(Closure)
什么是闭包?
- 函数可以访问其外部作用域的变量
- 即使外部函数执行完毕,内部函数仍然可以访问外部变量
在这个例子中:
cache是外部函数的局部变量- 返回的内部函数可以访问
cache - 即使外部函数执行完毕,
cache仍然存在
2. IIFE(立即执行函数表达式)
IIFE 的作用:
- 创建一个独立的作用域
- 避免全局变量污染
- 封装私有变量
语法:
javascript
(function() {
// 代码
})()
优势分析
封装性好 :cache 是私有变量,外部无法访问
避免污染 :不会创建全局变量
代码优雅 :使用闭包和 IIFE,符合函数式编程思想
性能优秀:时间复杂度 O(n),空间复杂度 O(n)
方法四:迭代实现(最优解)
代码实现
javascript
function fib(n) {
if(n <= 1) return n;
let prev = 0; // f(0)
let curr = 1; // f(1)
// 从 f(2) 开始计算到 f(n)
for(let i = 2; i <= n; i++) {
const next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
console.log(fib(100)); // 354224848179262000000
优势
时间复杂度 :O(n) - 线性时间
空间复杂度 :O(1) - 只使用常数空间
不会栈溢出 :不使用递归,不会出现调用栈过深的问题
性能最优:比递归方法更快,内存占用更少
对比分析
| 方法 | 时间复杂度 | 空间复杂度 | 栈溢出风险 | 代码复杂度 |
|---|---|---|---|---|
| 基础递归 | O(2^n) | O(n) | 高 | 低 |
| 缓存递归 | O(n) | O(n) | 中 | 中 |
| 闭包递归 | O(n) | O(n) | 中 | 中 |
| 迭代 | O(n) | O(1) | 无 | 低 |
实际应用场景
1. 前端性能优化
在需要频繁计算斐波那契数的场景(如动画、游戏),使用缓存或迭代方法可以显著提升性能。
2. 算法面试
斐波那契数列是算法面试中的经典题目,考察点包括:
- 递归思想
- 时间复杂度分析
- 优化能力
- 闭包理解
3. 动态规划入门
斐波那契数列是理解动态规划(DP)的绝佳例子:
- 重叠子问题
- 最优子结构
- 状态转移方程
4. 前端动画
在某些动画效果中,可以使用斐波那契数列来创建自然的缓动效果。
总结
从朴素递归到记忆化缓存,从闭包封装到迭代优化,斐波那契数列看似简单,却像一面镜子,映照出编程思维的演进路径:从"能跑就行"到"优雅高效" 。
- 递归 教会我们如何将复杂问题分解,但也暴露了重复计算与栈溢出的隐患;
- 记忆化 引入"空间换时间"的经典策略,是动态规划思想的雏形;
- 闭包 + IIFE 展示了 JavaScript 的函数式魅力,在性能与封装之间取得平衡;
- 而 迭代解法 则回归本质------用最朴素的循环,实现最优的时间与空间复杂度。
这不仅是一道面试题,更是一次对算法思维、语言特性与工程实践的综合演练。在前端日益复杂的今天,理解这些底层逻辑,才能写出既健壮又高效的代码。
真正的优化,不在于炫技,而在于在正确的地方,选择最合适的解法。
记住:算法学习不是死记硬背,而是理解思想,灵活运用! 🚀