美文网首页
数据结构与算法(四):斐波那契数列

数据结构与算法(四):斐波那契数列

作者: 顶级蜗牛 | 来源:发表于2023-05-04 17:01 被阅读0次

    相关文献:
    数据结构与算法(一):基础理论
    数据结构与算法(二):线性表的实现
    数据结构与算法(三):线性表算法设计练习
    数据结构与算法(四):斐波那契数列
    数据结构与算法(五):LRU
    数据结构与算法(六):栈
    数据结构与算法(七):栈/队列的算法解题思想
    数据结构与算法(八):队列
    数据结构与算法(九):二叉树/线索化二叉树
    数据结构与算法(十):哈夫曼树

    题目

    假设⼀对兔⼦每年⽣⼀对孩⼦,兔宝宝在两年之后才能⻓⼤,才能有⾃⼰的孩⼦,且兔⼦永远不会死。那么N年后会有多少只兔⼦?

    斐波那契算法

    F(1) -> 1
    F(2) -> 1
    F(3) -> 2
    F(4) -> 3
    F(5) -> 5
    F(6) -> 8
    F(7) -> 13
    F(8) -> 21
    ...
    F(n) = F(n-1) + F(n-2)
    第N年后兔子的数量 = 第N-1年的数量 + 第N-2年的数量
    即前两数的和

    一、递归算法

    什么时候使用递归?

    • a.数学定义是递归的
    • b.数据的结构是递归的
    • c.问题的解法是递归的
    /**
     递归
     时间复杂度:O(2^n)。
     */
    int fib(int n) {
     if (n <= 2) return 1; // 叶子节点
     else return fib(n-1) + fib(n-2); // 枝节点
    }
    

    分析算法
    这个算法需要多⻓时间?这个算法需要多⻓性能?以执⾏的指令为标准

    分析出规律:

    • 叶子节点:在二叉树中没有子节点,即F(1)F(2) 相当于代码中的 return 1;
    • 枝节点:在二叉树中有子节点,相当于代码中的return fib(n-1) + fib(n-2);
    • F(n)的叶子节点数量等于F(n)的结果值(最终都以F(1)/F(2)返回1);
    • F(n)的枝节点数量等于F(n)底下的叶子节点数量-1

    得出:执⾏n次 = F(n) + 2F(n) - 2 即 3F(n) - 2

    为什么会这么慢?
    原因在于不断地⼀遍⼜⼀遍地重复计算已经知道答案的节点结果值。

    二、动态规划 Dynamic Program

    /**
     动态规划 DP
     时间复杂度:O(n)。
     空间复杂度:O(n)。
     */
    int fib2(int n) {
        int f[n+1];
        f[1] = f[2] = 1;
        for (int i = 3; i <= n; i++)
            f[i] = f[i-1] + f[i-2];
        return f[n];
    }
    

    把之前计算的结果,把它保存下来。

    • 弊端:需要去分析程序使⽤的内存量。⼀个程序虽然花费了很多时间,但是仍然可以运⾏它,只是需要等待更⻓的时间。
      但是,如果⼀个程序占⽤⼤量内存,可能根本⽆法运⾏它。

    需要降低空间复杂度:

    /**
     动态规划 DP
     时间复杂度:O(n)。
     空间复杂度:O(1)。
     */
    int fib3(int n){
        int a = 1, b = 1;
        for (int i = 3; i <= n; i++) {
            int c = a + b;
            a = b;
            b = c;
        }
        return b;
    }
    

    此时空间复杂度已经优化成O(1),但是时间复杂度还是O(n)。
    有没有一种可能去优化时间复杂度呢?通过矩阵

    三、矩阵法

    1.矩阵(matrix)定义
    2.用矩阵法分析斐波那契
    // 声明一个2X2的矩阵
    struct Matrix {
        int mat[2][2];
    };
    
    // 实现矩阵相乘
    struct Matrix matrixMultiply(struct Matrix* a, struct Matrix* b) {
        struct Matrix c;
        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 2; j++) {
                c.mat[i][j] = (*a).mat[i][0] * (*b).mat[0][j] + (*a).mat[i][1] * (*b).mat[1][j];
            }
        }
        return c;
    }
    
    // 实现斐波那契矩阵的n次方
    void matpow(int n, struct Matrix *m) {
        struct Matrix q;
        // q: {{1,1},{1,0}}
        q.mat[0][0] = q.mat[0][1] = q.mat[1][0] = 1;
        q.mat[1][1] = 0;
        for (int i = 0; i < n; i++)
            *m = matrixMultiply(m, &q);
    }
    
    1.矩阵法1

    实现上面的理论:

    /**
     矩阵
     时间复杂度:O(n)。
     空间复杂度:O(1)。
     */
    int fib4(int n) {
        if (n < 2) { return n; }
        struct Matrix ret; // 单位矩阵(任何矩阵乘以它,都等于本身){{1,0},{0,1}}
        ret.mat[0][0] = ret.mat[1][1] = 1;
        ret.mat[0][1] = ret.mat[1][0] = 0;
        matpow(n-1, &ret); // 矩阵q的n-1次方,得到第一个元素就是结果
        return ret.mat[0][0];
    }
    
    2.矩阵法2

    对于矩阵法1的时间复杂度的优化:

    我们可以把矩阵看成是基数2,那么结果可以简化看成是2的n次方;
    比如n=9的情况 就可以写成 (2的4次方)再平方再乘以2 即(16的平方)*2。

    为什么要这样呢?我们先计算出n/2的次方的结果,就不需要计算另一半的相同的运算啦。从而达到节约计算的时间。

    这里的2就代表着矩阵{{1,1},{1,0}}

    这种方式的话,就可以保存之前已经计算好的结果,就不需要重复计算。

    void matpow2(int n, struct Matrix *m) {
        if (n > 1) {
            matpow(n/2, m); // 先计算出一半次方的结果
            *m = matrixMultiply(m, m); // 再平方
        }
        if (n & 1) {
            // 判断最后一位如果是奇数的话
            // 再需要乘以一个{{1,1}, {1,0}}
            struct Matrix q;
            q.mat[0][0] = q.mat[0][1] = q.mat[1][0] = 1;
            q.mat[1][1] = 0;
            *m = matrixMultiply(m, &q);;
        }
    }
    
    /**
     矩阵
     时间复杂度:O(logn)。
     空间复杂度:O(1)。
     */
    int fib5(int n) {
        struct Matrix ret;
        ret.mat[0][0] = ret.mat[1][1] = 1;
        ret.mat[0][1] = ret.mat[1][0] = 0;
        matpow2(n-1, &ret);
        return ret.mat[0][0];
    }
    

    再继续优化,就是第四部分内容。

    四、通项公式

    斐波那契是有一个通项公式的:

    /**
     通项公式
     时间复杂度:O(1)。
     */
    int fib6(int n) {
        double sqrt5 = sqrt(5);
        double fibN = pow((1 + sqrt5) / 2, n) - pow((1 - sqrt5) / 2, n);
        return round(fibN / sqrt5);
    }
    

    学习算法的总结:

    先分析,分析啥?想啥就分析啥
    先按⾃⼰能理解的去实现
    观察,优化,再分析
    能⼒范围之内实现的有哪些?
    能⼒范围之外还有哪些好⽅式?学习,记录
    实际应⽤?结合具体场景分析
    并不⼀定最优秀的算法就是最合适的

    相关文章

      网友评论

          本文标题:数据结构与算法(四):斐波那契数列

          本文链接:https://www.haomeiwen.com/subject/rxapjdtx.html