定义
- 斐波那契数(意大利语:Successione di Fibonacci),又译为菲波拿契数、菲波那西数、斐氏数、黄金分割数。所形成的数列称为斐波那契数列(意大利语:Successione di Fibonacci),又译为菲波拿契数列、菲波那西数列、斐氏数列、黄金分割数列。这个数列是由意大利数学家斐波那契在他的《算盘书》中提出。
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2 (n ≥ 2)
- 用文字来说,就是斐波那契数列由0和1开始,之后的斐波那契数就是由之前的两数相加而得出。首几个斐波那契数是:1、 1、 2、 3、 5、 8、 13、 21、 34、 55、 89、 144、 233、 377、 610、 987……(OEIS数列A000045)
- 特别指出:0不是第一项,而是第零项。
Fn 的通项公式:
斐波那契数列求解(多种)
解法一:递归
- 时间复杂度:O(2ⁿ)
- 空间复杂度:O(1)
- 原理: 斐波那契定义式
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2 (n ≥ 2)
function fib(n) {
if (n < 2) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
解法二:记忆化搜索(备忘录算法)
- 时间复杂度:O(n)
- 空间复杂度:O(n)
- 递归方法的优化版本
假如我们要计算 F5, 那我们计算过程是这样的(下图)
可以看出来相同的Fn (Fn-2) 会计算多次, F4(1) F3(2) F2(3) F1(5) F0(3)
那我们可以在实现代码时, 把计算过的值存起来等再次需要时直接使用不进行重复的计算
function fib(n, valueMap = new Map()) {
if (n < 2) {
return n;
}
// 查看是否缓存过值, 如果存在直接返回并结束
const value = valueMap.get(n);
if (value) {
return value;
}
const newValue = fib(n - 1, valueMap) + fib(n - 2, valueMap);
valueMap.set(n, newValue);
return newValue;
}
解法三:动态规划
- 时间复杂度:O(n)
- 空间复杂度:O(1)
- 原理分析: 递归是一种自顶向下求解的过程,那么我们能不能自低向上求解呢?
F0 = 0; F1 = 1; F2 = (F0 + F1) = 1; F3 = (F2 + F1) = 2; F4 = (F3 + F2) = 3; ......
因为 Fn = Fn-1 + Fn-2; 所有当我们计算到 Fn时, 我们需要的只是需要 Fn-1和Fn-2, 可以直接丢弃掉以前计算过的值, 这样可以节省空间(降低空间复杂度)
function fib(n) {
const baseN = 2; // 固定是 2
if (n < baseN) {
return n;
}
let [prev2, prev1] = [fib(0), fib(1)];
for (let i = baseN; i < n; i++) {
[prev2, prev1] = [prev1, prev2 + prev1];
}
return prev2 + prev1;
}
解法四:矩阵快速幂
时间复杂度:O(logn)
空间复杂度:O(1)
当时间复杂度为 O(n) 时, 我们一般可以通过快速幂的方式达到 O(logn)
首先构建递推关系:
即:
令:
普通写法, 时间复杂度:O(n)
// 矩阵乘法 [m*n] [n*k] = [m*k]
function multiply(arr0, arr1) {
// 可以根据矩阵的特性做一些校验
const result = [];
for (let i = 0; i < arr0.length; i++) {
const arrData = arr0[i];
const currArr = [];
for (let j = 0; j < arr1[0].length; j++) {
// 计算每一项的值
const data = arrData.reduce(
(prev, curr, currIdx) => prev + curr * arr1[currIdx][j]
, 0);
currArr.push(data);
}
result.push(currArr);
}
return result;
}
function fib(n) {
if (n < 2) {
return n;
}
const m = [
[1, 1],
[1, 0]
];
let result = [
[1, 0],
[0, 1],
];
for (let i = 0; i < n; i++) {
result = multiply(result, m);
}
return result[0][1];
}
使用快速幂写法, 时间复杂度:O(logn)
// 矩阵乘法 [m*n] [n*k] = [m*k]
function multiply(arr0, arr1) {
// 可以根据矩阵的特性做一些校验
const result = [];
for (let i = 0; i < arr0.length; i++) {
const arrData = arr0[i];
const currArr = [];
for (let j = 0; j < arr1[0].length; j++) {
// 计算每一项的值
const data = arrData.reduce(
(prev, curr, currIdx) => prev + curr * arr1[currIdx][j]
, 0);
currArr.push(data);
}
result.push(currArr);
}
return result;
}
function fib(n) {
if (n < 2) {
return n;
}
const m = [
[1, 1],
[1, 0]
];
let result = [
[1, 0],
[0, 1],
];
let temp = m;
while (n > 0) {
if ((n & 1) == 1) {
result = multiply(result, temp);
}
n >>= 1;
temp = multiply(temp, temp);
}
return result[0][1];
}
解法五:通项公式
原理: 使用通项公式
Fn 的通项公式: 或者:
function fib(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);
}
function fib(n) {
const sqrt5 = Math.sqrt(5);
const div1 = Math.pow((1 + sqrt5) / 2, n) / sqrt5;
// const div2 = Math.pow((1 - sqrt5) / 2, n) / sqrt5;
const div2 = 0;
return Math.round(div1 - div2);
}
解释一下为什么把 div2 直接设置为了 0 依然可以保证处理的正确性;
- 首先由于涉及到了小数计算, 存在精度问题, 我们使用 Math.round 获取整数 以保证我们计算结果的准确性
- 由于原表单式按照了通项公式, 所有理论上结果也是正确的
- 首先我们可以知道随着 n 的增大 div2 会变小. 当 n = 0 时, div2 ≈ 0.4472 取到最大值, 由于我们使用了 Math.round 所以即便我们 div2 = 0 获取到的最终值依然是正确的, 并且随着 n 的增大依然可以保证值的正确性