1. 问题理解
业务目标:计算并返回斐波那契数列的第 $n$ 项的值。
数学定义:斐波那契数列的规则是,从第 2 项开始,每一项都等于前两项之和。即 $F(0) = 0$, $F(1) = 1$, 且当 $n \ge 2$ 时,$F(n) = F(n-1) + F(n-2)$。
输入输出:输入一个非负整数 $n$,输出其对应的斐波那契数。
2. 核心算法
这段代码的核心算法是带有状态压缩的动态规划(Dynamic Programming with State Compression)。
为什么不用递归? 传统的递归解法
return fib(n-1) + fib(n-2)会产生大量的重复计算,时间复杂度高达 $O(2^n)$。动态规划通过自底向上计算,将时间复杂度降到了 $O(N)$。为什么不用数组? 虽然标准的动态规划会使用一个长度为 $n+1$ 的
dp数组来保存所有历史状态,但根据公式 $F(n) = F(n-1) + F(n-2)$,我们发现计算当前状态只依赖于它前面的两个状态。更早的历史数据其实已经没用了。状态转移(滚动变量):
代码中没有开辟数组,而是巧妙地使用了三个变量:
a代表 $F(i-2)$,b代表 $F(i-1)$,c代表当前正在计算的 $F(i)$。每一次循环,算出新的
c之后,整个“计算窗口”就向前滑动一步:原来的b变成了新的a,算出来的c变成了新的b。这就完成了状态的无缝交接。
3. 关键点和边界条件
这是代码能够正确、健壮运行的最后一道防线:
边界条件(Base Cases):
if n < 2 { return n }:这是极其关键的第一步。因为斐波那契数列的第 0 项是 0,第 1 项是 1。当输入 $n=0$ 或 $n=1$ 时,无法凑齐“前两项”来做加法,所以必须直接返回 $n$ 本身。这不仅处理了特殊值,也防止了后续循环逻辑越界。
关键点 1:循环次数的控制:
代码中的循环是
for i := 1; i < n; i++。由于 $n=0$ 和 $n=1$ 已经被提前拦截,进入循环时 $n$ 至少为 2。当 $n=2$ 时,循环从 $i=1$ 到 $i<2$,只会执行 1次。此时
c = a(0) + b(1) = 1,完全符合 $F(2)=1$ 的结果。循环次数控制得非常精准。
关键点 2:Go 语言的多重赋值:
代码中
a, b = b, c这一行非常优雅。在很多其他语言中,你需要引入一个临时变量(比如temp = b; b = c; a = temp)来完成变量值的滚动。Go 的多重赋值在底层也是原子的,避免了值被覆盖的逻辑错误。
隐藏的考点:整型溢出(Integer Overflow):
虽然在单纯的算法题中通常不深究,但在实际工程中,斐波那契数列的增长速度是指数级的。Go 语言的
int类型(在 64 位机器上是int64)在计算到 $n=93$ 左右时就会发生溢出。如果面试官追问,你可以提到:如果 $n$ 非常大,需要将返回值改为*big.Int并使用 Go 的math/big标准库。
参考代码:
dp五部曲:
func fib(n int) int {
if n < 2 {
return n
}
//1.确定dp数组及其下标的含义:dp[i] 表示第 i 个斐波那契数的值
dp := make([]int, n+1)
//2.推导公式
//3.dp数组如何初始化
dp[0] = 0
dp[1] = 1
//4.确定遍历的顺序:从前往后遍历,因为后面的值依赖前面的值
for i := 2; i <= n; i++ {
dp[i] = dp[i-1] + dp[i-2]
}
//5.举例推导 dp 数组:当 n=5 时,dp 数组应该是 [0, 1, 1, 2, 3, 5]
return dp[n]
}带状态压缩:
func fib(n int) int {
if n < 2 {
return n
}
a, b, c := 0, 1, 0
for i := 1; i < n; i++ {
c = a + b
a, b = b, c
}
return c
}