遞歸算法到非遞歸算法的轉換

遞歸實質在定義自身的同時又出現了對自身的調用。遞歸算法是許多軟件編程人員經常使用的方法,結構簡單、清晰、可讀性好。但在實際應用中也存在一些問題:1.並非每一門語言都支持遞歸,比較典型的FORTRAN語言,它明確規定了不容許直接或間接使用遞歸;2.遞歸算法在執行過程當中會消耗太多的時間和空間。而在實際設計程序過程當中,遞歸程序比非遞歸程序要容易設計,所以在許多狀況下,經常是先設計出遞歸程序,而後再將其轉換成等價的非遞歸程序。算法

1.基於循環結構的遞歸消除

可利用循環結構進行遞歸消除的遞歸有兩種狀況:尾遞歸單向遞歸編程

尾遞歸 即遞歸調用語句只有一個,並且處於算法的最後。 單向遞歸,即遞歸函數中雖然有一處以上的遞歸調用語句,但個遞歸調用語句的參數只與主調函數有關,相互之間參數無關,而且這些遞歸調用語句處於算法的最後。 對於尾遞歸和單向遞歸的遞歸消除各舉一個例子進行說明。典型的尾遞歸例子是求 N! (階乘)算法。 數據結構

算法爲:函數

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

分析算法可知,3!的遞歸依賴關係爲,fac(3)→fac(2)→fac(1) →fac(0),所以能夠利用循環結構直接從 fac(0)開始計算,一直循環到 fac(3)便可。 spa

算法爲:設計

int fac( int n){
  int fac=1;
  for (int i=1;i<=n; i++)
    fac=fac* i ;
  return fac;
}

典型的單向遞歸的例子是求斐波那契數列的 Fib(n)算法。 指針

算法爲:code

int Fib ( int n )
{
  if ( n <= 1 )
    return n;
  else
     return Fib (n-1) + Fib (n-2);
}

分析算法可知,Fab(5)的依賴關係如圖 1 所示。所以,由圖可知,可從下向上依次循環便可求得 Fib(5)。 blog

 

算法爲:遞歸

int Fib( int n )
{
   int x , y , z ;
  if ( n= =0 || n= =1)  
    return n;
  else
  {
    x=0 , y=1;
    for (i=2;i<=n; i++)
    {
      z=y;
      y=x+y;
      x=z;
    }
  }
  return y;
}

2.二叉樹遍歷

二叉樹的非遞歸遍歷是利用模擬棧的方法來實現非遞歸性遞歸的轉換。 遍歷分爲前序,中序和後序三種,一棵二叉樹的三種遍歷過程的遍歷路線相同,都是從左到右,可是遍歷的結果不一樣。 對於每種遍歷,樹中的結點都經歷三次,可是前序遍歷在第一次遇到節點時當即訪問,而中序遍歷是在第二次遇到節點時才訪問,後序遍歷在第三次遇到時才訪問。如下是三種遍歷的算法(其中前序和中序基本一致,固寫在同一算法中)

1)前序遍歷

遞歸遍歷:

void PreOrder(BiTree T)
{
  if(T!=NULL)
  {
    visit(T);
    PreOrder(T->lchild);
    PreOrder(T->rchild);
  }
}

 

非遞歸遍歷:

void PreOrder(Bi Tree *root,(*visit)())
{
  InitStack(s);//置空棧
  p=root;
  while(p! =Null||! stackempyt(s))
  {
    if(p! =Null)
    {
      Visit(p);
      Push(s,p);
      p=p->lchild;
    }
    else(! stackempty(s))
    {
      Pop(s,p);
      p=p->rchild;
    }
  }
}

2)中序遍歷

中序遞歸遍歷:

void InOrder(BiTree T)
{
  if(T!=NULL)
  {
      InOrder(T->lchild);
      visit(T);
      InOrder(T->rchild);
  }
}

中序非遞歸遍歷:

算法思想:能夠藉助棧,將二叉樹的遞歸算法轉換爲非遞歸算法,先掃描(並不是訪問)根結點的全部結點,並將他們一一進棧。而後出棧一個結點p(顯然p結點沒有左孩子結點或者左孩子結點已經被訪問過),則訪問他。而後掃描該結點的右孩子結點,將其進棧,再掃描該右孩子結點的全部左結點並一一進棧,如此繼續,直到棧空爲止。

void InOrder(Bi Tree *root,(*visit)())
{
  InitStack(s);//置空棧
  p=root;
  while(p! =Null||! stackempyt(s))
  {
    if(p! =Null)
    {
      Push(s,p);
      p=p->lchild;
    }
    else(! stackempty(s))
    {
      Pop(s,p);
      visit(p);
      p=p->rchild;
    }
  }
}

3)後序遍歷

後序遞歸遍歷:

void PostOrder(BiTree T)
{
  if(T!=NULL)
  {
      PostOrder(T->lchild);
      PostOrder(T->rchild);
      visit(T);
  }
}

後序非遞歸遍歷:

算法思想:由於後續遍歷遞歸二叉樹的順序是先訪問左子樹,再訪問右子樹,最後訪問根節點。當用堆棧來存儲結點,必須分清返回根節點時,是從左子樹返回的仍是從右子樹返回的。因此使用輔助指針人,其指向最近訪問過的結點,也能夠在結點中增長一個標誌域,記錄是否被訪問過。

void PostOrder(BiTree T){
    InitStack(s);
    p=T;
    r=NULL;
    while(p||!IsEmpty(s)){
        if(p){//向左
            push(S,p);
            p=p->lchild;//重置p指針,左子樹
        }
        else{//向右
            GetTop(S,p);
            if(p->rchild &&p->rchild!=r){//右子樹存在,且未被訪問過
                p=p->rchild;
                push(S,p);
                p=p->lchild;//重置p指針,右子樹
            }
            else{//知足後序遍歷時訪問結點條件
                pop(S,p);
                visit(p->data);
                r=p;//記錄最近訪問的結點
                p=NULL;//重置p指針,不須要左子樹,須要棧頂元素
            }
        }
    } 
}               

3.利用二叉樹的非遞歸遍從來消除遞歸

依靠二叉樹的非遞歸算法也可實現遞歸向非遞歸的轉換,由於遞歸程序均可以用樹結構表示,最後都轉化爲二叉樹的遍歷問題。 總的來講,轉換基本原理和二叉樹遍歷的非遞歸實現同樣仍是基於棧來消除遞歸。 所以肯定問題的遞歸調用樹,用樹遍歷的非遞歸算法來改進程序,就能達到遞歸向非遞歸的轉換了。舉例來講,對於斐波那契數列,遞歸定義爲:

 

調用二叉樹如圖 2 所示。如圖 2,求斐波那契數列 Fib(5)的非遞歸算法就是採用後序遍歷二叉樹的方法來遍歷斐波那契數列的調用二叉樹,凡是 visit 的部分替換成調用 Fib 函數便可。對於其它的遞歸算法也能夠採用一樣的辦法,先畫出調用二叉樹,而後根據實際狀況,判斷採用何種非遞歸遍歷,則利用二叉樹的非遞歸調用方法可完成遞歸向非遞歸的轉變。

 

參考:《遞歸到非遞歸算法的轉換》崔 蕊(南陽師範學院 計算機與信息技術學院 ,河南 南陽 473061)

   《王道——數據結構聯考複習指導》 王道論壇組編

相關文章
相關標籤/搜索