JS求解: 斐波那契数列(Fibonacci Sequence)

216 阅读3分钟

定义

  • 斐波那契数意大利语:Successione di Fibonacci),又译为菲波拿契数菲波那西数斐氏数黄金分割数。所形成的数列称为斐波那契数列意大利语:Successione di Fibonacci),又译为菲波拿契数列菲波那西数列斐氏数列黄金分割数列。这个数列是由意大利数学家斐波那契在他的《算盘书》中提出。

数学上,斐波那契数是以递归的方法来定义:

F0 = 0

F1 = 1

Fn = Fn-1 + Fn-2 (n ≥ 2)

  • 用文字来说,就是斐波那契数列由0和1开始,之后的斐波那契数就是由之前的两数相加而得出。首几个斐波那契数是:1123581321345589144233377610、 987……(OEIS数列A000045
  • 特别指出0不是第一项,而是第零项。

Fn 的通项公式: Fn=15[(1+52)n(152)n]Fn = \frac 1 {\sqrt 5} \bigg[(\frac {1 + \sqrt 5} 2) ^ n - (\frac {1 - \sqrt 5} 2) ^ n \bigg]

斐波那契数列求解(多种)

解法一:递归

  • 时间复杂度: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)

那我们可以在实现代码时, 把计算过的值存起来等再次需要时直接使用不进行重复的计算

image.png

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, 可以直接丢弃掉以前计算过的值, 这样可以节省空间(降低空间复杂度)

image.png

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)

首先构建递推关系: [abcd][FnF(n1)]=[Fn+F(n1)F(n)]=[F(n+1)F(n)]\begin{bmatrix} a & b \\c & d\end{bmatrix} \begin{bmatrix} Fn \\ F(n-1)\end{bmatrix} = \begin{bmatrix} Fn + F(n-1) \\ F(n)\end{bmatrix} = \begin{bmatrix} F(n+1) \\ F(n)\end{bmatrix}

即: [F(n+1)F(n)]=[1110]n[F(1)F(0)]\begin{bmatrix} F(n+1) \\ F(n)\end{bmatrix} = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix} ^ n \begin{bmatrix} F(1) \\ F(0)\end{bmatrix}

令: M=[1110]M = \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}

普通写法, 时间复杂度: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 的通项公式: Fn=15[(1+52)n(152)n]Fn = \frac 1 {\sqrt 5} \bigg[(\frac {1 + \sqrt 5} 2) ^ n - (\frac {1 - \sqrt 5} 2) ^ n \bigg] 或者:Fn=15(1+52)n15(152)nFn =\frac 1 {\sqrt 5}(\frac {1 + \sqrt 5} 2) ^ n - \frac 1 {\sqrt 5}(\frac {1 - \sqrt 5} 2) ^ n

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 的增大依然可以保证值的正确性

参考文档

juejin.cn/post/706116…

zh.wikipedia.org/zh-hans/%E6…