【剑指Offer】斐波那契数列

题目描述

你们都知道斐波那契数列,如今要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0)。
n<=39算法

解法1 递归

解题前先简单说明一下斐波那契数列,指的是这样一个数列:一、一、二、三、五、八、1三、2一、3四、……,因数学家列昂纳多·斐波那契以兔子繁殖为例子而引入,故又称为兔子数列。能够表示为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都记录了下来,当一个子问题被求解过之后,下一次就能够直接经过索引访问数组获得,而避免了再次求解。spa

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[ \begin{matrix} 1 & 2\\ 3 & 4\\ \end{matrix} \right] \]
矩阵和矩阵之间能够相乘,一个rxn的矩阵M和一个nxc的矩阵N相乘,它们的结果MN将会是一个rxc大小的矩阵。注意若是两个矩阵的行列不知足上面的规定,则这两个矩阵就不能相乘。怎样计算新的矩阵MN呢,能够用一个简单的方式描述:对于每一个元素c~ij~,咱们找到M中的第i行和N中的第j列,而后把它们对应元素相乘后再加起来,这个和就是c~ij~,对于有矩阵M,N以下
\[M = \left[ \begin{matrix} a & b\\ c & d\\ \end{matrix} \right] N = \left[ \begin{matrix} e & f\\ g & i\\ \end{matrix} \right] \]
则MN为
\[MN = \left[ \begin{matrix} ae + bg & af + bi\\ ce + dg & cf + di\\ \end{matrix} \right] \]
那么斐波那契数列和矩阵有什么关系呢?
咱们已知斐波那契第n项,Fn = F(n - 1) + F(n - 2),能够将它们转换成以下所示的矩阵形式
\[ \left[ \begin{matrix} F(n)\\ F(n-1)\\ \end{matrix} \right] = \left[ \begin{matrix} F(n-1) + F(n-2)\\ F(n-1)\\ \end{matrix} \right]= \left[ \begin{matrix} F(n-1) * 1 + F(n-2) * 1\\ F(n-1) * 1 + F(n-2) * 0\\ \end{matrix} \right]= \left[ \begin{matrix} 1 & 1\\ 1 & 0\\ \end{matrix} \right] \left[ \begin{matrix} F(n-1)\\ F(n-2)\\ \end{matrix} \right] \]

\[ \left[ \begin{matrix} F(n)\\ F(n-1)\\ \end{matrix} \right] = \left[ \begin{matrix} 1 & 1\\ 1 & 0\\ \end{matrix} \right] \left[ \begin{matrix} F(n-1)\\ F(n-2)\\ \end{matrix} \right] \]
\[ \left[ \begin{matrix} F(n-1)\\ F(n-2)\\ \end{matrix} \right] = \left[ \begin{matrix} 1 & 1\\ 1 & 0\\ \end{matrix} \right] \left[ \begin{matrix} F(n-2)\\ F(n-3)\\ \end{matrix} \right] \]
\[ \left[ \begin{matrix} F(n)\\ F(n-1)\\ \end{matrix} \right] = \left[ \begin{matrix} 1 & 1\\ 1 & 0\\ \end{matrix} \right] ^2 \left[ \begin{matrix} F(n-2)\\ F(n-3)\\ \end{matrix} \right] \]
以此类推
\[ \left[ \begin{matrix} F(n)\\ F(n-1)\\ \end{matrix} \right] = \left[ \begin{matrix} 1 & 1\\ 1 & 0\\ \end{matrix} \right] ^{n-1} \left[ \begin{matrix} F(1)\\ F(0)\\ \end{matrix} \right] \]
因此要求斐波那契的第n项,咱们只须要求得F1和F0构成的矩阵与特定矩阵的n-1次方相乘后的矩阵,而后取该矩阵的第一行第一列的元素值就是Fn
如今引入了一个新的问题,怎样求特定矩阵的n-1次方,即矩阵的快速幂code

矩阵的快速幂

在了解矩阵的快速幂以前,咱们先看普通整数的快速幂
求解整数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 = 1110blog

  • 和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[ \begin{matrix} F(n)\\ F(n-1)\\ \end{matrix} \right] = \left[ \begin{matrix} 1 & 1\\ 1 & 0\\ \end{matrix} \right] ^{n-1} \left[ \begin{matrix} F(1)\\ F(0)\\ \end{matrix} \right] \]
实现代码以下ci

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];
}
相关文章
相关标签/搜索