开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
引子
楼梯有 N阶,上楼可以一步上一阶,也可以一步上二阶。
编一个程序,计算共有多少种不同的走法。
例:
- 0层:1种
- 1层:1种
- 2层:2种
- 3层:3种
- 4层:5种
- ...
洛谷题链(原题需要压位高精、矩阵快速幂,本文只讨论该问题的常规解法及优化思路)
解题思路
走楼梯,要么一次走一阶,要么一次走两阶,那我每次的走法就等于=上一阶的方案数+上上阶的方案种数,这就推出了方程:
f[i]=f[i-1]+f[i-2]
同时考虑边界:0阶或者1阶的答案都是1种。
由此将问题转化为求解Fibonacci数列(也叫兔子数列,黄金分割数列,斐波那契数列等)第n阶。
基操写法(递推)
我们已知第n阶等于第n-1阶加第n-2阶,直接使用循环递推即可
const getFibonacci = (n) => {
let fibonacciArr = [1,1];
if (n < 2) return a[n];
for (let i = 2; i <= n; i++) {
if (i < n) fibonacciArr.push(fibonacciArr[i - 1] + fibonacciArr[i - 2]);
else return fibonacciArr[i - 1] + fibonacciArr[i - 2];
}
};
复制代码
递推 + 滚动压缩
已知方程f[i]=f[i-1]+f[i-2]
只使用了i,i-1,i-2
所以可以滚动压缩,节省数组开销,得到 f[i%3]=f[(i+1)%3]+f[(i+2)%3]
const getFibonacci = (n) => {
let fibonacciArr = [1, 1, 2];
if (n < 3) return fibonacciArr[n];
for (let i = 3; i <= n; i++) {
fibonacciArr[i % 3] = fibonacciArr[(i - 1) % 3] + fibonacciArr[(i - 2) % 3];
if (i == n) return fibonacciArr[i % 3];
}
};
复制代码
基操写法(递规)
根据公式f[i]=f[i-1]+f[i-2]
,同时递归边界n==0||n==1,可以很容易地写出递归写法
const getFibonacci = (n) => {
if(n==0||n==1)return 1;
return getFibonacci(n - 1) + getFibonacci(n - 2);
};
复制代码
很明显,当n逐渐增大时,运算速度指数级递增(一般计算机在求解n=40时开始吃力),原因留在评论区由大家探讨
递归 + 记忆化搜索
递归写法最常见的问题就是重复搜索,所以可以使用记忆化搜索(将查到的结果存起来)进一步优化
let fibonacciData = { 0: 1, 1: 1 };
const getFibonacci = (n) => {
if (fibonacciData[n]) return fibonacciData[n];
return (fibonacciData[n] = getFibonacci(n - 1) + getFibonacci(n - 2));
};
复制代码
递归 + 滚动压缩
同理,可以通过滚动压缩对递归进行优化
const getFibonacci = (first, second, current, n) => {
if(n<2)return 1;
if (current == n) return first + second;
return getFibonacci(first + second, first, current + 1, n);
};
复制代码
柯里化
可以使用柯里化的缓存特性将计算结果储存起来
// 缓存函数
const cached = (fn) => {
const fibonacciData = {};
return (n) => {
if (!fibonacciData[n]) {
fibonacciData[n] = fn(n);
}
return fibonacciData[n];
};
};
const getFibonacci = (n) => {
if (n < 2) return 1;
return cachedGetFibonacci(n - 1) + cachedGetFibonacci(n - 2);
};
var cachedGetFibonacci = cached(getFibonacci);
console.log(cachedGetFibonacci(1000));
复制代码
取巧写法(通项公式)
可以使用通项公式(也叫比内公式):
const getFibonacci = (n) => {
n+=1;
return ((1 / Math.sqrt(5)) * (Math.pow((1 + Math.sqrt(5)) / 2, n) - Math.pow((1 - Math.sqrt(5)) / 2, n))).toFixed(0);
};
复制代码
这种写法实际上不具有代码研究价值,但是公式的推导实际上具有很高的研究意义(比较难所以不在文中列举了),因为是最高效的写法,所以放到最后来说
总结
可以看出,调优的主要方向有两个大类:节省空间,节省时间;
实际开发过程需要我们去综合空间和时间的实际情况去取舍和调优。
本文是高阶函数/函数抽象的一个引子,本人将在后续进行javaScript进阶相关知识的持续更新。