遞歸法
遞歸法(Recursion)是一種在函數或方法中調用自身的編程技術,在計算機方法中,使用遞歸技術往往使函數的定義和算法的描述簡潔且易於理解。任何可以用計算機求解的問題所需要的計算時間都與其規模有關。而且規模越小,解題所需要的計算時間通常越小,從而比較容易處理。
簡而言之,遞歸思想就是用與自身問題相似但規模較小的問題來描述自己。
例如,兔子出生兩個月後就有繁殖能力,一對兔子每個月能生出一對兔子來。如果所有兔子都不死,那麽一年以後可以繁殖多少對兔子?
第一個月小兔子沒有繁殖能力,所有還是一對;兩個月後,生下一對,兔子共2對;三個月後,老兔子又生下一對,因為小兔子還沒有繁殖能力,所以一共是三對。
以此類推,如下所示。
所經過月數:0 1 2 3 4 5 6 7 8 9 10 11 12
兔子對數: 1 1 2 3 5 8 13 21 34 55 89 144 233
表中數字1,1,2,3,5,8^233構成一個序列。這個數列有個明顯的特點,即前面兩項之和構成後一項。用數學表示這個無窮數列稱為裴波那契數列,形式化描述為:
1 , n=0
F(n)= 1 ,n=1
F(n-1)+F(n-2) ,n>1
該數列就是個典型遞歸數列。
#include<iostream> using namespacestd; int fib(int n) { int f=0; if(n==1) return 0; if(n==2) return 1; f=fib(n-1)+fib(n-2); return f; } int main() { int n,i,m=0; cin>>n; m=fib(n); cout<<"第"<<n<<"項是"<<m<<endl; m=0; for(i=1;i<=n;i++) m=fib(i)+m; cout<<"前"<<n<<" 項和是"<<m<<endl; return 0; }
遞歸算法的特性:
(1)求解規模為n的問題可以轉換成一個或多個結果相同、規模較小的問題,然後從這些小問題的解能方便地構造出大問題的解。
(2)遞歸調用的次數必須是有限的。
(3)必須有結束遞歸的條件(邊界條件)來終止遞歸。即當規模為1時,能夠直接得解。
遞歸的執行過程:
遞歸算法的執行過程劃分為遞推和回歸兩個階段。在遞推階段,把規模為n的問題求解推到比原問題的規模較小的問題求解,且必須要有終止遞推的情況。在回歸階段,當獲得最簡單情況的解後,逐漸返回,依次得到規模較大問題的解。
需要註意的是,用遞歸描述問題並不表示程序也一定要直接用遞歸實現。
遞歸算法的優點:結構清晰、可讀性強,而且容易用數學歸納法來證明算法的正確性,因此它為設計算法、調試程序帶來很大方便。
遞歸算法的缺點:運行效率相對較低,無論是耗費的計算時間還是占用的存儲空間都比非遞歸算法要多。通常解決方法是消除遞歸算法中的遞歸調用,使遞歸算法轉換為非遞歸算法。
理論上遞歸算法都可以轉換為非遞歸算法。方法有如下3種
(1)通過分析,跳過分解部分,直接用循環結構實現求值過程。
(2)用棧保存程序的運行過程,通過分析只保存必須保存的信息,從而用非遞歸算法替代遞歸算法。
(3)利用棧保存參數,由於棧的後進先出特性與遞歸算法的執行過程相似,因而可以用非遞歸算法替代遞歸算法。
1漢諾塔問題求解
假設有n個金盤,並且金盤由小到大一次編號為1,2,3……n。要把放在A針上的n個金盤移到目的針C上。當只有一個金盤,即n=1時,只要將編號為1的金盤從A針直接移到C針上即可。當n>1時,可以把最上面的n-1個金盤看作一個整體。這樣n個金盤就分成了兩個部分:上面n-1個金盤和最下面的編號為n的金盤。移動金盤的問題可以轉換為如下3個步驟:
1)借助C針,將n-1個金盤從A針移到B針上;
2)將編號為n的金盤直接從A針移到C針上;
3)借助A針,將B針上的n-1個金盤移到C針上。
其中,第二步只移動一個金盤,容易解決。第一、三步不能直接解決,但由於已經把移到n個金盤問題變成了移到n-1個金盤的問題,問題規模變小。如果再把第一、三步分別變成類似的三個子問題,移到n-1個金盤的問題就可以變成移到n-2個金盤的問題,依次類推,從而將問題加以解決。
#include<iostream> using namespace std; void Move(int n,char x,char y) { cout<<"把"<<n<<" 號從 "<<x<<"挪動到 "<<y<<endl; } void Hannoi(int n,char a,char b,char c) { if(n==1) Move(1,a,c); else { Hannoi(n-1,a,c,b); Move(n,a,c); Hannoi(n-1,b,a,c); } } int main() { cout<<"以下是3層漢諾塔的解法"<<endl; Hannoi(3,‘a‘,‘b‘,‘c‘); cout<<"輸出完畢!"<<endl; return 0; }
2 八皇後問題
在8*8國際象棋盤上放置8個黃昏,使任何一個皇後都不能吃掉另一個。即任意兩個皇後都不能處於同一行、同一列或者同一斜線上,問共有多少種放置方案。
假設當前生成全排列中的第t個數字大於n,其中,t是每一行從左到右的標號,n是棋盤的行數,則說明生成了一個全排列,否則:
1)從1~8中依次取出一個數字來嘗試,如果此數字前面已經出現,則取下一個數字,直到找到第一個沒有出現過的數字;
2)將這個數字標記為已出現,然後以同樣的方法生成全排列中的下一個數字;
3)將這個數字標記為未出現;
顯然,這是一個遞歸定義,遞歸的結束條件為t>n。
#include<iostream> using namespace std; int result=1; int chess[8]; //根據前面幾行的棋子,檢查這一行所放的棋子是否合法 int check(int n) { int i; for(i=1;i<=n-1;i++) { if(chess[n] == chess[i]+(n-i) || chess[n]==chess[i]-(n-i) || chess[n]==chess[i]) return 0; } return 1; } void show_chess(void) { int i; cout<<endl<<"Result -"<<result<<endl; for(i=1;i<=8;i++) { cout<<i<<" : "<<chess[i]<<"\t"; } ++result; } //遞歸函數:放棋子 void putchess(int n) { int i; if(n<=8) { for(i=1;i<=8;i++) {//將第n行從第一格(i)開始往下放 chess[n]=i; if(check(n) == 1) {//若可放,則檢查是否放滿 if(n==8) show_chess();//若已放滿到8行時,則表示找出一種解,打印出來 else putchess(n+1);//若沒放滿則放下一行 putchess(n+1) } } } } int main() { cout<<"This is for 8*8 matrix"<<endl; putchess(1); return 0; }
遞歸法