遞歸算法探討

遞歸算法探討

遞歸在計算機科學和數學中是一個很重要的工具, 它在程序設計語言中用來定義句法, 在數據結構中用來解決表或樹形結構的搜索和排序等問題。另外, 遞歸在計算方法、運籌學模型、行爲策略和圖論的研究中都得到了廣泛的應用。 

1、 遞歸的概念

若一個對象部分地包含它自己, 或用它自己給自己定義, 則稱這個對象是遞歸的; 在程序設計中, 若一個過程直接地或間接地調用自己, 則稱這個過程是遞歸的過程。在定義一個過程或函數時出現了調用本過程或函數的成分, 即調用自己本身, 稱之爲直接遞歸;若過程或函數P調用過程或函數Q , 而 Q 調用P, 稱之爲間接遞歸。對於“問題定義是遞歸的, 數據結構是遞歸的, 問題解法是遞歸的”這3種情況, 都可以採用遞歸方法來處理。

2、 遞歸算法的本質

遞歸算法的本質是把一個大型複雜的問題層層轉化爲若干與原問題相似的規模較小的問題來處理, 當規模小到一定程度時, 可以直接得出它的解, 這樣通過遞推就可得到原來問題的解。遞歸調用的次數必須是有限的, 必須有遞歸結束的條件。遞歸算法的執行過程分爲兩步, 第一步是從目標出發追溯到源頭, 稱爲回溯。第二步是從源頭逐步回代達到目標, 稱爲遞推。由於存在遞推, 在回溯時, 必須保留其返回的地址與參數, 使程序能夠返回到調用處繼續執行, 這一步是系統通過設置棧來實現的, 程序設計者無需對棧進行管理。

3、遞歸算法的設計

適宜用遞歸算法求解的問題的充要條件是: 問題具有某種可借用的類同自身的子問題描述的性質; 某一有限步的子問題有直接的解存在。遞歸算法的設計, 通常有以下3 個步驟: 

1、分析問題, 設計遞歸公式將一個問題化解爲一個或多個子問題求解, 且子問題和原問題具有相同的解法。

2、設計遞歸結束條件, 控制遞歸,遞歸最後一級的調用必須不能再進行遞歸。 

3、確定參數,設計遞歸函數遞歸過程或遞歸函數的參數值在遞歸過程中必須是按規律變化的,且參數值的增或減方向應與遞歸終止條件相匹配, 這樣才能控制遞歸調用。一般遞歸函數設計的格式爲:  

if (遞歸結束條件)  

return (結束遞歸時的返回值);

else

return (遞歸表達式);

4、遞歸算法的實例

例1: 用遞歸函數編程求n 的階乘n!。階乘函數的遞歸定義如下: n! =n×(n-1)!  (n> 0)。這種定義方法是用階乘函數自身定義了階乘函數。由於n!和(n-1)!都是同一個問題的求解, 因此可將n!用遞歸函數來描述。

程序代碼如下: 

