【劍指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];
}
相關文章
相關標籤/搜索