斐波那契数
LeetCode传送门剑指 Offer 10- I. 斐波那契数列
题目
斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F(0) = 0, F(1) = 1
F(n) = F(n - 1) + F(n - 2), for n > 1.
给你 n ,请计算 F(n) 。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
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).
Example:
Input: n = 3
Output: 2
Explanation: F(3) = F(2) + F(1) = 1 + 1 = 2.
Input: n = 4
Output: 3
Explanation: F(4) = F(3) + F(2) = 2 + 1 = 3.
Constraints:
0 <= n <= 100
思考线
解题思路
方法一:递归
这道题斐波那契数列最简单的方法肯定是递归呀,教程上的经典案例。
于是就有了我最初不经世事的一版
/**
* @param {number} n
* @return {number}
*/
var fib = function(n) {
if(n < 2) return n;
return (fib(n -1) + fib(n-2))%1000000007
};
然而执行结果是:超出时间限制。原因在于我们的约束条件是0 <= n <= 100,如果用上面的递归不进行任何优化处理,所需要的时间是非常久的,因为里面牵扯到了很多的重复计算。
下图相同的颜色代表会重复计算的点。
那如果我们使用一个数组来储存每一个计算过的值是不是就好很多了呢?由于递归是纵向的,所以我们可以在递归过程中一遍求解一遍保存对应的值,这样后面遇到相同的n的求解的时候我们直接返回对应的值就行了。
那么根据上面的思路我们得到了递归的优化版 -- 记忆递归法
/**
* @param {number} n
* @return {number}
*/
var fib = function(n) {
if(n < 2) return n;
if(cache[n]) return cache[n]
const res = (fib(n -1) + fib(n-2))%1000000007
cache[n] = res;
return res;
};
通过了,看来这个方法是可行的。那么我们还有没有其他的方法来解这道题呢?
既然给我们了递推公式,那么我们试着用动态规划来解决这道题。
方法二:动态规划
首先确定本题的递推初始条件 为 F(0) = 0, F(1) = 1,和递推公式来进行动态规划。
- 状态定义:设
dp为一维数组,其中dp[i]的值代表 斐波那契数列第i个数字 。 - 转移方程:
dp[i] = dp[i-1] + dp[i-2], 即对应数列定义f(n) = f(n-1) + f(n-2) - 初始状态:
dp = [0, 1] - 返回值: dp[n],即是 我们要找的第n个数字。
于是我们就有了第一版动态规划解答
/**
* @param {number} n
* @return {number}
*/
var fib = function(n) {
const MOD = 1000000007
const dp = [0, 1];
for (let i = 2; i <= n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2]) %MOD;
}
return dp[n];
};
这个解法的时间和空间复杂度都是O(n)
但是我们再观察,我们在循环里只用到了 dp[i-1] && dp[i-2],所以我们可以省去数组部分,使用变量 a,b来代表要此时的dp[i-1] dp[i-2],那么设初始值 a = 0, b = 1.
再根据我们的递推公式进行循环计算可以得到想要的解。
过程如下
/**
* @param {number} n
* @return {number}
*/
var fib = function(n) {
if(n < 2) return n;
let a = 0, b = 1;
const MOD = 1000000007
for(let i = 2; i <= n ; i ++) {
const res = (a + b)%MOD;
a = b;
b = res;
}
return b;
};
这样时间复杂度虽然没变化但是空间复杂度降到了常量级别为 O(1)
经过上面两个解法,我觉得我似乎已经满了,但是在你不知道的领域也许有魔法。
于是我就遇到了别人写的矩阵快速幂的解法。由于线性代数的知识,我已经完全还给了老师,所以在这里我专门整理了一份关于线性代数的基础知识。大家有需要的一起去阅读一下。 根据矩阵的乘法和快速幂的知识,我们来使用矩阵快速幂来解决这个问题。
方法三:矩阵快速幂
使用矩阵快速幂的好处是时间复杂度变低了,只需要O(log n) 的时间复杂度。
上面的两种解法,我们要计算f[n]时,需要将f[0]~f[n-1]的值都算一遍,因此需要线性的复杂度。使用矩阵快速幂来解答这个问题,能保证更加快速高效的找到答案。
我们的递推关系为f(n) = f(n - 1) + f(n - 2). 要求解f(n),我们只需要知道 f(n-1)和f(n-2)即可。
我们将因子构建成一个列向量:
而我们想要求得的结果的列向量为
利用递推关系,我们可以得到
根据矩阵乘法规则,我们可以得到:
根据递推公式我们再进行推导
根据矩阵运算的结合律,最终我们有
到此,我们已经将问题转化为求解 的问题。
我们可以使用矩阵基础知识中写好的矩阵快速幂的计算公式来解决本题。
代码实现如下
var fib = function (n) {
const MOD = 1000000007;
if (n === 0) return 0;
let mat = [
[1, 1],
[1, 0],
];
let res = [
[1, 0],
[0, 1],
];
let k = n - 1;
while (k > 0) {
if (k & 1) res = multi(res, mat);
mat = multi(mat, mat);
k >>= 1;
}
return res[0][0] % MOD;
};
// 计算矩阵相乘
function multi(a, b) {
const MOD = 1000000007;
const row1 = a.length,
row2 = b.length,
col1 = a[0].length,
col2 = b[0].length,
res = [];
if (col1 !== row2) {
return new Error('not a right matrix to multiply');
}
// 初始化结果数组
for (let i = 0; i < row1; i++) {
res[i] = new Array(col2).fill(0);
}
for (let i = 0; i < row1; i++) {
for (let j = 0; j < col2; j++) {
for (let k = 0; k < col1; k++) {
res[i][j] = (res[i][j] + a[i][k] * b[k][j]) % MOD;
}
}
}
return res;
// 下面是根据本题写的偷懒方案
/* return [
[
(((a[0][0] * b[0][0]) % MOD) + ((a[0][1] * b[0][1]) % MOD)) % MOD,
(((a[0][0] * b[1][0]) % MOD) + ((a[0][1] * b[1][1]) % MOD)) % MOD,
],
[
(((a[1][0] * b[0][0]) % MOD) + ((a[1][1] * b[0][1]) % MOD)) % MOD,
(((a[1][0] * b[1][0]) % MOD) + ((a[1][1] * b[1][1]) % MOD)) % MOD,
],
];
*/
}
这就是我对本题的解法,如果有疑问或者更好的解答方式,欢迎留言互动。