【劍指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越來越大冗餘計算的增長是爆炸性的。

遞迴的思想是自頂向下的,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[ \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次方,即矩陣的快速冪
矩陣的快速冪
在瞭解矩陣的快速冪之前,我們先看普通整數的快速冪
求解整數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[ \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] \]
實現程式碼如下
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]; }