1. 程式人生 > >算法總結之遞推與遞歸

算法總結之遞推與遞歸

總結 c++代碼 不同的 否則 狀況 def 函數調用 n) ron

遞推算法

遞歸算法大致包括兩方面的內容: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)狀態;也可以把球傳給另外兩個人,即2
G(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)
    }

由上面代碼可看到,函數的最後調用就是一個函數,不涉及其他計算。

小結

遞歸函數一定要有遞歸起點作為遞歸結束標誌。

算法總結之遞推與遞歸