典型算法及應用——“遞歸法”探究


     在起始條件已知的情況下,解決一類重複性問題的最佳方案莫過於使用程序設計的三大基本結構之一的“循環”結構(分爲“有限次”和“無限次”循環兩種情況)。然而現實生活中也存在這樣一類問題——起始條件不明確,但結尾卻已知;或者問題自身嵌套着自身。在這種情況下我們將採用反向思維,從結尾條件開始往前推演,直到把起始條件推算出爲止。這樣的一種算法往往被成爲“遞歸”法。本章節將主要對此算法進行一系列的探究。

 

一、遞歸法的定義和數學模型:

遞歸法在數學上的表達函數有點特殊,它是一種自調用函數,形式如下:

從定義式可以看出遞歸函數的最大特徵在於自身的輸出作爲自身的輸入,直至輸入爲某一個條件而終止。

下面就結合實踐,具體闡述並驗證這一理論。

 

【例1】有甲、乙、丙、丁四人,從甲開始到丁,一個比一個大1歲,已知丁10歲,問甲幾歲?

【分析】這是遞歸法的一道非常典型的題目——因爲我們可以很顯然知道:假設要計算甲的年齡,那麼必須直到乙的年齡;同樣,算乙的必須直到丙的,算丙的必須知道丁的,因爲丁已知,自然可以往前推算了。現在假設有一個數學模型(函數)可以計算出他們各自的年齡(方便期間我們給他們編號——甲=1,乙=2,丙=3,丁=4),那麼存在這一個F(X)函數,X表示某人的編號,其規律如下:

F(1)=F(2)+1

F(2)=F(3)+1

F(3)=F(4)+1

F(4)=10

顯然,直到X=4的時候是一個終止值,其餘情況下都是返回F(X’),F(X’’)……F(X’’……’),且前者總是比後至大1,這也符合了X’和X總是呈現一定函數關係(設想一下,如果不是等差和等比,又怎麼可能在一個遞歸函數中進行計算?要知道,函數本身就是一個公式表示,既然是公式,那麼一定是一種函數關係Y=F(X)),此處顯然X和X’的關係是X=X’+1。

根據規律式,我們可以寫出該遞歸函數:

int AgeCal(int id)
{
    if(id==4) return 10;
    else
    return (AgeCal(id+1)+1); 
}

 

【例2】計算n!

【分析】雖然這道題目不像例1一樣清晰明瞭告訴你使用“遞歸”法反推,但是我們有這樣一個常識——n!=(n-1)!*n;(n-1)!=(n-2)!*(n-1)……n=0或1,返回1.

顯然n與n-1,n-2也是線性的遞減數列(等差關係)。其規律如下:

F(n)=F(n-1)*n

F(n-1)=F(n-2)*(n-1)

F(n-2)=F(n-3)*(n-2)

……

F(1)=1或者F(0)=1(防止別人直接輸入0)

編寫其遞歸函數,如下:

int Fac(int n)
{
    if(n==1 || n==0)
{
    return 1;
}
else
  return Fac(n-1)*n;
}

 

從例1、2可以知道,遞歸函數編寫非常清晰明瞭,且結構簡單輕快,難點在於發現遞歸函數,一旦遞歸函數發現,只要對應翻譯成某種程序語言的代碼就可以了。

【例3】求一組整數中的最大(小)值(整數是一個int[]數組,個數未知)。

【分析】當數字只有兩個的時候,我們可以使用>和<直接比較;但是當數字超過2個的時候(假設3個),那麼我們可以使用一個預訂的函數(比如Max(1,2)和3進行比較),由於1,2兩個數比較的時候已經得到一個最大值,因此在回代到Max中又變成了兩個數的比較。這樣,我們可以發現一個規律:

F(1,2,3,4……n)=F(1,2,3,4……n-1)和n比較

F(1,2,3,4……n-1)=F(1,2,3,4……n-2)和n-1比較

……

F(1,2,3)=F(1,2)和3比較

F(1,2)=結果(並回代)

