斐波那契数列 - 动态规划实现 详解笔记 - 实践

1. 问题描述

斐波那契数列(Fibonacci Sequence)是一个经典的数学序列,定义如下:

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2),其中 n ≥ 2

数列展示0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...


2. 动态规划思想

与递归相比,动态规划通过存储子问题的解避免重复计算,核心要素包括:

要素说明在本题中的应用
最优子结构问题的最优解包含子问题的最优解F(n) 的解依赖于 F(n-1)F(n-2)
记忆化将已计算的结果存储起来避免重复计算数组或变量存储中间结果
自底向上从基础情况开始逐步构建到目标问题F(0)F(1) 开始计算到 F(n)
空间优化根据状态依赖关系降低空间复杂度O(n) 优化到 O(1)

3. 算法对比

方法时间复杂度空间复杂度优缺点
递归(暴力)O(2ⁿ)O(n)❌ 重复计算极多,效率极低,指数级爆炸
动态规划(数组)O(n)O(n)✅ 高效,易理解,保留所有中间结果
动态规划(降维)O(n)O(1)✅ 最优解,空间效率最高,仅保留必要状态
矩阵快速幂O(log n)O(1)⚡ 最快但实现复杂,适合超大n,理论最优

4. 示例演示:计算 F(6)

4.1 方法1:使用数组(O(n)空间)

初始化数组:arr[0]=0, arr[1]=1
i=2: arr[2] = arr[1] + arr[0] = 1 + 0 = 1
i=3: arr[3] = arr[2] + arr[1] = 1 + 1 = 2
i=4: arr[4] = arr[3] + arr[2] = 2 + 1 = 3
i=5: arr[5] = arr[4] + arr[3] = 3 + 2 = 5
i=6: arr[6] = arr[5] + arr[4] = 5 + 3 = 8
结果:F(6) = 8
数组状态:[0, 1, 1, 2, 3, 5, 8]

4.2 方法2:降维优化(O(1)空间)

初始化:i=0 (F), j=1 (F)
k=2: c = 0+1=1,  更新 i=1, j=1  (计算F)
k=3: c = 1+1=2,  更新 i=1, j=2  (计算F)
k=4: c = 1+2=3,  更新 i=2, j=3  (计算F)
k=5: c = 2+3=5,  更新 i=3, j=5  (计算F)
k=6: c = 3+5=8,  更新 i=5, j=8  (计算F)
结果:F(6) = 8
内存占用:仅需3int变量!

5. 应用场景

  • 爬楼梯问题:每次爬1或2阶,求到达n阶的方法数
  • 兔子繁殖问题:经典的斐波那契起源问题
  • 黄金分割:相邻两项比值趋近于黄金分割比 φ ≈ 1.618
  • 艺术设计:斐波那契螺旋线在自然界和艺术中广泛存在
  • 算法优化:学习动态规划的经典入门案例
  • 植物生长:树叶的排列、花瓣数量等自然现象
  • 金融分析:技术分析中的斐波那契回调线

6. 代码实现详解

6.1 方法1:动态规划 - 使用数组存储

/**
* 使用数组实现斐波那契数列
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
public static int fibonacci(int n) {
// 创建数组存储所有斐波那契数
int[] arr = new int[n + 1];
// 初始化前两项
arr[0] = 0;
arr[1] = 1;
// 处理边界情况
if (n < 2) {
return arr[n];
}
// 自底向上计算每一项
for (int i = 2; i <= n; i++) {
arr[i] = arr[i - 1] + arr[i - 2];  // 状态转移方程
}
return arr[n];
}

优点

  • 逻辑清晰,易于理解
  • 保留了所有中间结果,便于调试
  • 如果需要多次查询不同的 F(i),只需计算一次

缺点

  • 空间复杂度较高,对于超大 n 可能内存不足
  • 计算单个 F(n) 时存储了大量不必要的中间结果

6.2 方法2:动态规划 - 空间优化(降维)

/**
* 使用降维优化实现斐波那契数列
* 时间复杂度:O(n)
* 空间复杂度:O(1) - 只使用常数个变量
*/
public static int fibonacciJW(int n) {
// 处理边界情况:F(0) = 0
if (n == 0) {
return 0;
}
// 处理边界情况:F(1) = 1
if (n == 1) {
return 1;
}
// 初始化两个变量:i表示F(k-2),j表示F(k-1)
int i = 0;  // F(0)
int j = 1;  // F(1)
// 从第2项开始滚动计算到第n项
for (int k = 2; k <= n; k++) {
int c = i + j;  // 计算当前项 F(k) = F(k-1) + F(k-2)
i = j;          // 向前滚动:F(k-2) = 旧的F(k-1)
j = c;          // 向前滚动:F(k-1) = 新计算的F(k)
}
// 循环结束后,j 就是 F(n)
return j;
}

