斐波那契数 图解题解
这道题到底在问什么
- 输入
- n = 10
- 输出
- 55
最优解:为什么这么做
一句话答案:LeetCode 509 斐波那契数:F(0)=0、F(1)=1,往后每项是前两项之和 dp[i]=dp[i-1]+dp[i-2]。裸递归重复算子问题会指数爆炸,改自底向上后每格 O(1),两个变量滚动,时间 O(n)、空间 O(1)。
斐波那契数这道题到底在算什么
给一个整数 n,返回斐波那契数列的第 n 项 F(n)。规矩很简单:F(0)=0、F(1)=1,从第 2 项起每项等于前两项之和 F(n)=F(n-1)+F(n-2)。题目例子 n=10 答案 55,数列从头是 0、1、1、2、3、5、8、13、21、34、55。难在怎么算——同一个数列,写法不同能差出天上地下。
为什么照定义裸递归会慢到跑不动
最顺手的写法是照定义递归:fib(n) 返回 fib(n-1)+fib(n-2),n<2 返回 n。但它把同一个子问题反复重算。算 fib(5) 要 fib(4) 和 fib(3),fib(4) 又要 fib(3) 和 fib(2)——fib(3) 被算两遍。整棵调用树里,算一个 fib(5) 发生 15 次调用,fib(2) 重算 3 次、fib(1) 重算 5 次,结果却只是 5。
n 一大重复就滚成指数,时间复杂度 O(2^n)(大 O 记号描述规模变大时操作数怎么涨),算到 fib(50) 已是几十亿次调用。慢的根子是每个子问题都从零重算。
改成自底向上,dp[i] 存的就是 F(i)
既然子问题被重复算,就让每个只算一次、把结果存下来。定义 dp[i] 表示第 i 项 F(i),下标 i 就是项号。开头两格按定义填死 dp[0]=0、dp[1]=1,它们是种子,不套转移式(转移式就是由前几项推出当前项的式子,这里前面没有两格可加)。从第 2 项起每格照 dp[i]=dp[i-1]+dp[i-2] 往右推,用的都是已算好的前两格。
这就是自底向上:从最小子问题往大推,推到 dp[n] 就是答案。记忆化递归(自顶向下,算过的存表直接取)效果一样,都把 O(2^n) 压回 O(n)。
为什么整张表可以只留两个变量
算第 i 格只用得上紧挨的前两格。既然只需前两项,就没必要开整个数组,用两个变量轮着装即可——这叫滚动变量(只留最近几项、循环覆盖旧值)。
参考代码里 a、b 起初是 0 和 1,每轮做 a, b = b, a + b,相当于把窗口向右挪一格。循环跑完 b 就是 F(n),空间从 O(n) 降到 O(1)。
拿 n=10 亲手把整串数推一遍
两个种子先摆好 dp[0]=0、dp[1]=1。从第 2 项起每格是前两格相加:dp[2]=1+0=1;dp[3]=1+1=2;dp[4]=2+1=3;dp[5]=3+2=5;dp[6]=5+3=8;dp[7]=8+5=13;dp[8]=13+8=21;dp[9]=21+13=34;dp[10]=34+21=55。
推到 dp[10] 得 55,和题目答案对上;用滚动变量做,最后 b 也是 55。
复杂度是多少,n=0/1 的边界怎么收
从第 2 项推到第 n 项是一趟线性循环,每格一次加法,共 O(n) 次操作;只用 a、b 两个变量,空间 O(1)。
两个边界最容易踩坑。一是 n<2:n=0 返回 0、n=1 返回 1,直接返回 n,别硬套转移式访问不存在的前两格。二是起点别写反,dp[0]=0、dp[1]=1 反了整列会全错,照定义抄时最常见。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这条转移式——下面每一格都只是把它前两格相加。
- 4一行 11 格,列号就是下标 i(0 到 10)。dp 待填,从最左两格的起点开始。
- 5第 0 项按定义就是 0:dp[0]=0。
- 6第 1 项按定义是 1:dp[1]=1。两个起点定好,后面全靠相加。
- 7算 dp[2]:看它前面两格——dp[1]=1 和 dp[0]=0,把这两个加起来就是答案。
- 81 + 0 = 1,填进 dp[2]。每一项都是这样长出来的。
- 9算 dp[3]:看它前面两格——dp[2]=1 和 dp[1]=1,把这两个加起来就是答案。
- 101 + 1 = 2,填进 dp[3]。每一项都是这样长出来的。
- 11算 dp[4]:看它前面两格——dp[3]=2 和 dp[2]=1,把这两个加起来就是答案。
- 122 + 1 = 3,填进 dp[4]。每一项都是这样长出来的。
- 13算 dp[5]:看它前面两格——dp[4]=3 和 dp[3]=2,把这两个加起来就是答案。
- 143 + 2 = 5,填进 dp[5]。每一项都是这样长出来的。
- 15算 dp[6]:看它前面两格——dp[5]=5 和 dp[4]=3,把这两个加起来就是答案。
- 165 + 3 = 8,填进 dp[6]。每一项都是这样长出来的。
- 17算 dp[7]:看它前面两格——dp[6]=8 和 dp[5]=5,把这两个加起来就是答案。
- 188 + 5 = 13,填进 dp[7]。每一项都是这样长出来的。
- 19算 dp[8]:看它前面两格——dp[7]=13 和 dp[6]=8,把这两个加起来就是答案。
- 2013 + 8 = 21,填进 dp[8]。每一项都是这样长出来的。
- 21算 dp[9]:看它前面两格——dp[8]=21 和 dp[7]=13,把这两个加起来就是答案。
- 2221 + 13 = 34,填进 dp[9]。每一项都是这样长出来的。
- 23算 dp[10]:看它前面两格——dp[9]=34 和 dp[8]=21,把这两个加起来就是答案。
- 2434 + 21 = 55,填进 dp[10]。每一项都是这样长出来的。
- 25最右 dp[10]=55 就是 F(10)。整张表从左往右一路相加得来。
⚠️ 容易写错的地方
✗ 错:从 i=0 套转移式
✓ 对:dp[0]、dp[1] 是定义值,转移从 i=2 起
i<2 没有前两格
✗ 错:起点设反
✓ 对:dp[0]=0、dp[1]=1
反了整列全错
✗ 错:裸递归重复算
✓ 对:改自底向上或记忆化
裸递归是指数级 O(2^n)
完整代码(Python / C++ / Java)
Python
def fib(n):
if n < 2:
return n
a, b = 0, 1 # dp[i-2], dp[i-1]
for _ in range(2, n + 1):
a, b = b, a + b
return bC++
int fib(int n){
if(n < 2) return n;
int a = 0, b = 1;
for(int i = 2; i <= n; ++i){
int c = a + b;
a = b; b = c;
}
return b;
}Java
int fib(int n){
if(n < 2) return n;
int a = 0, b = 1;
for(int i = 2; i <= n; i++){
int c = a + b;
a = b; b = c;
}
return b;
}复杂度
时间
O(n)
一遍线性递推,每格 O(1) 一次加法
空间
O(1)
只需前两项滚动,不用整张 dp
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 斐波那契数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么 dp[i] 只依赖前两项,就能用两个变量优化到 O(1)?+
因为转移式 dp[i]=dp[i-1]+dp[i-2] 只用到相邻的前两格,dp[i-3] 及更早的值算完就再也用不到。既然任何时刻只需要最近两项,用 a、b 两个变量轮替存放即可,每算出一个新值就把窗口向右挪一格,整张 dp 数组不必留存,空间从 O(n) 降到 O(1),时间仍是 O(n)。
斐波那契和爬楼梯 LeetCode 70 是什么关系?+
转移式一模一样,都是 dp[i]=dp[i-1]+dp[i-2],区别只在起点。爬楼梯里 dp[0]=dp[1]=1(站在地面算一种空走法),斐波那契里 dp[0]=0、dp[1]=1。所以爬楼梯常被叫作斐波那契的「面试皮肤」,会了一题,另一题只需把两个种子换掉。
斐波那契有通项公式,能直接 O(1) 算出 F(n) 吗?+
理论上有比奈公式 F(n)=(φ^n-ψ^n)/√5,其中 φ=(1+√5)/2。但它带无理数和幂运算,浮点计算到 n 稍大就有精度误差,得到的未必是精确整数,而且求幂本身也不是真正的 O(1)。工程上仍以 O(n) 递推为准;若想更快又要保持整数精确,可用矩阵快速幂做到 O(log n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 斐波那契数 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。