數據結構與算法 | 【斐波那契數列與遞歸】

斐波那契數列(Fibonacci sequence),又稱黃金分割數列,當n趨向於無窮大時,前一項與後一項的比值越來越逼近黃金分割0.618(或者說後一項與前一項的比值小數部分越來越逼近 0.618)。
limn+anan+1= 512 \lim_{n \to +\infty} \frac{a_n}{a_{n+1}} \quad = \ \frac{\sqrt{5}-1}{2}
黃金比例和斐波那契數列的數學意義密切相關,在我們的生活中小到細胞分裂、花瓣的排列紋路,大到人口密度和土地面積測繪,甚至是宇宙星系都有着斐波那契數列的身影。

有關斐波那契數列的數學定理和相關公式請參考斐波那契數列——百度詞條。其中斐波那契數列的排列方式很有趣,前兩項的和等於第三項的和,我們很容易就可以依據這個特點寫出它的遞推公式:an=an1+an2a_n = a_{n-1} + a_{n-2} 。並且按照遞推公式我們還可以推導出相應的通項公式:

an=15[(1+52)n(152)n](通項公式) a_n = \frac{1}{\sqrt{5}} \left[ \left( \frac{1+\sqrt{5}}{2} \right)^n - \left( \frac{1-\sqrt{5}}{2} \right)^n \right] \tag{通項公式}

下面讓我們通過幾個示例來了解斐波那契數列的魅力把。

示例1:無窮數列 1,1,2,3,5,13,21,34,55,…, 稱爲 Fibonacci 數列,計算第n位數列。

同樣的,這次先用循環和遞歸做一遍,然後在想辦法進行優化。

#include <iostream>
using namespace std;

/*
題目:無窮數列 1,1,2,3,5,13,21,34,55,..., 稱爲 Fibonacci 數列,計算第n位數列。
*/

// 循環實現
int Fibonacci(int n)
{
	/*
	1     1     2    5
	a=1 b=1 
			 c=a+b=2
	    a=b    b=c
	分析:
	c 保存兩數之和,a、b向後移動
	*/
	int a = 1, b = 1, c = 1;
	for (int i = 3; i <= n; ++i)
	{
		c = a + b;
		a = b;
		b = c;
	}
	return c;
}

// 遞歸實現
int Fib(int n)
{
	if (n <= 2)	return 1;
	else return Fib(n - 1) + Fib(n-2);
}

int main()
{
	for (int i = 1; i < 10; ++i)
	{
		cout << Fibonacci(i) << " ";
		cout << Fib(i) << endl;
	}
	return 0;
}

分析遞歸效率
對於循環來講,它使用遞推公式的方式求解,基本上沒有需要優化的地方,除非我們選擇使用通項公式求解,而對於當前的遞歸來說卻還有着一些優化的空間。
遞歸過程分析:如:欲求的第五位斐波那契數,其遞歸調用方式如下

1遞歸
1遞歸
2遞歸
2遞歸
2遞歸
2遞歸
3遞歸
3遞歸
1
2
3
4
5
.3
2.
.2
.1

如圖所示:

  • 在第一次遞歸時,把求5的問題轉換爲求4的問題和求3的問題
  • 在第二次遞歸時,把求4的問題轉換爲求3的問題求2的問題
  • 在第二次遞歸時,把求3的問題轉換爲求2的問題求1的問題
  • 在第三次遞歸時,把求3的問題轉換爲求2的問題求1的問題

我們發現僅僅是計算第五個斐波那契數就要重複計算兩次求3問題,三次求2問題,兩次求1問題,這些重複的計算顯然是沒有意義的。從我們人的角度來分析問題,我們只需要計算一次“1”、一次“2”、一次“3”、一次“4”,就可以得到“5”的值了。並且我們也確實這樣做了。正如我們所見到的,我們使用循環實現的求斐波那契數就是按照這種邏輯設計的。

