遞歸算法與非遞歸算法比較

轉載自:https://blog.csdn.net/mhsszm/article/details/78445591


非遞歸效率高;遞歸代碼寫出來思路清晰,可讀性強。

生成可執行文件大小應該和編譯器有關吧。。。。

遞歸的話函數調用是有開銷的,而且遞歸的次數受堆棧大小的限制。 
以二叉樹搜索爲例: 

bool search(btree* p, int v) 
{ 
    if (null == p) 
        return false; 

    if (v == p->v) 
        return true 
    else 
    { 
        if (v < p->v) 
            return search(p->left, v); 
        else 
            return search(p->right, v); 
    } 
}

如果這個二叉樹很龐大,反覆遞歸函數調用開銷就很大,萬一堆棧溢出怎麼辦? 
現在我們用循環改寫: 

bool search(btree* p, int v) 
{ 
    while (p) 
    { 
        if (v == p->v) 
            return true; 
        else 
        { 
            if (v < p->v) 
                p = p->left; 
            else 
                p = p->right; 
        } 
    } 
    return false; 
}

遞歸好處:代碼更簡潔清晰,可讀性更好 

遞歸可讀性好這一點,對於初學者可能會反對。實際上遞歸的代碼更清晰,但是從學習的角度要理解遞歸真正發生的什麼,是如何調用的,調用層次和路線,調用堆棧中保存了什麼,可能是不容易。但是不可否認遞歸的代碼更簡潔。一般來說,一個人可能很容易的寫出前中後序的二叉樹遍歷的遞歸算法,要寫出相應的非遞歸算法就比較考驗水平了,恐怕至少一半的人搞不定。所以說遞歸代碼更簡潔明瞭。 

遞歸壞處:由於遞歸需要系統堆棧,所以空間消耗要比非遞歸代碼要大很多。而且,如果遞歸深度太大,可能系統撐不住。 

樓上的有人說: 
小的簡單的用循環,, 
太複雜了就遞歸吧,,免得循環看不懂 

      話雖然簡單,其實非常有道理:對於小東西,能用循環幹嘛要折騰?如果比較複雜,在系統撐的住的情況下,寫遞歸有利於代碼的維護(可讀性好)。 

      另:一般尾遞歸(即最後一句話進行遞歸)和單向遞歸(函數中只有一個遞歸調用地方)都可以用循環來避免遞歸,更復雜的情況則要引入棧來進行壓棧出棧來改造成非遞歸,這個棧不一定要嚴格引入棧數據結構,只需要有這樣的思路,用數組什麼的就可以。

至於教科書上喜歡n!的示例,我想只是便於遞歸思路的引進和建立。真正做代碼不可能的。


循環方法比遞歸方法快, 因爲循環避免了一系列函數調用和返回中所涉及到的參數傳遞和返回值的額外開銷。 

遞歸和循環之間的選擇。一般情況下, 當循環方法比較容易找到時, 你應該避免使用遞歸。這在問題可以按照一個遞推關係式來描述時, 是時常遇到的, 比如階乘問題就是這種情況。反過來, 當很難建立一個循環方法時, 遞歸就是很好的方法。實際上, 在某些情形下, 遞歸方法總是顯而易見的, 而循環方法卻相當難找到。當某些問題的底層數據結構本身就是遞歸時, 則遞歸也就是最好的方法了。


遞歸其實是方便了程序員難爲了機器。它只要得到數學公式就能很方便的寫出程序。優點就是易理解,容易編程。但遞歸是用棧機制實現的(c++),每深入一層,都要佔去一塊棧數據區域,對嵌套層數深的一些算法,遞歸會力不從心,空間上會以內存崩潰而告終,而且遞歸也帶來了大量的函數調用,這也有許多額外的時間開銷。所以在深度大時,它的時空性就不好了。

循環其缺點就是不容易理解,編寫複雜問題時困難。優點是效率高。運行時間只因循環次數增加而增加,沒什麼額外開銷。空間上沒有什麼增加。


遞歸算法與迭代算法的設計思路區別在於:函數或算法是否具備收斂性,當且僅當一個算法存在預期的收斂效果時,採用遞歸算法纔是可行的,否則,就不能使用遞歸算法。

當然,從理論上說,所有的遞歸函數都可以轉換爲迭代函數,反之亦然,然而代價通常都是比較高的。

但從算法結構來說,遞歸聲明的結構並不總能夠轉換爲迭代結構,原因在於結構的引申本身屬於遞歸的概念,用迭代的方法在設計初期根本無法實現,這就像動多態的東西並不總是可以用靜多態的方法實現一樣。這也是爲什麼在結構設計時,通常採用遞歸的方式而不是採用迭代的方式的原因,一個極典型的例子類似於鏈表,使用遞歸定義及其簡單,但對於內存定義(數組方式)其定義及調用處理說明就變得很晦澀,尤其是在遇到環鏈、圖、網格等問題時,使用迭代方式從描述到實現上都變得很不現實。


