zoukankan      html  css  js  c++  java
  • 【剑指Offer】斐波那契数列

    题目描述

    大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
    n<=39

    解法1 递归

    解题前先简单说明一下斐波那契数列,指的是这样一个数列:1、1、2、3、5、8、13、21、34、……,因数学家列昂纳多·斐波那契以兔子繁殖为例子而引入,故又称为兔子数列。可以表示为F(n) = F(n-1) + F(n-2)。这道题在不考虑效率的情况下,最直接的解法是用递归,代码如下

    实现代码

    public int Fibonacci(int n)
    {
        if (n == 0)
        {
            return 0;
        }
        else if (n == 1 || n == 2)
        {
            return 1;
        }else
        {
            return Fibonacci(n - 1) + Fibonacci(n - 2);
        }
    }
    

    解法2 动态规划

    解法1使用递归虽然很直观,简单,但是效率太低。在n <= 39的情况下,运行时间为1277ms,究其原因还是算法中存在大量重复运算。以求解斐波那契数列第6项的过程来说明,如下图,在求解F6的过程中,F4会被重复计算2次,F3会被重复计算3次,这都导致了多余的消耗,且随着n越来越大冗余计算的增长是爆炸性的。
    斐波那契数列第6项
    递归的思想是自顶向下的,Fn的求解基于Fn-1和Fn-2,Fn-1的求解又基于Fn-2和Fn-3等等依次类推。而现在我们可以反过来,自底向上,在已知F1 = 1,F2 = 1的情况下求解F3,再利用F3和F2求解F4直到求出Fn。即不使用递归,使用循环迭代的方式。相比于解法1,优化后的算法运行时间只有39ms。

    实现代码

    public int FibonacciOptimize(int n)
    {
        if (n == 0)
        {
            return 0;
        }
        int fibl = 1, fibn = 1;
        for(int i = 2; i < n; i++)
        {
            fibn = fibl + fibn;
            fibl = fibn - fibl;
        }
        return fibn;
    }
    
    //或者是更简洁一点的写法
    
    public int FibonacciOptimize2(int n)
    {
        int f = 0, g = 1;
        while(n -- > 0)
        {
            g += f;
            f = g - f;
        }
        return f;
    }
    

    动态规划

    上面不使用递归,而使用循环的方式,我们可以给它起一个高大上的名字,动态规划。什么叫做动态规划呢,其实和它本身字面上的意思并没有太大关系。
    对于递归算法,编译器常常都只能做很低效的处理,递归算法如此慢的原因在于,编译器模拟的递归不能保留预先算出来的值,对已经求解过的子问题仍在递归的进行调用,导致了大量的冗余计算,比如上面的斐波那契递归算法。当我们想要改善这种情况时,可以将递归算法改成非递归算法,让后者把那些子问题的答案系统地记录下来,利用这种方法的一种技巧就叫做动态规划。比如上面的代码,我们都是用了两个变量把上一次的计算结果记录了下来,避免了重复计算。
    可能上面的算法对动态规划的体现并不是那么直观,可以看下面这段代码。我们用一个数组,将每次求解出来的Fn都记录了下来,当一个子问题被求解过以后,下一次就可以直接通过索引访问数组得到,而避免了再次求解。

    public int FibonacciOptimize3(int n)
    {
        if (n == 0)
        {
            return 0;
        }
        int[] array = new int[n + 1];
        array[0] = 1;
        array[1] = 1;
        for(int i = 2; i < n; i++)
        {
            array[i] = array[i - 1] + array[i - 2];
        }
        return array[n - 1];
    }
    

    解法3

    除了使用递归和动态规划外,我们还可以使用矩阵来求解斐波那契数列。对于矩阵这里不再进行扩展,只介绍本算法会用到的基本概念。如下所示的M就是一个2x2的矩阵,2行2列。

    [M = left[ egin{matrix} 1 & 2\ 3 & 4\ end{matrix} ight] ]

    矩阵和矩阵之间可以相乘,一个rxn的矩阵M和一个nxc的矩阵N相乘,它们的结果MN将会是一个rxc大小的矩阵。注意如果两个矩阵的行列不满足上面的规定,则这两个矩阵就不能相乘。怎样计算新的矩阵MN呢,可以用一个简单的方式描述:对于每个元素c~ij~,我们找到M中的第i行和N中的第j列,然后把它们对应元素相乘后再加起来,这个和就是c~ij~,对于有矩阵M,N如下

    [M = left[ egin{matrix} a & b\ c & d\ end{matrix} ight] N = left[ egin{matrix} e & f\ g & i\ end{matrix} ight] ]

    则MN为

    [MN = left[ egin{matrix} ae + bg & af + bi\ ce + dg & cf + di\ end{matrix} ight] ]

    那么斐波那契数列和矩阵有什么关系呢?
    我们已知斐波那契第n项,Fn = F(n - 1) + F(n - 2),可以将它们转换成如下所示的矩阵形式

    [left[ egin{matrix} F(n)\ F(n-1)\ end{matrix} ight] = left[ egin{matrix} F(n-1) + F(n-2)\ F(n-1)\ end{matrix} ight]= left[ egin{matrix} F(n-1) * 1 + F(n-2) * 1\ F(n-1) * 1 + F(n-2) * 0\ end{matrix} ight]= left[ egin{matrix} 1 & 1\ 1 & 0\ end{matrix} ight] left[ egin{matrix} F(n-1)\ F(n-2)\ end{matrix} ight] ]

    [left[ egin{matrix} F(n)\ F(n-1)\ end{matrix} ight] = left[ egin{matrix} 1 & 1\ 1 & 0\ end{matrix} ight] left[ egin{matrix} F(n-1)\ F(n-2)\ end{matrix} ight] ]

    [left[ egin{matrix} F(n-1)\ F(n-2)\ end{matrix} ight] = left[ egin{matrix} 1 & 1\ 1 & 0\ end{matrix} ight] left[ egin{matrix} F(n-2)\ F(n-3)\ end{matrix} ight] ]

    [left[ egin{matrix} F(n)\ F(n-1)\ end{matrix} ight] = left[ egin{matrix} 1 & 1\ 1 & 0\ end{matrix} ight] ^2 left[ egin{matrix} F(n-2)\ F(n-3)\ end{matrix} ight] ]

    以此类推

    [left[ egin{matrix} F(n)\ F(n-1)\ end{matrix} ight] = left[ egin{matrix} 1 & 1\ 1 & 0\ end{matrix} ight] ^{n-1} left[ egin{matrix} F(1)\ F(0)\ end{matrix} ight] ]

    所以要求斐波那契的第n项,我们只需要求得F1和F0构成的矩阵与特定矩阵的n-1次方相乘后的矩阵,然后取该矩阵的第一行第一列的元素值就是Fn
    现在引入了一个新的问题,怎样求特定矩阵的n-1次方,即矩阵的快速幂

    矩阵的快速幂

    在了解矩阵的快速幂之前,我们先看普通整数的快速幂
    求解整数m的n次方,一般是m^n^ = m * m * m .....,连乘n次,算法复杂度是O(n),这样的算法效率太低,我们可以通过减少相乘的次数来提高算法效率,即快速幂
    对于n我们可以用二进制表示,以14为例,14 = 1110

    [m^{14} = m^{1110} = m^{2^{3} * 1 + 2^{2} * 1 + 2^{1} * 1 + 2^{0} * 1} = m^{2^{3} * 1} * m^{2^{2} * 1} * m^{2^{1} * 1} * m^{2^{0} * 0} ]

    [= m^{8} * m^{4} * m^{2} * m^{0} = m^{8} * m^{4} * m^{2} * 1 ]

    可以发现这样的规律,指数n的二进制从低位到高位依次对应底数m的1次方,2次方,4次方,8次方...,当该二进制位是1的时候,则乘以底数对应的次方数,如果该二进制位是0,则表示乘以1。使用快速幂后,原本需要14次连乘,现在只需要4次连乘。
    那么怎样得到一个整数的二进制位呢,又怎样判断该二进制位是0还是1呢
    可以使用与运算和右移运算,例如对于14 = 1110

    • 和1按位与得到0,即第一个二进制位是0
    • 1110右移一位,得到0111,和1按位与得到1,即第二个二进制位是1
    • 0111右移一位,得到0011,和1按位与得到1,即第三个二进制位是1
    • 0011右移一位,得到0001,和1按位与得到1,即第四个二进制位是1
    • 0001右移一位,得到0000,等于0则,算法结束

    对应的代码如下

    public int pow(int m, int n)
    {
        int ret = 1;
        while(n > 0)
        {
            if ((n & 1) > 0)
            {
                ret = ret * m;
            }
            m *= m;
            n >>= 1;
        }
        return ret;
    }
    

    对应矩阵的快速幂就是

    // 简单实现了2*2矩阵的乘法
    public int[,] matrixMul(int[,] m, int[,] n)
    {
        int[,] ret = {
            { m[0,0] * n[0,0] + m[0,1] * n[1,0],  m[0,0] * n[0,1] + m[0,1] * n[1,1]} ,
            { m[1,0] * n[0,0] + m[1,1] * n[1,0],  m[1,0] * n[0,1] + m[1,1] * n[1,1]}
        };
        return ret;
    }
    // 矩阵的快速幂
    public int[,] matrixPow(int[,] m, int n)
    {
    	// 单位矩阵,作用相当于整数乘法中的1
        int[,] ret = { { 1, 0 }, { 0, 1 } };
        while(n > 0)
        {
            if ((n & 1) > 0)
            {
                ret = matrixMul(m, ret);
            }
            m = matrixMul(m, m);
            n >>= 1;
        }
        return ret;
    }
    

    实现代码

    在已经知道矩阵的快速幂之后,求解Fn就可以直接代入公式

    [left[ egin{matrix} F(n)\ F(n-1)\ end{matrix} ight] = left[ egin{matrix} 1 & 1\ 1 & 0\ end{matrix} ight] ^{n-1} left[ egin{matrix} F(1)\ F(0)\ end{matrix} ight] ]

    实现代码如下

    public int FibonacciOptimize4(int n)
    {
        if (n == 0)
        {
            return 0;
        }
        int[,] matrix = { { 1, 1 }, { 1, 0 } };
        // 这里的F1和F0矩阵多加了一列0,0,不会影响最终结果,是因为matrixMul只实现了2*2矩阵的乘法
        int[,] unit = { { 1, 0 }, { 0, 0 } };
        // 调用前面代码的矩阵乘法和矩阵快速幂
        int[,] ret = matrixMul(matrixPow(matrix, n - 1), unit);
        return ret[0, 0];
    }
    

    更多题目的完整描述,AC代码,以及解题思路请参考这里

  • 相关阅读:
    正则表达式
    javascript if(条件)------------条件中可以使用的值
    maven配置
    获取中文日期
    Oracle无安装客户端安装方法
    Eclipse org.eclipse.compare plug-in
    Differences or similarities between Java and C++
    Java Programming Guidelines
    Native Method
    Object in Java same as pointer
  • 原文地址:https://www.cnblogs.com/iwiniwin/p/10798884.html
Copyright © 2011-2022 走看看