前面提到過,通常情況下循環問題可以轉換爲遞歸問題,那麼我們是否可以把循環式的代碼稍加修改變成遞歸式呢?
循環式分析:
在循環中使用了三個變量,a、b 用於保存 an1a_{n-1}an2a_{n-2} 的值,使用 c 保存相加後的結果,a、b再向後移動一位。整體的過程如下表所示:

1 1 2 3 5 8 13 21 34
a b c
a b c
a b c
a b c
a b c
a b c
a b c

遞歸算法設計:
對於一個 1, 1, 2, 3, 5, 8, 13, 21, 34 的數列,遞歸的求法是將大問題轉化爲小問題後在求解,如:求34問題->求21問題->求13問題->求5問題->求3問題->求2問題。

分析:把這個數列類比成一個數組的話,遞歸就像是倒着數組求值,循環是正向對數組求值。並且在整個過程中都是遍歷完整個數組的,遞歸次數與循環次數相同。

算法:在進行每一步遞歸時同時進行計算,從斐波那契數的起始數 1,1 開始計算,待到遞歸到結束時已經求得斐波那契數值。並且這個算法不存在重複求值問題。

遞歸調用式:fib(n,a,b) =>fib(n1,b,a+b)fib(n , a,b) \ => fib(n-1,b,a+b) ,調用過程中,n爲遞歸次數,a 的位置保存b的值,b 的位置保存 c 的值(a+b)
遞歸過程分析:求第9個斐波那契數
fib(9,1,1) ⇒ fib(8,1,2) ⇒ fib(7,2,3) ⇒ fib(6,3,5) ⇒ fib(5,5,8) ⇒ fib(4,8,13) ⇒ fib(3,13,21) ⇒ fib(2,21,34)
 a1=1  ,  a2=1   ,  a3=2   ,  a4=3   ,  a5=5   ,  a6=8    ,  a7=13    ,  a8=21    ,  a9=34\ a_1=1 \ \ , \ \ a_2=1 \ \ \ ,\ \ a_3 = 2 \ \ \ ,\ \ a_4 = 3 \ \ \ ,\ \ a_5 = 5 \ \ \ ,\ \ a_6 = 8 \ \ \ \ ,\ \ a_7 = 13\ \ \ \ ,\ \ a_8 = 21\ \ \ \ ,\ \ a_9 = 34

因此,對於遞歸式fib(n,a,b)fib(n , a,b),的遞歸終止條件也確定了。在 n=2n = 2 時返回 b ,或者在 n=1n = 1 返回 a 都可以,下面是C++的代碼實現:

#include <iostream>
using namespace std;

/*
題目:無窮數列 1,1,2,3,5,13,21,34,55,..., 稱爲 Fibonacci 數列,計算第n位數列。
*/


int Fib_Reverse(int n, int a, int b)
{
	if (n <= 2) return b;
	else return Fib_Reverse(n - 1, b, a + b);
}

int NiceFib(int n)
{
	int a = 1, b = 1;
	return Fib_Reverse(n, a, b);
}

int main()
{
	for (int i = 1; i < 10; ++i)
	{
		cout << NiceFib(i) << " ";
	}
	cout << endl;
	return 0;
}

除了直接求斐波那契數列的問題外,還有很多其衍生問題。如:求楊輝三角問題、爬樓梯問題、兔子繁殖問題、青蛙跳臺階問題等等。另外,盧卡斯數列 1、3、4、7、11、18…,也具有斐波那契數列同樣的性質。值得一提的是在現代物理、準晶體結構、化學等領域,斐波納契數列都有直接的應用。感興趣的同學可以自行在網上查找相關資料。

最後,如果覺得我的文章對你有幫助的話請幫忙點個贊,你的鼓勵就是我學習的動力。如果文章中有錯誤的地方歡迎指正,有不同意見的同學也歡迎在評論區留言,互相學習。

——學習路上,你我共勉
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章