時隔一年,又重新學數據結構二叉樹部分,被各種遞歸搞得苦不堪言,以下是網上資料的一些彙總整理
函數的遞歸調用與分治策略
遞歸方法是算法和程序設計中的一種重要技術。遞歸方法即通過函數或過程調用自身將問題轉化爲本質相同但規模較小的子問題。遞歸方法具有易於描述和理解、證明簡單等優點,在動態規劃、貪心算法、回溯法等諸多算法中都有着極爲廣泛的應用,是許多複雜算法的基礎。遞歸方法中所使用的“分而治之”的策略也稱分治策略。
遞歸方法的構造
構造遞歸方法的關鍵在於建立遞歸關係。這裏的遞歸關係可以是遞歸描述的,也可以是遞推描述的。下面由一個求n的階乘的程序爲例,總結出構造遞歸方法的一般步驟。
[例1]從鍵盤輸入正整數N(0<=N<=20),輸出N!。
[分析]N!的計算是一個典型的遞歸問題。使用遞歸方法來描述程序,十分簡單且易於理解。
[步驟1]描述遞歸關係 遞歸關係是這樣的一種關係。設{U1,U2,U3,…,Un…}是一個序列,如果從某一項k開始,Un和它之前的若干項之間存在一種只與n有關的關係,這便稱爲遞歸關係。
注意到,當N>=1時,N!=N*(N-1)!(N=1時,0!=1),這就是一種遞歸關係。對於特定的K!,它只與K與(K-1)!有關。
[步驟2]確定遞歸邊界 在步驟1的遞歸關係中,對大於k的Un的求解將最終歸結爲對Uk的求解。這裏的Uk稱爲遞歸邊界(或遞歸出口)。在本例中,遞歸邊界爲k=0,即0!=1。對於任意給定的N!,程序將最終求解到0!。
確定遞歸邊界十分重要,如果沒有確定遞歸邊界,將導致程序無限遞歸而引起死循環。例如以下程序:
#include <iostream.h>
int f(int x){
return(f(x-1));
}
main(){
cout<<f(10);
}
它沒有規定遞歸邊界,運行時將無限循環,會導致錯誤。
[步驟3]寫出遞歸函數並譯爲代碼 將步驟1和步驟2中的遞歸關係與邊界統一起來用數學語言來表示,即
N*(N-1)! 當N>=1時
n!=
1 當N=0時
再將這種關係翻譯爲代碼,即一個函數:
long f(int n){
if (n==0)
return(1);
else
return(n*f(n-1));
}
[步驟4]完善程序 主要的遞歸函數已經完成,將程序依題意補充完整即可。
//ex1.cpp
#include <iostream.h>
long f(int n){
if (n==0)
return(1);
else
return(n*f(n-1));
}
main(){
int n;
cin>>n;
cout<<endl<<f(n);
}
綜上,得出構造一個遞歸方法基本步驟,即描述遞歸關係、確定遞歸邊界、寫出遞歸函數並譯爲代碼,最後將程序完善。以下繼續引用一些例子來討論遞歸方法的應用。
經典遞歸問題
以下討論兩個十分經典的遞歸問題。它們的算法構造同樣遵循剛剛得出的四個步驟。瞭解這兩個問題可加深對遞歸方法的理解。
[例2]Fibonacci數列(兔子繁殖)問題:已知無窮數列A,滿足:A(1)=A(2)=1,A(N)=A(N-1)+A(N-2)(N>=3)。從鍵盤輸入N,輸出A(N)。
[分析]遞歸關係十分明顯,由A(N)的表達式給出。需要注意的是本例中對於N>=3,A(N)的值與A(N-1)和A(N-2)都有關。
[代碼]
//ex2.cpp
#include <iostream.h>
long fibonacci(int x)
{
if ( (x==1) || (x==2) )
return(1);
else
return(fibonacci(x-1)+fibonacci(x-2));
}
main(){
int n;
cin>>n;
cout>>endl>>fibonacci(n);
}
[例3]Hanoi塔問題。
[問題描述]在霍比特人的聖廟裏,有一塊黃銅板,上面插着3根寶石針(分別爲A號,B號和C號)。在A號針上從下到上套着從大到小的n個圓形金片。現要將A針上的金片全部移到C針上,且仍按照原來的順序疊置。移動的規則如下:這些金片只能在3根針間移動,一次只能一片,且任何時候都不允許將較大的金片壓在較小的上面。從鍵盤輸入n,要求給出移動的次數和方案。
[分析]由金片的個數建立遞歸關係。當n=1時,只要將唯一的金片從A移到C即可。當n>1時,只要把較小的(n-1)片按移動規則從A移到B,再將剩下的最大的從A移到C(即中間“藉助”B把金片從A移到C),再將B上的(n-1)個金片按照規則從B移到C(中間“藉助”A)。
本題的特點在於不容易用數學語言寫出具體的遞歸函數,但遞歸關係明顯,仍可用遞歸方法求解。
[代碼]
//ex3.cpp
#include <iostream.h>
hanoi(int n,char t1,char t2,char t3){
if (n==1)
cout<<"1 "<<t1<<" "<<t3<<endl;
else
{
hanoi(n-1,t1,t3,t2);
cout<<n<<" "<<t1<<" "<<t3<<endl;
hanoi(n-1,t2,t1,t3);
}
}
main(){
int n;
cout<<"Please enter the number of Hanoi:";
cin>>n;
cout<<"Answer:"<<endl;
hanoi(n,'A','B','C');
}
函數遞歸調用的應用與分治策略
許多算法都採用了分治策略求解,而可以說分治與遞歸是一對孿生兄弟,它們經常同時被應用於算法的設計中。下面討論著名的Catalan數問題,人們在對它的研究中充分應用了分治策略。
[例4]Catalan數問題。
[問題描述]一個凸多邊形,通過不相交於n邊形內部的對角線,剖分爲若干個三角形。求不同的剖分方案總數H(n)。H(n)即爲Catalan數。例如,n=5時H(5)=5。
[分析]Catalan數問題有着明顯的遞歸子問題特徵。在計算Catalan數時雖然可以推導出只關於n的一般公式,但在推導過程中卻要用到遞歸公式。下面討論三種不同的解法,其中第三種解法沒有使用遞歸,它是由前兩種解法推導而出的。
[解法1]對於多邊形V1V2…Vn,對角線V1Vi(i=3,4,…,n-1)將其分爲兩部分,一部分是i邊形,另一部分是n-i+1邊形。因此,以對角線V1Vi爲一個剖分方案的剖分方案數爲H(i)*H(n-i+1)。還有一種的特殊情形,是對角線V2Vn將其分爲一個三角形V1V2Vn和一個n-2+1邊形。爲了讓它同樣符合粗體字給出的公式,規定H(2)=1。於是得到公式:
H(n)=∑H(i)*H(n-i+1) (i=2,3,…,n-1) ----公式(1)
H(2)=1
有了這個遞歸關係式,就可以用遞推法或遞歸法解出H(n)。
[解法2]從V1向除了V2和Vn外的n-3個頂點可作n-3條對角線。每一條對角線V1Vi把多邊形剖分成兩部分,剖分方案數爲H(i)*H(n-i+2),由於Vi可以是V3V4…Vn-1中的任一點,且V1可換成V2,V3,…,Vn中任一點也有同樣的結果。考慮到同一條對角線在2個頂點被重複計算了一次,於是對每個由頂點和對角線確定的剖分方案都乘以1/2,故有
H(n)=n∑(1/2)H(i)*H(n-i+2) (i=3,4,…,n-1)
把(1/2)提到∑外面,
H(n)=n/(2*(n-3))∑H(i)*H(n-i+2) (i=3,4,…,n-1) ----公式(2)
規定H(2)=H(3)=1,這是合理的。
由公式(2)和H(2)=1,同樣可以用遞推法或遞歸法解出H(n)。
[解法3] 把公式(1)中的自變量改爲n+1,再將剛剛得出的公式(2)代入公式(1),得到
H(n+1)=∑H(i)*H(n-i+2) (i=2,3,…,n) 由公式(1)
H(n+1)=2*H(n)+∑H(i)*H(n-i+2) (i=3,4,…,n-1) 由H(2)=1
H(n+1)=(4n-6)/n*H(n) 由公式(2)
H(n)=(4n-10)/(n-1)*H(n-1) ----公式(3)
這是一個較之前兩種解法更爲簡單的遞歸公式,還可以繼續簡化爲
H(n)=1/(n-1)*C(n-2,2n-4) ----公式(4)
這就不需要再使用遞歸算法了。然而在程序設計上,公式(4)反而顯得更加複雜,因爲要計算階乘。因此最後給出由公式(3)作爲理論依據範例程序代碼。代碼相當簡單,這都歸功於剛纔的推導。如果用前兩種解法中的遞歸關係,程序會變得複雜且容易寫錯。因此,有時對具體問題將遞歸關係公式進行必要的化簡也是至關重要的。
[代碼]
//ex4.cpp
#include <iostream.h>
#define MAXN 100
long f(int x){
if (x==3)
return(1);
else
return((4*x-10)*f(x-1)/(x-1));
}
main(){
int n;
cout<<"\nPlease input N for a Catalan number:";
cin>>n;
if ( (n<=MAXN) && (n>=3) )
cout<<"The answer is:"<<f(n);
}
本例編程時還有一個細節問題需要注意。注意函數f中的斜體部分,按照公式(4)計算時一定要先進行乘法再進行除法運算,因爲(4*x-10)並不總能整除(x-1),如果先進行除法則除出的小數部分將自動被捨去,從而導致得到不正確的解。
數學上許多有重要意義的計數問題都可以歸結爲對Catalan數的研究。可以看到,本例中的遞歸關係經簡化還是相當簡單的。下面討論一個遞歸關係略爲複雜的例子。
[例5]快速排序問題。
快速排序是程序設計中經常涉及的一種排序算法。它的最好時間複雜度爲O(nlog2n),最差爲O(n2),是一種不穩定的排序方法(大小相同的數在排序後可能交換位置)。
[算法描述]快速排序的一種基本思想是:要將n個數按由小到大排列,在待排序的n個數中選取任一個數(在本例中取第一個),稱爲基準數,在每一次快速排序過程中設置兩個指示器i和j,對基準數左邊和右邊的數同時從最左(i)和最右(j)開始進行掃描(i逐1遞增,j逐1遞減),直到找到從左邊開始的某個i大於或等於基準數,從右邊開始的某個j小於或等於基準數。一旦發現這樣的i和j(暫且稱之爲一個“逆序對”),則把第i個數和第j個數交換位置,這樣它們就不再是逆序對了,緊接着再將i遞增1,j遞減1。如此反覆,在交換過有限個逆序對後,i和j將越來越靠近,最後“相遇”,即i和j指向同一個數,暫且稱之爲相遇數(極端情況下,如果一開始就不存在逆序對,i和j將直接“相遇”)。相遇後就保證數列中沒有逆序對了(除了在上述的極端情況下基準數和自身也算構成一個逆序對,注意粗體字給出的逆序對的定義)。繼續掃描,非極端情況下,由於數列中已經沒有逆序對,i遞增1(如果相遇數小於基準數)或者j遞減1(如果相遇數大於基準數)後即算完成了一趟快速排序,這時第1到第j個數中的每個都保證小於或等於基準數,第i到第n個數中的每個保證大於或等於基準數。此時,遞歸調用函數,對第1到第j個數和第i到第n個數分別再進行一趟快速排序。如果在極端情況下,程序認爲基準數和自身構成逆序對,則將基準數與自身交換(這其實沒有作用)之後i遞增1,j遞減1(注意斜體字給出的對逆序對的處理方法),同樣對第1到第j個數和第i到第n個數分別再進行一趟快速排序。
最後的問題就是確定遞歸邊界。由於被排序的數列將不斷被劃分爲兩個至少含一個數的子列(因爲在每趟排序最後進行遞歸調用函數時i<>j),最後子列的長度將變爲1。這就是遞歸的邊界。在程序實現是,本着“能排則排”的原則,只要第一個數小於j(或者第i個數小於最後一個數),即進行遞歸。
[主程序(遞歸函數體)]
void QuickSort(RecType R[ ],int s,int t)
{
int i=s,j=t,k;
RecType temp;
if (s<t)
{
temp=R[s] // 用區間第1個記錄作爲基準
while( i!=j) //從兩端向中間交替掃描,直至i=j;
{
while( j>i&&R[j].key>temp.key)
j--;
if(i<j)
{
R[i]=R[j];
i++;
}
while( i<j&&R[i].key<temp.key)
i++;
if(i<j)
{
R[j]=R[i];
j--;
}
}
R[i]=temp;
QuickSort(R,s,i-1);
QuickSort(R,i+1,t);
}
}
[例6]“九宮陣”智力遊戲。
[問題描述]一個9×9方陣,由9個“九宮格”組成,每個九宮格又由3×3共9個小格子組成。請在每個空白小格子裏面填上1~9的數字,使每個數字在每個九宮格內以及在整個九宮陣中的每行、每列上均出現一次。
(1)編程將下面圖中的九宮陣補充完整。
(2)討論是否可能給出“九宮陣”的全部解?
|
9 |
|
|
|
4 |
5 |
|
7 |
|
|
3 |
|
7 |
9 |
4 |
|
|
|
|
|
3 |
6 |
|
|
8 |
9 |
3 |
|
|
1 |
|
|
|
|
|
|
|
4 |
|
|
|
|
2 |
3 |
|
1 |
2 |
|
|
3 |
|
|
|
8 |
|
|
|
|
|
|
5 |
|
|
6 |
|
2 |
9 |
|
|
|
|
|
2 |
1 |
|
|
|
8 |
|
|
[分析]本題可利用回溯法解決,其基本思想爲深度優先搜索(DFS),這也是一種以分治策略爲基礎的算法。回溯法與純粹的DFS不同的是,它在搜索過程中不斷殺死不合題意的結點。這一點保證瞭解法的效率。
首先考慮如何得出全部解的情況。
解空間樹容易構造,只需按順序(從第一行第一個數字開始到第一行最後一個,然後第二行……,一直到最後一行最後一個數字)“嘗試”填入數字即可。
爲了解決這個問題,我們需要先編寫一個函數check,其原型爲int check(int i,int j,int k),用於求第i行第j列能否填上數字k。如果可以,返回1,否則返回0。由於我們是按順序填入數字的,看起來一個數字後面的數字並不在判斷能否填的範圍內。但爲了解決題中某個特解問題的方便,還是引入較爲嚴謹的判斷方法。
函數check代碼如下:
int check(int i,int j,int k){
int l,m,pi,pj;
//1. Check the line
for (l=1;l<=9;l++)
if ( (l!=j) && (a[i][l]!=0) && (a[i][l]==k) )
return(0);
//2. Check the column
for (l=1;l<=9;l++)
if ( (l!=i) && (a[l][j]!=0) && (a[l][j]==k) )
return(0);
//3. Check the 3x3 matrix
//3.1 Firstly we will have to check the parent_i(pi) and parent_j(pj)
if (i<=3) pi=1;
else if (i<=6) pi=4;
else pi=7;
if (j<=3) pj=1;
else if (j<=6) pj=4;
else pj=7;
//3.2 Now we can check it
for (l=0;l<=2;l++)
for (m=0;m<=2;m++){
if ( ((pi+l)!=i) && ((pj+m)!=j) )
if ( ( a[pi+l][pj+m]!=0 ) && ( a[pi+l][pj+m]==k ) )
return(0);
}
return(1);
}
結合註釋很容易就能接受函數的思想,不予過多說明。
下面考慮程序最重要的部分,即遞歸函數。思路是這樣的:假設某一格能填入某數,把這個格子看成解空間樹的一個結點,由它可以擴展出9個兒子,即下一格填什麼數(由1到9逐個嘗試)。對下一格,同樣這樣考慮。不斷用函數check函數考察某一個能否填入某數,一旦函數check返回0,則殺死這個結點。
如果能一直填到最後一個數,結點仍未被殺死,則這是一個解。
這種思想可用僞代碼表示如下:
procedure backtrack(i,j,k:integer);
if check(i,j,k)=true then
begin
a[i,j]=k;
Generate_next_i_and_j;
if i<10 then
begin
for l:=1 to 9 do
backtrack(i,j,l);
end
else
Do_Output;
a[i,j]:=0;
end;
注意斜體的“a[i,j]:=0”必不可少!當對某個結點(x,y)擴展的過程中,可能在擴展到(x+m,y+n)時它的子樹被完全殺死(每個結點都被殺死,亦即按照(x,y)及之前的填數方案填數,無解)。這時需要保證(x,y)以後所填的數被重新置零,這個語句的作用即在每個結點被殺死時都將其置零。
將僞代碼翻譯爲C++代碼:
backtrack(int i,int j,int k){
int l;
if (check(i,j,k)==1){
a[i][j]=k; //Fill in the okay solution
//Generate next i,j
if (j<9) j++;
else { i++; j=1; } //End of Generate next i,j
if (i<10){
for (l=1;l<=9;l++)
backtrack(i,j,l);
}
else
output();
a[i][j]=0; /*When fails and goes upperwards, the value must be cleared*/
}
}
函數output()用雙重循環完成輸出。在主函數main()對backtrack(1,1,i)進行一個循環,i從1取到9,即可完成整個程序。運行時發現九宮格的解相當多,即使保存到文件中也不現實。這就回答了第2個問題。
對於第1個問題,將這個程序略加改動,即賦予全局數組a以初值,並在過程backtrack中產生下一個i和j時跳過有初值的部分,即可將程序轉化爲求填有部分空缺的九宮格程序。
最後給出填充有部分空缺的九宮格的完整源代碼。
[代碼]
#include <iostream.h>
int a[11][11]={0};
int check(int i,int j,int k){
int l,m,pi,pj;
//1. Check the line
for (l=1;l<=9;l++)
if ( (l!=j) && (a[i][l]!=0) && (a[i][l]==k) )
return(0);
//2. Check the column
for (l=1;l<=9;l++)
if ( (l!=i) && (a[l][j]!=0) && (a[l][j]==k) )
return(0);
//3. Check the 3x3 matrix
//3.1 Firstly we will have to check the parent_i(pi) and parent_j(pj)
if (i<=3) pi=1;
else if (i<=6) pi=4;
else pi=7;
if (j<=3) pj=1;
else if (j<=6) pj=4;
else pj=7;
//3.2 Now we can check it
for (l=0;l<=2;l++)
for (m=0;m<=2;m++){
if ( ((pi+l)!=i) && ((pj+m)!=j) )
if ( ( a[pi+l][pj+m]!=0 ) && ( a[pi+l][pj+m]==k ) )
return(0);
}
return(1);
}
output(){
int i,j;
cout<<"One solution is:"<<endl;
for (i=1;i<=9;i++)
{
for (j=1;j<=9;j++)
cout<<a[i][j]<<" ";
cout<<endl;
}
}
backtrack(int i,int j,int k){
int l;
if (check(i,j,k)==1)
{
a[i][j]=k; //Fill in the okay solution
//Generate next i,j
do{
if (j<9) j++;
else { i++; j=1; }
} while (a[i][j]!=0); //End of Generate next i,j
if (i<10)
{
for (l=1;l<=9;l++)
backtrack(i,j,l);
}
else
output();
a[i][j]=0; /*When fails and goes upperwards, the value must be cleared*/
}
}
init(){
a[1][2]=9; a[1][6]=4; a[1][7]=5; a[1][9]=7;
a[2][3]=3; a[2][5]=7; a[2][6]=9; a[2][7]=4;
a[3][4]=3; a[3][5]=6; a[3][8]=8; a[3][9]=9;
a[4][1]=3; a[4][4]=1;
a[5][3]=4; a[5][8]=2; a[5][9]=3;
a[6][2]=1; a[6][3]=2; a[6][6]=3;
a[7][1]=8; a[7][8]=5;
a[8][2]=6; a[8][4]=2; a[8][5]=9;
a[9][2]=2; a[9][3]=1; a[9][7]=8;
}
main(){
int i;
for (i=1;i<=9;i++){
init();
backtrack(1,1,i);
}
}
遞歸方法在算法與數據結構中的應用無所不在,如動態規劃(狀態方程)、回溯法(深度優先搜索)等等,以上兩例只是冰山一角。只有熟悉掌握函數遞歸調用的編程方法,深入理解分治策略的重要思想,才能編寫出功能強大、高效簡明的程序。
所謂遞歸,簡而言問。在函數中直接調用函數本身,稱爲直接遞歸調用。在函數中調用其它函數,其它函數又調用原函數,這就構成了函數自身的間接調用,稱爲間接遞歸調用。,具有很好的可讀性,還往往使某些看起來不易解決的問題變得容易解決。但在遞歸函數中,由於
遞歸,作爲C語言最經典的算法之之就是在調用一個函數的過程中又直接或間接地調用該函數本身,以實現層次數據結構的查詢和訪一,是一種非常有用的程序設計方法。雖然用遞歸算法編寫的程序結構清晰存在着自調用過程,程序控制反覆進入其自身,使程序的分析設計有一定困難,致使很多初學者往往對遞歸迷惑不解,也在這上面花了不少的時間,卻收效甚微。那麼,程使問題得到解決。
說明:使用其他的辦法比較麻究竟什麼是遞歸?怎麼實現遞歸呢?
而採用遞歸方法來解決問難解決,而使用遞歸的方法可以很好地解決問題
1、可以把要解決用遞歸調的問題轉化爲一個新問題,而這個新的問題的解決方法仍與原來的解決方法相同,只是所處理的對象有規律地遞增或遞減。
說明:解決問題的方法相同,調用函數的參數每次不同(有規律的遞增或遞減),如果沒有規律也就不能適用。
3、必定要有一個明確的結束遞歸的題,必須符合以下三個條件:
2、可以應用這個轉化過煩或很條件。
說明:一定要能夠在適當的地方結束遞歸調用。不然可能導致系統崩潰。
好知道是這樣以後;我,求n!的問題可以轉化爲n*(n-1)!的新問題。比如n=4:
第一部分: n-2)!:3
第三部分:2*1 (n-2)(n-3)!
第四部分:1 (n-4)! 4*3*2們來寫一個衆多教材上的程序:使用遞歸的方法求n!。
當n>1時*1 n*(n-1)!
第二部分*2*1 (n-1)( 4-4=0,得到值1,結束遞歸。
我給h>
int fac(int n)
{int c;
printf("now t的源程序如下:
#include <stdio.he number is %d ",n);
getchar();
if(n==1 || n==0) c=1; -1);
printf("now the number is %d
else c=n*fac(n and the %d! is %d",n,n,c);
getchar();
return c;}
void main()
{ int n=4; 執行過程。運行結果如圖1所示,當主函數第一次調用fac()函數的時候,由於n=4不等於0和1,並不立即返回結果1,而是執行c=n*fac(n-1),用實參n-1(值爲3)調用fac()函數自己,即遞歸調用fac(3)。於是進入第二層調用fac(),這時也沒有得出結果,繼續用實參n-1(值爲
printf("result is %d.\n",fac(n)); }
可以看到,加上兩條printf()和getchar()語句後,可以察看各級調用及其中間答案,很清楚的看到程序的2)調用fac()函數自己。同樣經過第三層調用後進入第四層調用,這時候n=1,算出1!=1,返回值算出4!=4*3!=4*6=24,結束整個遞歸調用,得出最終結果並輸出。滿足結束遞歸的條件,然後把得出的結果1返回給第三次調用的fac函數,得出2*1!=2,然後把結果2返回給第二次調用的fac函數,得出3*2!=6,最後第一次調用的fac函數根據第二次調用的
我們做事情,一般都是從頭開始的,而遞歸卻是從末尾開始的。比如上面的函數,當n>1的時候,就只能求助於n-1,而(n-1)>1時,就求助於n-2,然後……直到(n-k)=1時,函數fac終於有了返回值1了,它再從頭開始計算,然後一直算到n爲止。所以說,遞歸簡直就是一個數學模型,它的工作過程就是自己調用自己。以下是幾點對遞歸的說明:
1、當函數自己調用自佔用的存儲單元也就越己時,系統將自動把函數中當前的變量和形參暫時保留起來,在新一輪的調用過程中,系統爲新調用的函數所用到的變量和形參開闢另外的存儲單元(內存空間)。每次調用函數所使用的變量在不同的內存空間。
2、遞歸調用的層次越多,同名變量的多。一定要記住,每次函數的調用,系統都會爲該函數的變量開闢新的內存空間。上一層的調用點,同時取得當初進入該層時,函數中的變量和形參所佔用的內存空間的數據。
4、在開發過程中使用printf()和getchar()可以看到執行過程,並且可以在發現錯誤後停止運行。
很多人說所有遞歸問題都可以用非遞較好的可讀性,因此很多問題用遞歸可很容易解決。同時由於遞歸調用過程中,系統要爲每一層調用中的變量開闢內存空間、要記住每一層調用後的返回點、要增加許多額外的開銷,因此函數的遞歸調用通常會降低程序的運行效率(在許多情況下,速度的差別不太明顯)。
過這樣一個動物繁殖問題:若一頭小母牛,從出生起第四個年頭開始每年生一頭母牛,按此規律,第n年時有多少頭母牛?
如果不用遞歸函數來做,每當母牛到第4歲的時候纔會生下一頭小母牛,所以,每年增加的新的1歲小母牛都是上一年3歲的 3、當本用非遞歸的方法往往使程序變得十分複雜難以讀懂,而函數的遞歸調用在解決這類問題時能使程序簡潔明瞭有
我曾經碰到母牛加上4歲的母牛生下數量之和次調用的函數運行結束時,系統將釋放本次調用時所佔用的內存空間。程序的流程返回到歸的方法來解決,能不用遞歸就不用遞歸。但是對於一些比較複雜的遞歸問題,分析過程如圖2所示
給出程序如下:
main()
{int i,n; scanf("%d",&n);
for(i=2;i<=n;i++)
{ f4=f4+f3;f3=f2;f2=f1;f1=f4;}
printf(“now you have %
int f1=1,f2=0,f3=0,f4=f(n)=f(n-1)+f(n-3)呢,請看:
f(n)-f(n-1)=f(n-3)
因爲第n年要比n-1年多的牛,都是0;
printf("pleas但是可讀性太差,不易理解。那麼如果用遞歸函數求此問題呢?
我們先寫出函數表達式:f(n)=f(n-1)+f(n-3)
爲什麼大於三歲的牛生的小牛,而e input how many years it past:\n");
d cattle!\n “,f1+f2+f3+f4); }
程序雖然簡短, f(n-3)正是那些在n年大於三歲的牛,然後它們在第n年生下相同數量的小牛。
源代碼如下:
#include< e(ct,n-1)+cattle(ct,n-3)); }
main()stdio.h>
int cattle(int ct,int n)
{ if(n<4) return (ct);
else return (cattl
{int ct=1,n; se input how many years it past:\n");
scanf("%d",&n);
printf("you have %d cattle
printf("plea now!\n",cattle(ct,n));}
運行結果如圖3所示:見,遞歸函數的主要優點是可以把算法寫的比使用非遞歸函數時更清晰更簡潔,而且某些問題,特別是與
可人工智能有關的問題,更適宜用遞歸方法。遞歸的另一個優點是,遞歸函數不會受到懷方使用if語句,強迫函數在未執行遞歸調用前返回。如果不這樣做,在調用函數後,它永遠不會返回,,較非遞歸函數而言,某些人更相信遞歸函數。編寫遞歸函數時,必須在函數的某些地能解決,但是造成無窮遞歸。在遞歸函數中不使用if語句,是一個很常見的錯誤。此外,象漢諾塔問題就只能靠遞歸才疑現實中很多問題都是比較簡單的,沒有象漢諾塔那麼複雜,我們只要會用遞歸編程來爲我們解決一些問題就行了,所以就不必深究了。