🔗 斐波那契数
📝 题目描述
斐波那契数形成的序列被称为斐波那契数列,该数列由0
和1
开始,后面的每一项数字都是前面两项数字的和。通常情况下,数列中的数用F(n)
表示,如下。
n >= 0
F(0) = 0, F(1) = 1
F(n) = F(n - 1) + F(n - 2)
,其中n > 1
现给定n
,请你计算出F(n)
。
📋 代码模版
class Solution {
public int fib(int n) {
}
}
function fib(n: number): number {
}
💡 提示
🚀 示例
🖊️ 题解
记忆化搜索
本题直接给出了⭐️斐波那契数列中第i
(1 < i <= n
)个数字值⭐️的方程式dfs
,递归公式为
综上所述,我们很容易写出以下代码。
class Solution {
public int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
}
function fib(n: number): number {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
😭但经过实践,这种算法的效率并不是很高。
那么,导致以上代码运行效率低下的原因是什么呢?接下来,让我们来分析一下它的时间复杂度。假设n = 5
,可以画出以下递归树。
递归树为一颗二叉树。其中,每个节点都代表一个子问题的计算,节点总数为指数级别。因此,我们可以得出结论:随着n
的增大,由于重复计算不断增加,子问题的个数也随之呈指数级增长。基于此,这个算法的时间复杂度为
⭐以上所述为多路递归的典型问题,即重复计算。为了应对这个问题,经典的解决方法是创建一个「备忘录」,在计算每个子问题之前先查找备忘录。如果备忘录中没有记录计算结果,就进行计算并将结果记录在备忘录中;反之,则直接获取记录的值并返回。
tip: 多路递归是指递归函数体中多次调用了自身。
这种带备忘录的递归算法通常也被称为记忆化搜索,对应本题的算法代码如下。
class Solution {
public int fib(int n) {
int[] memory = new int[n + 1];
return dfs(n, memory);
}
private int dfs(int i, int[] memory) {
if (i <= 1) {
return i;
}
if (memory[i] != 0) {
return memory[i];
}
return memory[i] = dfs(i - 1, memory) + dfs(i - 2, memory);
}
}
function fib(n: number): number {
const memory: number[] = [];
const dfs = (i: number): number => {
if (i <= 1) {
return i;
}
if (memory[i] !== undefined) {
return memory[i];
}
return memory[i] = dfs(i - 1) + dfs(i - 2);
}
return dfs(n);
}
记忆化搜索是一种典型的空间换时间的思想,它是在一颗存在大量冗余的递归树上通过「剪枝」,保证了每个相同的节点只计算一次。因此,该算法的时间复杂度为
动态规划
根据以上dfs
的解法,我们可以很轻松地将其翻译成动态规划dp
解法,具体步骤如下。
- 状态定义:设
为一维数组,其中 代表⭐️斐波那契数列中第 i
个数字的值⭐️。 - 初始状态:根据边界条件,初始化
数组的前两项,即 $dp[0] = 0, dp[1] = 1 $。 - 状态转移方程:
,其中 $ 2 \leq i \leq n$。 - 返回值:
,即 F(n)
的值。
class Solution {
public int fib(int n) {
if (n <= 0) {
return n;
}
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
function fib(n: number): number {
if (n <= 0) {
return n;
}
const dp: number[] = [];
dp[0] = 0;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
以上算法需要一个很长的DP table
(
这种技巧通常也被称为状态压缩,对应本题的算法代码如下。
class Solution {
public int fib(int n) {
if (n <= 0) {
return n;
}
int pre = 0, cur = 1;
for (int i = 2; i <= n; i++) {
int sum = pre + cur;
pre = cur;
cur = sum;
}
return cur;
}
}
function fib(n: number): number {
if (n <= 0) {
return n;
}
let pre = 0, cur = 1;
for (let i = 2; i <= n; i++) {
const sum = pre + cur;
pre = cur;
cur = sum;
}
return cur;
}
💭 复杂度分析
基于记忆化搜索
的解决方案的复杂度分析如下。
- 时间复杂度:
。 - 空间复杂度:
。
基于动态规划
的最优(状态压缩)解决方案的复杂度分析如下。
- 时间复杂度:
。 - 空间复杂度:
。