題目描述
大家都知道斐波那契數列,現在要求輸入一個整數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列。
矩陣和矩陣之間可以相乘,一個rxn的矩陣M和一個nxc的矩陣N相乘,它們的結果MN將會是一個rxc大小的矩陣。注意如果兩個矩陣的行列不滿足上面的規定,則這兩個矩陣就不能相乘。怎樣計算新的矩陣MN呢,可以用一個簡單的方式描述:對於每個元素c~ij~,我們找到M中的第i行和N中的第j列,然后把它們對應元素相乘后再加起來,這個和就是c~ij~,對於有矩陣M,N如下
則MN為
那么斐波那契數列和矩陣有什么關系呢?
我們已知斐波那契第n項,Fn = F(n - 1) + F(n - 2),可以將它們轉換成如下所示的矩陣形式
即
以此類推
所以要求斐波那契的第n項,我們只需要求得F1和F0構成的矩陣與特定矩陣的n-1次方相乘后的矩陣,然后取該矩陣的第一行第一列的元素值就是Fn
現在引入了一個新的問題,怎樣求特定矩陣的n-1次方,即矩陣的快速冪
矩陣的快速冪
在了解矩陣的快速冪之前,我們先看普通整數的快速冪
求解整數m的n次方,一般是m^n^ = m * m * m .....,連乘n次,算法復雜度是O(n),這樣的算法效率太低,我們可以通過減少相乘的次數來提高算法效率,即快速冪
對於n我們可以用二進制表示,以14為例,14 = 1110
可以發現這樣的規律,指數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就可以直接代入公式
實現代碼如下
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代碼,以及解題思路請參考這里