相應的遞歸函數如下(C#):

Code
int Max(int[]numbers)
{
    if(numbers.Length==2)
    {
        return (numbers[0]>numbers[1]?numbers[0]:numbers[1]);
    }
    else
    {
        int[]tempnumbers=new int[numbers.Length-1];
        for(int i=0;i<numbers.Length-1;++i)
        {
            tempnumbers[i]=numbers[i];
        }
        return (Max(tempnumbers)>numbers[numbers.Length-1]? Max(tempnumbers): numbers[numbers.Length-1]
    }
}

 

【例4】計算下列算式(共n項,n由輸入決定)

2/1+3/2+5/3+8/5+……+A/B+(A+B)/A+……

【分析】本算式有兩部分組成——分母和分子,我們逐一分析:

首先看分母:第一項是1(我們把“分子/分母”看成一個項),第二項是第一項的分子,第三項是第二項的分子……第n項是第n-1項分子,那麼我們可以寫出這樣一個遞歸函數:

double GetFenMu(int n)
{
    if(n==1)return 1;
    else
    {
        return GetFenZi(n-1);
}
}

再看分子,第一項是2,第二項是第一項的分母+分子……第n項是n-1項的分母和分子的和,遞歸函數自然如下:

double GetFenZi(int n)
{
    if(n==1)return 2;
    else
    {
        return GetFenZi(n-1)+GetFenMu(n-1);
    }
}

主程序調用的時候,只需要外部一個大循環即可:

for(int i=1;i<=n;++i)
{
    sum+=GetFenZi(i)+GetFenMu(i);
}

例4的遞歸函數有兩個,它們相互調用,直到彼此雙方完成返回結果爲止。相比較例1~例3而言,這種調用方法稱爲“間接遞歸”。因此,我們這就引出“間接遞歸”的定義——

若存在着F(X)和G(Y)兩種函數,它們分別以對方的輸出作爲自己的輸入,直到X或者Y滿足某個特定常數爲止,這樣的遞歸稱爲“間接遞歸”;同樣地,後者的輸入X’(Y’)必須和前者X(Y)呈現固定的函數關係。

*二、遞歸和遞推的關係探究(猜測,草案,可供參考):

       我們知道,遞歸是遞推的反思維。那麼現在我們感興趣的問題在於——遞歸和遞推之間是不是有一個普遍使用的關係式(函數)?

       爲了實現這個目標,我們不妨先來探究下計算機內部究竟在遞歸的時候幹了什麼?數據存儲的結構是怎樣的?這將有助我們進一步構建遞歸到遞推算法規律的發現。我們還是以例1作爲引證,再次觀察以下式子:

F(1)=F(2)+1

F(2)=F(3)+1

F(3)=F(4)+1

F(4)=10

我們發現,當主函數調用F函數的時候(輸入1),程序開始轉入F函數內部,帶着參數“1”進入函數體進行計算。如果該函數不是一個遞歸函數,那麼它應該直接返回一個結果給主函數,但是它在不滿足終止條件的時候觸發了自身(進入F(2)),此時,因爲F(1)的結果尚未得出,但是計算機主進程不得不轉入F(2)的函數計算,此時它將在堆棧區域開闢一個地址,存放F(1)函數的計算結果(是一個式子F(2)+1),此時主進程在進入F(2),接着又要如法炮製,直到F(4) 得到圓滿結果,主進程將根據先前堆棧中的指針以此將結果彈出,直到最底層的那個F(1),求解完畢。所以遞歸的本質是一個壓棧和出棧的過程。

根據這個原理,我們使用“壓棧-出棧”方式解決例1問題(使用C#語言)

【分析】首先我們應該爲每一個人創建一個對象,以便保持前次的狀態;接着我們創建這些對象,逐一將他們放入堆棧中(當到達第四個人的時候,給出初值10),接着在出棧的過程中,使用臨時變量記錄每一個出棧的數值,並以此進行遞增(+1)。下面是源碼:


發佈了253 篇原創文章 · 獲贊 4 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章