遞推算法
遞歸算法大致包括兩方面的內容:1)遞歸起點 ; 2)遞歸關系
遞推起點
遞歸起點一般由題目或者實際情況確定,不由遞歸關系推出。如果無法確定遞歸起點,那么遞歸算法就無法實現。可見,遞歸起點是遞歸算法中的重要一筆。
遞推關系
遞歸關系是遞歸算法的核心。常見的遞歸關系有以下幾項:
- 1)一階遞推;
- 2)多階遞推;
- 3)間接遞推;
- 4)逆向遞推;
- 5)多維遞推。
下面通過栗子來詳細介紹一下上述類別的遞推關系。
1. 一階遞推
在計算f(i)時,只用到前面項中的一項,如等差數列。公差為3的等差數列,其遞推關系為:
f(i)=f(i-1)+3
eg. 平面上10條直線最多能把平面分成幾部分?
分析:以直線數目為遞推變量,假定i條直線把平面最多分成f(i)部分,則f(i-1)表示i-1條直線把平面分成的最多部分。在i-1條直線的平面上增加直線i,易得i與平面上已經存在了的i-1條直線最多各有一個交點,即直線i最多被分成i段,而這i段將會依次將平面一分為二,即直線i將最多使平面多增加i部分。所以,遞推關系可表示為:f(i)=f(i-1)+i
易得當0條直線時,平面為1部分。所以f(0)=1為遞推起點。
上述分析可用下面代碼表示(c++):
#define MAX 100
int f[MAX] //存放f(i)
int lines(int n){
//輸入n為直線數目
//輸出最多部分數
int i;
f(0)=1;
for(i=1;i<=n;i++){
f[i]=f[i-1]+3;
}
return f[i];
}
2. 多階遞推
在計算f(i)時,要用到前面計算過的多項,如Fibonacci數列。
eg.求Fibonacci的第10項。
分析:總所周知,Fibonacci數列中的第n項等於第n-1項加上n-2項。所以遞推關系為
f(i)=f(i-1)+f(i-2);且f[0]=f[1]=1。
C++代碼如下:
#define MAX 100
int f[MAX];
int fib(int n){
//輸入n為項數
//輸出第n個fib數
int i;
f[0]=0;
f[1]=1;
for(i=2;i<=n;i++){
f[i]=f[i-1]+f[i-2];
}
return f[n]
}
3. 間接遞推
在計算f[i]時需要中間量,而計算中間量要用到之前計算過的項。
eg.現有四個人做傳球游戲,要求接球后馬上傳給別人。由甲先傳,並作為第一次傳球。求經過10次傳球,球仍回到發球人甲手中的傳球方式的種數。
分析:定義兩個狀態,1)當前球在甲上,經過i次傳球之后球仍在甲上,此狀況記為F,其傳球方式的種數為f(i);2)當前球不在甲手上,經過i次傳球之后球在甲手上,此狀態記為G,其傳球方式的種數為g(i)。
對於狀態1):甲傳出一個球之后,接球的人的狀態便變成G(i-1)了,由於甲可以傳給3個不同的人,所以f(i)=3g(i-1);
對於狀態2):持球者可以選擇把球傳給甲,此時是F(i-1)狀態;也可以把球傳給另外兩個人,即2G(i-1)狀態。所以g(i)=f(i-1)+2*g(i-1).
計算遞推起點,由於甲第一次不可能把球傳給自己,所以f(1)=0;其他人要傳一次球就把球傳給甲,那只有一種方式(直接把球傳給甲),即g(1)=1.
上述遞推關系便是間接遞推。用c++實現如下:
#define MAX 100
int f[MAX];
int g[MAX];
int ball(int n){
//輸入n為傳球次數
//輸出為傳球方式的種數
int i;
f[1]=0;
g[1]=0;
for(i=2;i<=n;i++){
f[i]=3*g[i-1];
g[i]=f[i-1]+2*g[i-1];
}
return f[n];
}
4. 逆向遞推
顧名思義,就是從后面開始往前推。
eg.硬幣下棋游戲。棋盤上標有第0站,第1站...第100站,一開始棋子在第0站,棋手每次投一次硬幣,若硬幣正面向上,則往前跳兩站;否則,往前跳一站...直到棋子跳到第99站(勝利大本營),第100站(失敗大本營)時,游戲結束。如果硬幣出現正反面的概率均為0.5,分別求出棋子到達勝利大本營和失敗大本營的概率。
分析:假設記從第i站開始,最后到達100站的概率為f(i)。而從第i站,投擲一次硬幣,有0.5的概率到達第i+1站,有0.5的概率到達i+2站。所以遞推關系為:f(i)=0.5f(i+1)+0.5f(i+2).
易得遞推起點f(100)=1,f(99)=0.因為到達99站,游戲結束。
上述就是逆遞推的一個過程。c++實現如下:
#define MAX 100
double f[MAX];
double prob(){
//無輸入
//輸出為到達100站的概率
int i;
f[100]=1.0;
f[99]=0;
for(i=98;i.=0;i--){
f[i]=0.5*f[i+1]+0.5*f[i+2];
}
return f[0];
}
5. 多維遞推
元素處於一個多維矩陣中,遞推需要使用矩陣中其他位置的元素。
例子日后更新。
遞歸函數
在計算機科學中,如果一個函數的實現中,出現對函數自身的調用語句,則該函數稱為遞歸函數。
遞推算法可以用遞歸函數來實現。一般來說循環遞推算法比遞歸函數要快,但遞歸函數的可讀性更棒。
把上面的部分遞推算法改寫成遞歸函數。
1)平面划分
int lines(int i){
if(i<=0)
return 1;
else
return lines(i-1)+i;
}
2)Fibonacci數
int fib(int i){
if(i==0)
return 0;
if(i==1)
return 1;
else
return fib(i-1)+fib(i-2);
}
由上面的代碼可以分析到,遞推起點在遞歸函數中起到了遞歸截止作用。
遞歸函數的執行過程
遞歸函數每次調用自身都會生成一個激活幀(包含程序的參數、局部變量、返回值、以及該程序執行完畢后返回上一層的指令地址等),同時把計算控制交給下一次調用。這些激活幀存在在系統中先進后出的棧里。所以,程序的遞歸調用過大的話,會引發棧溢出。
尾遞歸
在計算機科學里,尾調用是指一個函數里的最后一個動作是一個函數調用的情形:即這個調用的返回值直接被當前函數返回的情形。
上面說到遞歸函數需要在調用多次時需要保留很多激活幀,這會引發棧溢出。但如果采用尾遞歸的話,就可以避免這個情況。因為尾遞歸在程序的最后動作只是調用函數,不涉及其他計算問題,所以可以優化刪去很多中間的激活幀。
如上面遞歸函數fib(),其最后一步就涉及加法,所以不是尾遞歸,但可以把它改成尾遞歸。如下:
int fib(int n,int f1,int f2)
{
//初始f1=0;f2=1
if(n==0)
return f1;
else
return fib(n-1,f2,f1+f2)
}
由上面代碼可看到,函數的最后調用就是一個函數,不涉及其他計算。
小結
遞歸函數一定要有遞歸起點作為遞歸結束標志。