演算法——Fibonacci數列的多種解法(遞迴演算法)
咳咳,金宸歐巴今天來更新部落格了,今天想寫的一點內容是關於斐波那契數列的解法,fibonacci數列的定義如下:
F(n)=
{
a, n=1
b, n=2
F(n-1)+F(n-2), n>2並且n是奇數
F(n-1)+F(n-2)+F(n-3), n>2並且n是偶數
}
這裡a和b是定值,現給出a,b和n,你的任務是計算F(n)。
wtf!!這個不是很簡單麼?遞迴!遞迴啊!
如題目中的意思所示,第一個數是我們給的一個a,第二個數是我們給的值b,凡是遇見大於2的奇數就將數列中的前兩個值相加,遇見大於2的偶數就將數列中的前三個數字相加,遞迴走一波!於是有了以下程式碼……
//
// main.cpp
// 斐波那契
//
// Created by 王金宸 on 16/4/13.
// Copyright © 2016年 王金宸. All rights reserved.
// 斐波那契是不能用遞迴演算法的
#include <iostream>
using namespace std;
int feiBo(int x,int y,int t);
int main(int argc, const char * argv[])
{
int a,b,c,n;
long long int k;
cin>>c;
for (int t = 0; t != c; t++)
{
cin>>a>>b>>n;
k = feiBo(a,b,n);
cout<<k<<endl;
}
return 0;
}
int feiBo(int x,int y,int t)
{
int sum = 0;
if (t==1)
{
sum = x;
}
else if (t==2)
{
sum = y;
}
else if ((t%2!=0)&&(t>2))
{
sum = feiBo(x,y,t-1) + feiBo(x,y,t-2);
}
else if ((t%2==0)&&(t>2))
{
sum = feiBo(x,y,t-1) + feiBo(x,y,t-2) + feiBo(x,y,t-3);//返回的sum只是一個值改變的變數只是中間值
}
else
printf("error");
return sum;
}
程式是沒有錯誤的啦,輸入輸出也沒有錯誤,但是!but這樣有一個問題就是我們的程式執行時間太長了!,為什麼這麼說呢?如果我們輸入一個值20,我們想要得到f(20)的值,我們必須獲取到f(19),f(18),f(17)的值,然而函式體是在巢狀執行的,想要得到f(20)具體的值,我們要遞迴到f(2),f(1),我們知道,時間複雜度取決於語句執行的次數,如果這樣使用遞迴,程式的時間複雜度會隨著n的增大趨近於指數級,那怎樣能夠避免這個問題呢?是不是遞迴就不能夠使用了?
下面有三種解決辦法:
- 第一,使用正向的迴圈推導,廢話不多說!上程式碼!:
#include <iostream>
using namespace std;
int main(int argc, const char * argv[])
{
long long int a,b,c,d,sum = 0;
cin>>d;
for (int i = 0;i != d;i++)
{
cin>>a>>b>>c;
if(c == 1)
cout<<a<<endl;
if(c == 2)
cout<<b<<endl;
for (int k = 1; k < c-1; k++)
{
if (k%2 != 0)
{
sum = a + b;
a = b;
b = sum;
}
else
{
sum = b + b;
a = b;
b = sum;
}
}
if (c>2)
{
cout<<sum<<endl;
}
sum = 0;
}
return 0;
}
我們不能倒著推難道還不能正著推麼?這種方法的原理是使用for迴圈和拷貝賦值的方法,依次算出第三個值到你所需要的值,時間複雜度也降低到了O(n)
- 第二種方法是使用陣列,陣列較上一個方法相比時間複雜度一樣,但是便於取值,不用進行拷貝賦值
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
int m,i,n,t,f,z,a[40],b[40];
cin>>m;
t=m;
while(m)
{
cin>>f; a[0]=f;
cin>>z; a[1]=z;
cin>>n;
for(i=2; i<n ;i++){
if(!( i%2 ))
{
a[i]=a[i-1]+a[i-2];
}
else
{
a[i]=a[i-1]+a[i-2]+a[i-3];
}
}
b[t-m]=a[n-1];
m--;
}
for(i=0; i<t ;i++)
cout<<b[i]<<endl;
return 0;
}
最後,我們用遞迴的方法來實現一下這個題目,普通的遞迴是不夠好的,所以這裡要使用一種叫做尾遞迴的方法
先來看下尾遞迴的定義(來自百度百科) 尾遞迴:如果一個函式中所有遞迴形式的呼叫都出現在函式的末尾,我們稱這個遞迴函式是尾遞迴的。當遞迴呼叫是整個函式體中最後執行的語句且它的返回值不屬於表示式的一部分時,這個遞迴呼叫就是尾遞迴。尾遞迴函式的特點是在迴歸過程中不用做任何操作,這個特性很重要,因為大多數現代的編譯器會利用這種特點自動生成優化的程式碼。 當編譯器檢測到一個函式呼叫是尾遞迴的時候,它就覆蓋當前的活動記錄而不是在棧中去建立一個新的。編譯器可以做到這點,因為遞迴呼叫是當前活躍期內最後一條待執行的語句,於是當這個呼叫返回時棧幀中並沒有其他事情可做,因此也就沒有儲存棧幀的必要了。通過覆蓋當前的棧幀而不是在其之上重新新增一個,這樣所使用的棧空間就大大縮減了,這使得實際的執行效率會變得更高。雖然編譯器能夠優化尾遞迴造成的棧溢位問題,但是在程式設計中,我們還是應該儘量避免尾遞迴的出現,因為所有的尾遞迴都是可以用簡單的goto迴圈替代的。
– 由此我們可以看出,使用尾遞迴首先具備的條件是遞迴形式的呼叫都出現在函式的末尾,而且返回值不屬於表示式的一部分,它所具備的優勢就是當函式進入遞迴函式的時候不像其他的遞迴再次進入遞迴,而是返回了這個遞迴,然後這個遞迴再開啟出來,這樣就不會一直壓棧,而是出棧了之後再入棧。
//使用尾遞迴
// Created by 王金宸 on 16/4/13.
// Copyright © 2016年 王金宸. All rights reserved.
#include <iostream>
using namespace std;
int feiBo(int x,int y,int t);
int d = 0;
int main(int argc, const char * argv[])
{
int a,b,c,n;
long long int k;
cin>>c;
for (int t = 0; t != c; t++)
{
cin>>a>>b>>n;
k = feiBo(a,b,n);
d = 0;
cout<<k<<endl;
}
return 0;
}
int feiBo(int x,int y,int t)
{
if (t==1)
{
return x;
}
else if (t==2)
{
return y;
}
else
{
if (d%2==0)
{
d++;
return feiBo(y, x+y, t-1);
}
else
{
d++;
return feiBo(y, 2*y, t-1);
}
}
return 0;
}
int feiBo(int x,int y,int t)就是一個尾遞迴函式,而下面是尾遞迴的四種return的返回值,可以看出返回值的形式都是值或feiBo函式,全域性變數d的作用是一個計數器,在呼叫的遞迴時候可以改變全域性變數的值而當主函式return的時候全域性變數才會釋放,所以有必要在每次迴圈之後給全域性變數清零,而且全域性變數的宣告要初始化,這樣比較安全哈哈哈。
jacksonWant原創,jacksonWant一個為了情懷而奮鬥的小碼農,文中如有勘誤可以給我發郵件。