1. 题目来源
# 1. 题目来源
509. 斐波那契数 (opens new window) 这题的要求是0 <= n <= 30 (用无脑递归可以通过) 剑指 Offer 10- I. 斐波那契数列 (opens new window) 这题的要求是0 <= n <= 100 (用无脑递归不可以通过)
# 2. 题目描述
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。 斐波那契数列的定义如下: F(0) = 0, F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中 N > 1. 斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
# 3. 题目解析
# 3.1 暴力递归
就直接暴力递归,虽然能解决问题,但是数字一大复杂度就特别高。
/**
* @param {number} n
* @return {number}
*/
var fib = function(n) {
if( n === 0) return 0
if(n === 1) return 1
return fib(n-1) + fib(n-2)
};
这种无脑解法可以初步解决问题,但是我们仔细思考一下,他的优化空间很大
首先一个问题就是这里重复递归的次数实在是太多了,比如说我要求fib(5), 如图可视,我们重复求解了多次fib(2)和fib(3)其实这是没有必要的,我们可以用一个缓存cache,来将我们求过了的fib(n)保存在缓存中,下次用到他的时候直接读取他的值就可以了,而不是再重新递归求值,
# 3.2 递归 + 缓存
/**
* @param {number} n
* @return {number}
*/
// 首先定义一个缓存数组,用来存放求出来的Fib(k)的值 k = 2, ... , n
let cache = []
var fib = function(n) {
// 1. 如果缓存中已经有这个值了,就直接返回,不要再递归求值了
if(cache[n] !== undefined){ return cache[n] }
// 两个初始值
if(n === 0) return 0
if(n === 1) return 1
// 递归求得fib(n)
let v = (fib(n-1) + fib(n-2)) %1000000007
// 将fib(n)保存在cache[n]的位置
cache[n] = v
// 返回fib(n)
return v
};
# 3.3 动态规划
加了缓存的递归虽然可以通过测试了,减少了重复的递归计算,但是利用了额外的空间来存储缓存,还有进一步优化的空间
var fib = function(n) {
if(n<2) return n;
let i = 0;
let j = 1;
let sum = 1;
// 每循环一次,将i和j的值相加得到sum放在j后面,将i和j都后移一位
for(let k = 2; k <= n; k++){
i = j;
j = sum;
// sum 是i和j的和 保存在i和j的后面
sum = (i + j) % 1000000007;
}
return sum ;
};
# 3.4 矩阵快速幂
Leetcode (opens new window)上面的题解!!!扩展思路!! 计算出 $M^n$ 之后, $F(n+1) = M^n[0][0]$ , $F(n) = M^n[1][0]$
var fib = function(n) {
if (n < 2) {
return n;
}
// 定义一个矩阵
const q = [[1, 1], [1, 0]];
// 求矩阵的 n-1 次幂
const res = pow(q, n - 1);
// 取矩阵左上方[0][0]的元素即为所求
return res[0][0];
};
// 定义矩阵幂运算
const pow = (a, n) => {
// 定义一个二维单位矩阵
let ret = [[1, 0], [0, 1]];
// 循环乘矩阵
while (n > 0) {
// 判断n的奇偶性,奇数为真,偶数为假
if ((n & 1) === 1) {
// n是奇数,最终一定会来这里
ret = multiply(ret, a);
}
// n右移一位,相当于除以二向下取整
n >>= 1;
// 幂运算 a做平方
a = multiply(a, a);
}
return ret;
}
// 定义矩阵乘法
const multiply = (a, b) => {
// 先定义一个全0的矩阵
const c = new Array(2).fill(0).map(() => new Array(2).fill(0));
for (let i = 0; i < 2; i++) {
for (let j = 0; j < 2; j++) {
// 根据矩阵的性值得到c的每一个位置的元素的值
c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
}
}
// 返回得到的乘积矩阵
return c;
}
时间复杂度:$O(\log n)$
空间复杂度:$O(1)$
# 3.5 通项公式
Leetcode (opens new window)上面的题解!!!扩展思路!!变成数学问题了~~
var fib = function(n) {
const sqrt5 = Math.sqrt(5);
const fibN = Math.pow((1 + sqrt5) / 2, n) - Math.pow((1 - sqrt5) / 2, n);
return Math.round(fibN / sqrt5);
};
# 4. 相关题目
# 第 N 个泰波那契数
1137. 第 N 个泰波那契数 (opens new window)
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n,请返回第 n 个泰波那契数 Tn 的值。
- 带缓存的递归
/**
* @param {number} n
* @return {number}
*/
let cache = []
var tribonacci = function(n) {
if(cache[n] !== undefined) return cache[n]
if( n < 2) return n
if( n === 2 ) return 1
let v = tribonacci(n-1) + tribonacci(n-2) + tribonacci(n-3)
cache[n] = v
return v
};
- 动态规划
var tribonacci = function(n) {
if( n < 2) return n
if( n === 2 ) return 1
let i = 0;
let j = 1;
let k = 1
let sum = 2;
// 每循环一次,将i和j的值相加得到sum放在j后面,将i和j都后移一位
for(let m = 4; m <= n; m++){
i = j;
j = k;
k = sum;
sum = (i + j + k);
}
return sum ;
};
# 爬楼梯
# 题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
# 题目分析
如何爬到第三层? 需要先爬到第二层,或者第一层,所以爬到第三层的方案数等于爬到第二层和爬到第一层的方案数之和, 也就是$F(3)=F(2)+F(1)$ 初始条件很好计算$F(2) = 2$,以及$F(1)=1$ 所以总结一下求第n个的方案数就是前两个方案数的和
$$F(n) = F(n-1) + F(n-2)$$
那就将 爬楼梯的问题 转换为了 斐波那契数列问题
# 代码
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
if(n<3) return n
let i = 1;
let j = 2;
let sum = 3;
for(let k = 4; k<=n; k++){
i = j
j = sum
sum = i + j
}
return sum;
};
# 使用最小花费爬楼梯
746. 使用最小花费爬楼梯 (opens new window)
# 题目描述
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
cost 的长度范围是 [2, 1000]。 cost[i] 将会是一个整型数据,范围为 [0, 999] 。
# 题目分析
和上一题一样,不过这次加了权值,而且让求的是最小花费
如果cost的长度是n,那最高层就是n 而cost下标范围为0,...,n-1
求到第n层的最小花费,如何到第n层,在第n-1层或者在第n-2层都可以到第n层,所以就要选这两种之间花费小的那个。 $$F(n) = min ( cost[n-1] + F(n-1) , cost[n-2] + F(n-2) )$$
接下来考虑起始的初始值
$$F(0) = 0$$ $$F(1) = 0$$
# 代码
/**
* @param {number[]} cost
* @return {number}
*/
var minCostClimbingStairs = function(cost) {
const N = cost.length;
let i = 0
let j = 0
let result = 0
for(let k = 2; k<=N; k++){
i = j
j = result
result = (i+cost[k-2]) < (j+cost[k-1]) ? (i+cost[k-2]) : (j+cost[k-1])
}
return result
};