核心思想
观察到计算 F(n) 只需要 F(n-1)F(n-2),不需要保存所有历史值。使用两个变量滚动更新,将空间复杂度从 O(n) 降到 O(1)

变量含义

  • i:表示 F(k-2),前前一项
  • j:表示 F(k-1),前一项
  • c:表示 F(k),当前项 = i + j

⚠️ 注意事项

  • 变量更新顺序很重要:先算 c,再更新 ij
  • 循环结束后,j 存储的就是 F(n)
  • 如果需要保留所有中间结果,应使用方法1

7. 测试方法

public static void main(String[] args) {
// 测试用例:计算 F(21)
// 期望输出:10946(两种方法结果相同)
System.out.println(fibonacci(21));      // 使用数组方法
System.out.println(fibonacciJW(21));    // 使用降维方法
}

测试结果验证

10946
10946

F(21) 完整数列
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946


8. 复杂度对比总结

实现方式时间复杂度空间复杂度推荐场景
数组存储O(n)O(n)需要多次查询不同F(i),或需要调试中间状态
降维优化O(n)O(1)仅需计算单个F(n),追求极致空间效率
矩阵快速幂O(log n)O(1)n极大(如10⁹)时,对时间要求苛刻的场景

9. 扩展思考

9.1 如何选择实现方式?

  • n ≤ 10⁶:优先使用降维优化方法,简单高效
  • 需要多次查询:使用数组方法,空间换时间
  • n极大(10⁹级别):考虑矩阵快速幂通项公式
  • 教学演示:从递归数组DP降维逐步演进

9.2 从斐波那契到动态规划

斐波那契是学习DP的最佳入门案例

  1. ✅ 有明确的状态转移方程
  2. ✅ 具备最优子结构特性
  3. ✅ 存在重叠子问题(递归会重复计算)
  4. ✅ 容易进行空间优化

9.3 性能测试建议

// 测试大数值性能(注意递归会栈溢出)
long start = System.currentTimeMillis();
System.out.println(fibonacciJW(1000000)); // 计算F(100万)
long end = System.currentTimeMillis();
System.out.println("耗时: " + (end - start) + "ms"); // 约10-20ms

10. ✅ 总结

10.1 核心要点

  1. 状态转移F(n) = F(n-1) + F(n-2) 是动态规划的精髓
  2. 自底向上:从已知到未知,避免递归的重复计算
  3. 空间优化:观察状态依赖,将 O(n) 空间降至 O(1)
  4. 边界处理F(0)=0F(1)=1 是计算基础

10.2 学习建议

  • 初学者:先掌握数组方法,理解DP流程
  • 进阶:挑战降维优化,理解状态压缩思想
  • 高阶:尝试实现矩阵快速幂,探索O(log n)解法
  • 实践:用斐波那契解决爬楼梯、兔子繁殖等实际问题

10.3 代码优势

  • 清晰注释:每一步都有详细说明
  • 两种实现:对比学习,理解空间优化
  • 边界完备:处理了 n=0n=1 的特殊情况
  • 教学友好:适合作为动态规划的入门案例

11. 相关题目推荐

题目难度关联度核心思想
LeetCode 70 - 爬楼梯Easy★★★★★斐波那契的直接应用
LeetCode 509 - 斐波那契数Easy★★★★★本题的官方版本
LeetCode 746 - 使用最小花费爬楼梯Easy★★★★☆变体DP问题
LeetCode 1137 - 第N个泰波那契数Easy★★★☆☆三项递推,思路类似
posted @ 2025-12-15 19:57  clnchanpin  阅读(117)  评论(0)    收藏  举报