把遞歸函數轉換成非遞歸程序的一般方法

      把遞歸算法轉化爲非遞歸算法有如下三種基本方法:

(1). 通過分析,跳過分解過程,直接用循環結構的算法實現求解過程。

(2). 自己用棧模擬系統的運行時棧,通過分析只保存必須保存的信息,從而用非遞歸算法替代遞歸算法。

(3). 利用棧保存參數,由於棧的後進先出特性吻合遞歸算法的執行過程,因而可以用非遞歸算法替代遞歸算法。

●      遞歸函數的原理 
        用棧保存未完成的工作,在適當的時候從棧中取出並執行。系統保存了工作的數據和狀態,數據就是函數的局部變量, 
        狀態就是程序指針。 
  
●      非遞歸程序原理 
        1. 和遞歸函數的原理相同,只不過是把由系統負責保存工作信息變爲程序自己保存,這樣能減少保存數據的冗餘(主要是 
        節省了局部變量的空間),提高存儲效率。 

        2. 把程序要完成的工作分成兩類:手頭工作和保存在棧中的待完成的工作。手頭工作指程序正在做的工作。由於某些工作 
        不能一步完成,必須暫緩完成,於是可把它保存在棧中,這就是待完成的工作。

        3. 手頭工作必須有其結束條件,不能永遠做下去;保存的待完成工作必須含有完成該項工作的所有必要信息。

        4. 程序必須有秩序地完成各項工作。如,可把手頭工作恰當處理(直接處理或暫時保存)後,才能繼續接手下一步的工作。 

        5. 待完成工作必須轉換成手頭工作才能處理。 
  
●      棧的大小 
        所有遞歸問題,其遞歸過程可以展開成一棵樹,葉子節點是可解的,按照問題的要求,處理所有葉子節點,就可解決 
        問題本身。可能需要保存(Data, Status),Data是工作數據,Status是工作狀態;(Data, Status)決定了整個工作。 
        棧的大小等於樹的高度-1,-1是因爲根節點不需保存。 
  
●      舉例 
例1.    漢諾塔問題 
遞歸函數: 

void Hanoi(UINT x, UINT y, UINT n) 
// x    Source 
// y    Destination 
// n    Number of plates 
{ 
    if (n == 0) return; 
    Hanoi(x, x^y, n-1); 
    Move(x, y); 
    Hanoi(x^y, y, n-1); 
} 

說明:x、y可取1、2、3三數之一,並且x≠y,x^y表示x、y按位異或,得到除x、y之外的第三個數。1^2=3, 1^3=2, 2^3=1 

非遞歸程序: 

#define N 5 
tyepdef struct _HANOIDATA 
{ 
    UINT x; 
    UINT y; 
    UINT n; 
}HANOIDATA; 
void Hanoi(HANOIDATA hanoiData) 
{ 
    HANOIDATA  stack[N]; 
    int        top = -1;      //stack pointer 
  
    while (hanoiData.n || top != -1)    // 存在手頭工作或待完成工作 
    { 
        while (hanoiData.n)    // 處理手頭工作直到無現成的手頭工作, 
                               // 即下次的手頭工作必須從棧中取得 
        { 
            hanoiData.n --; 
            stack[++top] = hanoiData;  // 保存待完成工作 
            hanoiData.y ^= hanoiData.x; // 新的手頭工作 
        } 
        if (top != -1)  // 存在待完成工作 
        { 
            hanoiData = stack[top--];  // 從棧中取出 
            Move(hanoiData.x, hanoiData.y);   // 直接處理 
            hanoiData.x ^= hanoiData.y; // 未處理完的轉換成手頭工作 
        } 
    } 
} 


例2. 後根序遍歷二叉樹 
遞歸函數: 

void PostTraverse(BINARYTREE root) 
{ 
    if (root == NULL) return; 
    PostTraverse(root->LChild); 
    PostTraverse(root->RChild); 
    Visit(root); 
}

非遞歸程序: 

