一、题目描述
The Fibonacci numbers, commonly denoted F(n) form a sequence, called the Fibonacci sequence,
such that each number is the sum of the two preceding ones, starting from 0 and 1. That is,
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), for N > 1.
Given N, calculate F(N).
Input: 2
Output: 1
Explanation: F(2) = F(1) + F(0) = 1 + 0 = 1.
二、题解
方法一:递归
- 如果 N 等于 1 或等于 2,则返回 1。
- 否则,通过 调用自身求解问题。
public int fib(int N) {
// if (N == 0) return 0;
// if (N == 1) return 1;
if (N == 1 || N == 2) return 1;
return fib(N - 1) + fib(N - 2);
}
复杂度分析
- 时间复杂度:
,整体算法复杂度就是子问题个数乘以解决一个子问题需要的时间。子问题个数,即递归树中的节点总数。
- 例如: 就是一个加法操作,时间为 O(1),但整颗递归树为一棵完全二叉树。
- 空间复杂度: ,栈空间随 N 增加而不断增长。
方法二:记忆化递归(自顶向下)
通过观察递归树可以发现暴力递归存在大量重复计算,这是导致算法低效的原因。如何解决? 既然有那么多重复计算,不如把这些重复计算的结果先记住,留给后面的的递归使用。
int[] memo = null;
public int fib(int N) {
memo = new int[N + 1];
return search(N);
}
private int search(int N) {
if (N == 1 || N == 2)
return 1;
if (memo[N] == 0)
memo[N] = search(N - 1) + search(N - 2);
return memo[N];
}
复杂度分析
- 时间复杂度: ,我们不再进行重复计算。
- 空间复杂度: ,使用了空间大小为 N 的数组。
方法三:动态规划(自底向上)
上面的记忆化搜索叫做「自顶向下」,动态规划叫做「自底向上」。
啥叫「自顶向下」?注意我们刚才画的递归树,都是从上向下延伸,从一个规模较大的原问题向规模较小的子问题递归。
- 比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) ,然后逐层返回答案,这就叫「自顶向下」
啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的 f(1) 和 f(2) 开始往上推,直到推到我们想要的答案 f(20),这就是动态规划的思路。这也是为什么动态规划一般都脱离递归,使用迭代完成计算。
啥叫「状态转移方程」?我们知道动态规划一般都会有一个描述问题结构的数学形式,这个状态转移就是这个数学方程的不同表现形式罢了。
public int fib2(int N) {
if (N <= 1) return N;
int[] memo = new int[N + 1];
memo[1] = memo[2] = 1;
for (int i = 3; i <= N; i++) {
memo[i] = memo[i - 1] + memo[i - 2];
}
return memo[N];
}
复杂度分析
- 时间复杂度: ,
- 空间复杂度: ,
方法四:dp(空间压缩)
其实我们不需要大小为 N 的 memo 数组来存储所有的状态,只需存储某个状态之前的两个状态即可。
public int fib3(int N) {
if (N <= 1) return N;
if (N == 2) return 1;
int curr = 0;
int pre1 = 1;
int pre2 = 1;
for (int i = 3; i <= N; i++) {
curr = pre1 + pre2;
pre2 = pre1;
pre1 = curr;
}
return curr;
}
复杂度分析
- 时间复杂度: ,
- 空间复杂度: ,
结语:这里好像没有展示动态规划的另一个重要特性 「最优子结构」,斐波那契数列的例子严格来说不算动态规划,因为没有涉及求最值的环节。