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

}