void PostTraverse(BINARYTREE p) 
{ 
    while ( p != NULL || !Stack.IsEmpty() )// 存在工作(手頭或待完成) 
    { 
        while (p != NULL)    // 處理手頭工作,直到無現成手頭工作 
        { 
            Stack.Push(p, RCHILD_AND_ITSELF); 
            p = p->LChild; 
        } 
        if (!Stack.IsEmpty())  // 是否存在待完成工作 
        { 
            Stack.Pop(p, Tag); 
            if (Tag == RCHILD_AND_ITSELF)   // 情況一: RChild &Itself 
            { 
                Stack.Push(p,ONLY_ITSELF)  // 保存待完成工作 
                p = p->RChild; // 新的手頭工作 
            } 
            else        //tag == ONLY_ITSELF,  情況二: Only Itself 
            { 
                visit(p); 
                p = NULL;     // 已無現成的手頭工作 
            } 
        } 
    } 
} 

●      總結 
非遞歸程序的設計應注意: 
1.      保存暫緩執行的工作 
2.      無現成手頭工作的條件 
3.      無待完成工作的條件 
  
程序模式 

void NonRecursiveFunction(DATATYPE Data) 
{ 
    while ( ExistHandyWork() || ExistSavedWork() ) 
    { 
        while ( ExistHandyWork() ) 
        { 

            Process(Work, Status)  //Probably push work onto stack 
            NewHandyWork(); 
        } 
        if ( ExistSavedWork() ) 
        { 
            Pop(Work, Status); 
            Process(Work, Status);  //Probably generate new handy work 
        } 
    } 
}

 

遞歸算法向非遞歸算法轉換

遞歸算法實際上是一種分而治之的方法,它把複雜問題分解爲簡單問題來求解。對於某些複雜問題(例如hanio塔問題),遞歸算法是一種自然且合乎邏輯的解決問題的方式,但是遞歸算法的執行效率通常比較差。因此,在求解某些問題時,常採用遞歸算法來分析問題,用非遞歸算法來求解問題;另外,有些程序設計語言不支持遞歸,這就需要把遞歸算法轉換爲非遞歸算法。

    將遞歸算法轉換爲非遞歸算法有兩種方法,一種是直接求值,不需要回溯;另一種是不能直接求值,需要回溯。前者使用一些變量保存中間結果,稱爲直接轉換法;後者使用棧保存中間結果,稱爲間接轉換法,下面分別討論這兩種方法。

1. 直接轉換法

直接轉換法通常用來消除尾遞歸和單向遞歸,將遞歸結構用循環結構來替代。

尾遞歸是指在遞歸算法中,遞歸調用語句只有一個,而且是處在算法的最後。例如求階乘的遞歸算法:

long fact(int n)
{
    if(n==0) 
        return 1;
    else
        return n*fact(n-1);
}

當遞歸調用返回時,是返回到上一層遞歸調用的下一條語句,而這個返回位置正好是算法的結束處,所以,不必利用棧來保存返回信息。對於尾遞歸形式的遞歸算法,可以利用循環結構來替代。例如求階乘的遞歸算法可以寫成如下循環結構的非遞歸算法:

long fact(int n)
{
  int s=0;
  for(int i=1; i<=n;i++)
      s=s*i;//用s保存中間結果
  return s;
}

單向遞歸是指遞歸算法中雖然有多處遞歸調用語句,但各遞歸調用語句的參數之間沒有關係,並且這些遞歸調用語句都處在遞歸算法的最後。顯然,尾遞歸是單向遞歸的特例。例如求斐波那契數列的遞歸算法如下:

int f(int n)
{
    if (n==1 | | n= =0)
        return 1;
    else
        return f(n-1)+f(n-2);

}

對於單向遞歸,可以設置一些變量保存中間結構,將遞歸結構用循環結構來替代。例如求斐波那契數列的算法中用s1和s2保存中間的計算結果,非遞歸函數如下:

int f(int n)
{
  int i,s;
  int s1=1, s2=1;
  for(i=3; i<=n; ++i)
    {
        s=s1+s2;
        s2=s1; // 保存f(n-2)的值
        s1=s; //保存f(n-1)的值
  }
  return s;
}

2. 間接轉換法

該方法使用棧保存中間結果,一般需根據遞歸函數在執行過程中棧的變化得到。其一般過程如下:

將初始狀態s0進棧
while (棧不爲空)
{
  退棧,將棧頂元素賦給s;
  if (s是要找的結果) 返回;
  else
    {
     尋找到s的相關狀態s1;
     將s1進棧
  }
}

間接轉換法在數據結構中有較多實例,如二叉樹遍歷算法的非遞歸實現、圖的深度優先遍歷算法的非遞歸實現等等。

使用非遞歸方式實現遞歸問題的算法程序,不僅可以節省存儲空間,而且可以極大地提高算法程序的執行效率。本文將遞歸問題分成簡單遞歸問題和複雜遞歸問題;簡單遞歸問題的非遞歸實現採用遞推技術加以求解,複雜遞歸問題則根據問題求解的特點採用兩類非遞歸實現算法,使用棧加以實現。

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