求斐波拉契數(shù)列的第n項(xiàng)
寫一個(gè)函數(shù),輸入n,求斐波拉契數(shù)列的第n項(xiàng),斐波拉契數(shù)列的定義如下:
一般解法
long long Fibonacci(unsigned int n)
{
if (n<=0)
return 0;
if (n==1)
return 1;
return Fibonacci(n-1) + Fibonacci(n-2);
}
這是我們常用的遞歸函數(shù),但是上述遞歸的解法存在很嚴(yán)重的效率問(wèn)題
分析
我們以求解f(10)為例來(lái)分析遞歸的求解過(guò)程。想求得f(10),需要先求得f(9)和f(8),想求得f(9),需要先求得f(8)和f(7)。。。我們可以用樹形結(jié)構(gòu)來(lái)表示這種依賴關(guān)系,如下圖
可以發(fā)現(xiàn)這棵樹有很多節(jié)點(diǎn)是重復(fù)的,而且重復(fù)的節(jié)點(diǎn)數(shù)會(huì)隨著n的增大而急劇增加,這意味著計(jì)算量會(huì)隨著n的增大而急劇增大。事實(shí)上,用遞歸方法計(jì)算的時(shí)間復(fù)雜度是以n的指數(shù)的方式遞增的。
實(shí)用解法
上述代碼之所以慢,是因?yàn)橹貜?fù)的計(jì)算太多,我們只要想辦法避免重復(fù)計(jì)算就好。比如,我們可以把已經(jīng)得到的數(shù)列中間項(xiàng)保存起來(lái),在下次需要的時(shí)候我們先查找一下,如果前面已經(jīng)計(jì)算過(guò)就不再重復(fù)計(jì)算了。
更簡(jiǎn)單的辦法是從下往上計(jì)算,首先根據(jù)f(0)和f(1)算出f(2),再根據(jù)f(1)和f(2)算出f(3)。。。以此類推就可以算出第n項(xiàng)。這種思路的時(shí)間復(fù)雜度為O(n),實(shí)現(xiàn)如下:
long long Fibonacci(unsigned int n)
{
int result[2] = {0,1};
if (n<2)
return result[n];
long long fibNMinusOne = 0;
long long fibNMinusTwo = 1;
long long fibN = 0;
for (unsigned int i = 2; i <= n; i++) {
fibN = fibNMinusOne + fibNMinusTwo;
fibNMinusOne = fibNMinusTwo;
fibNMinusTwo = fibN;
}
return fibN;
}
使用矩陣
先看一個(gè)數(shù)學(xué)公式
有了這個(gè)公式,我們只需要求得矩陣的n-1次方
就可以得f(n)。現(xiàn)在的問(wèn)題轉(zhuǎn)換為如何求矩陣
乘方。如果簡(jiǎn)單地從0開始循環(huán),n次方將需要n次運(yùn)算,并不比前面的方法要快。
但我們可以考慮乘方的如下性質(zhì):
由公司可知,要求得n次方,我們先求得n/2次方,再把n/2的結(jié)果平方一下即可,這可以利用遞歸實(shí)現(xiàn)
完整的實(shí)現(xiàn)
#include <cassert>
struct Matrix2By2
{
Matrix2By2
(
long long m00 = 0,
long long m01 = 0,
long long m10 = 0,
long long m11 = 0
)
:m_00(m00), m_01(m01), m_10(m10), m_11(m11)
{
}
long long m_00;
long long m_01;
long long m_10;
long long m_11;
};
// 矩陣相乘
Matrix2By2 MatrixMultiply
(
const Matrix2By2& matrix1,
const Matrix2By2& matrix2
)
{
return Matrix2By2(
matrix1.m_00 * matrix2.m_00 + matrix1.m_01 * matrix2.m_10,
matrix1.m_00 * matrix2.m_01 + matrix1.m_01 * matrix2.m_11,
matrix1.m_10 * matrix2.m_00 + matrix1.m_11 * matrix2.m_10,
matrix1.m_10 * matrix2.m_01 + matrix1.m_11 * matrix2.m_11);
}
Matrix2By2 MatrixPower(unsigned int n)
{
assert(n > 0);
Matrix2By2 matrix;
if(n == 1)
{
matrix = Matrix2By2(1, 1, 1, 0);
}
else if(n % 2 == 0) // 偶數(shù)
{
matrix = MatrixPower(n / 2);
matrix = MatrixMultiply(matrix, matrix);
}
else if(n % 2 == 1) // 奇數(shù)
{
matrix = MatrixPower((n - 1) / 2);
matrix = MatrixMultiply(matrix, matrix);
matrix = MatrixMultiply(matrix, Matrix2By2(1, 1, 1, 0));
}
return matrix;
}
long long Fibonacci(unsigned int n)
{
int result[2] = {0, 1};
if(n < 2)
return result[n];
Matrix2By2 PowerNMinus2 = MatrixPower(n - 1);
return PowerNMinus2.m_00;
}
解法比較
用不同的方法求解斐波拉契數(shù)列的時(shí)間效率大不相同,第一種基于遞歸的解法,雖然直接但時(shí)間效率很低,在實(shí)際的軟件開發(fā)中不會(huì)用這種方法。
第二種方法把遞歸的算法用循環(huán)實(shí)現(xiàn),極大地提高了時(shí)間效率。
第三種方法把求斐波那契數(shù)列轉(zhuǎn)換成求矩陣的乘方,是一種很有創(chuàng)意的算法。雖然我們可以用O(logn)求的矩陣的n次方,但由于隱含的時(shí)間常熟較大,很少會(huì)有軟件采用這種算法。
相關(guān)問(wèn)題
青蛙跳臺(tái)階
一只青蛙一次可以跳上一級(jí)臺(tái)階,也可以跳上2級(jí)臺(tái)階。求該青蛙跳上一個(gè)n級(jí)的臺(tái)階總共有多少種跳法。
分析
首先我們考慮最簡(jiǎn)單的情況。如果只是1級(jí)臺(tái)階,那顯示只有一種條法。如果有2級(jí)臺(tái)階,那就有兩種條法;一種是分兩次跳,每次跳1級(jí);另一種就是一次跳2級(jí)。
我們把n級(jí)臺(tái)階時(shí)的跳法看成n的函數(shù)f(n)。當(dāng)n>2時(shí),第一次跳的時(shí)候就有兩種不同的選擇:一是第一次跳一級(jí),此時(shí)跳法數(shù)目等于后面剩下的n-1級(jí)臺(tái)階跳法的數(shù)目,即f(n-1),二是第一次跳2級(jí),此時(shí)跳法數(shù)目等于后面剩下的n-2級(jí)臺(tái)階的跳法數(shù)目,即為f(n-2)。因此,n級(jí)臺(tái)階的不同跳法的總數(shù)f(n)=f(n-1)+f(n-2),其實(shí)就是斐波拉契數(shù)列,如下面表達(dá)式所示
遞歸實(shí)現(xiàn)
long long Fibonacci(unsigned int n)
{
if (n<=0)
return 0;
if (n==1)
return 1;
if (n==2)
return 2;
return Fibonacci(n-1) + Fibonacci(n-2);
}
循環(huán)實(shí)現(xiàn)
long long Fibonacci(unsigned int n)
{
if (n<0)
return 0;
int result[3] = {0,1,2};
if (n<3)
return result[n];
long long fibNMinusOne = 1;
long long fibNMinusTwo = 2;
long long fibN = 0;
for (unsigned int i = 3; i <= n; i++) {
fibN = fibNMinusOne + fibNMinusTwo;
fibNMinusOne = fibNMinusTwo;
fibNMinusTwo = fibN;
}
return fibN;
}
矩形覆蓋問(wèn)題
我們可以用2x1的小矩形橫著或者豎著去覆蓋更大的矩形。問(wèn)用8個(gè)2x1的小矩陣無(wú)重疊地覆蓋一個(gè)2x8的大矩陣,共有多少種方法?
分析:
我們將2x8的覆蓋方法記為f(8)。用第一個(gè)小矩形覆蓋大矩形最左邊的時(shí)候,有兩種選擇:豎著放置或者橫著放置。
豎著放置的時(shí)候,右邊還剩下2x7的區(qū)域,記為f(7);
橫著放置的時(shí)候,左上角放置一個(gè),則對(duì)應(yīng)的左下角必須還有一個(gè)小矩陣,則右邊還剩下2x6的區(qū)域,記為f(6);
因此,f(8)=f(7)+f(6),可以看出,這仍然是斐波那契數(shù)列。
long long Fibonacci(unsigned int n)
{
if (n<=0)
return 0;
if (n==1)
return 1;
if (n==2)
return 2;
return Fibonacci(n-1) + Fibonacci(n-2);
}
參考
《劍指offer》