你們都知道斐波那契數列,如今要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項爲0)。
n<=39算法
解題前先簡單說明一下斐波那契數列,指的是這樣一個數列:一、一、二、三、五、八、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); } }
解法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都記錄了下來,當一個子問題被求解過之後,下一次就能夠直接經過索引訪問數組獲得,而避免了再次求解。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]; }
除了使用遞歸和動態規劃外,咱們還可使用矩陣來求解斐波那契數列。對於矩陣這裏再也不進行擴展,只介紹本算法會用到的基本概念。以下所示的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
對應的代碼以下遞歸
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]; }