Long f( int n) {

 if ( n = = 0 )

 return 1; //遞歸的終止條件及相應的操作 

else

 return f (n-1) ; //遞歸調用 } 

例2: 中序遍歷二叉樹的遞歸算法。 

void Inorder ( BTreeNode BT ) {

 if ( BT ! = NULL ) {

 Inorder ( BT - > lchild) ;

 Visit (BT ) ;

 Inorder ( BT - > rchild) ; } } 

5、遞歸算法的執行過程分析

遞歸的執行依賴系統堆棧的支持, 遞歸的執行過程主要分爲兩步, 回溯(逐層深入遞歸調用) 和遞推(層層向上遞歸返回) , 在回溯時需要做的工作有: ①進行斷點保存, 局部變量、形式參數保存。②控制流程轉向遞歸調用的入口。

在本次遞歸調用結束後向上層調用返回時需要做的工作有: ①保存本次調用的函數結果, 恢復調用函數時的局部變量和形式參數。②根據遞歸調用時將控制流程轉回到調用函數中遞歸調用的下一行代碼處繼續執行。

綜上所述, 遞歸算法的執行過程是不斷地自調用, 直到到達遞歸出口才結束自調用過程;到達遞歸出口後,遞歸算法開始按最後調用的過程最先返回的次序返回;返回到最外層的調用語句時遞歸算法執行過程結束。

6、遞歸算法的非遞歸化

遞歸算法在執行時, 存在多次進棧和出棧, 流程的跳轉和返回, 甚至會出現多次重複計算, 從而影響執行效率。還有一些高級程序設計語言沒有提供遞歸的機制和手段。因此, 有些時候將遞歸算法非遞歸化是有必要的。非遞歸化最重要的是理解遞歸的執行過程。對於一般的遞歸算法,可以利用以下兩種方法對其進行非遞歸化。

1、 尾遞歸的非遞歸化

如果遞歸調用語句是函數的最後一條執行語句,則稱這種遞歸調用爲尾遞歸。當遞歸調用進入內層時,外層上與各形式參數對應的實際參數值和返回地址都會被編譯系統自動保存下來,以備返回時使用。對於尾遞歸,調用返回時,其後已沒有執行語句了。因而外層的實際參數值不會再用到,故沒有必要保留。此外, 由於遞歸調用語句是最後一條可執行語句,返回地址肯定在函數末尾,故其返回地址也沒有必要保留下來。對於這種情況,關鍵是從遞歸調用出發,從上而下遞歸到底, 找到遞歸的終止條件, 然後用循環實現遞歸算法的非遞歸化。

例3: 求n 的階乘n!的遞歸算法的非遞歸化。

例1中給出了求n的階乘n!的遞歸算法,從上而下遞歸:

 f(n) = nf(n-1) = n(n-1)f(n-2) = n(n-1) (n- 2) f (n- 3) : : f (0)

 f (0) = 1。

設最終結果用f表示, 由遞歸的終止條件“n=0時, 結果爲1”知, f的初始值= 1。由此,可從下而上地用循環實現求f(i),其中i從1到任意正整數n,f隨着i的變化而變化, 其非遞歸算法如下: 

Long f(int n) {

int i;

long f = 1; 

for( i=1; i<=n; i++) 

f = f*i; 

return f; } 

類似的情形很多, 如求2個正整數的最大公約數和求Fibonacci數列等。

2、 非尾遞歸的非遞歸化

如果遞歸調用語句不是函數中的最後一個語句, 則稱該遞歸調用爲非尾遞歸。對於非尾遞歸調用中的入口地址, 計算機隱含地自動設置堆棧, 保留調用入口地址, 供遞歸返回使用。而用非遞歸方法, 堆棧是人爲設定, 顯現在程序中, 功能與遞歸算法相同。

例4: 中序遍歷二叉樹。

由二叉樹的遞歸算法的執行過程知, 在二叉樹非空時, 首先訪問根的左子樹, 再訪問根, 最後訪問根的右子樹; 訪問根的左子樹時, 先要訪問左子樹根的左子樹, 再訪問左子樹的根, 其次再訪問左子樹根的右子樹⋯⋯, 如此遞歸下去, 一直到樹的最左下結點被訪問(向左下搜索時, 將當前結點壓入系統棧中保存, 以便向上回退時調出) , 然後訪問最左下結點的父結點, 通過彈棧獲得最左下結點的父結點, 然後處理該父結點的右子樹, 以此類推, 循環直到整棵樹訪問完畢。根據上述對遞歸執行過程的分析, 其對應的非遞歸算法爲: 

void Inorder ( BTreeNode BT ) 

{

if ( ! BT ) return ;

Stack S = Init-Stack () ; 

while ( ! Empty-Stack(S) ) 

{  while(BT) //當指針BT非空時入棧 

{Push (S,BT);

   BT = BT-> lchild;} 

Pop(S,BT) ; 

Visit(BT);

BT = BT->rchild; 

} //end while (Empty-Stack ( s) ) 

}//end InOrder () 

7、結束語

遞歸算法具有代碼簡潔, 思路清晰的優點, 是設計算法的強有力工具。一般而言, 遞歸程序的執行效率低於非遞歸程序, 但非遞歸算法往往難於編寫, 容易出錯; 理論上, 遞歸算法都可以轉化爲非遞歸算法, 但存在一些算法很難非遞歸化, 如複雜的間接遞歸, 因此, 要根據問題需求及軟件和硬件的環境等具體情形選擇遞歸還是非遞歸。



FROM:http://blog.csdn.net/kntao/article/details/6146721

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章