1. 程式人生 > >【c語言】遞迴和非遞迴的相互轉換

【c語言】遞迴和非遞迴的相互轉換

前面已經介紹過遞迴的相關概念這裡不多介紹,直接介紹轉換方法:
一、遞迴轉非遞迴的兩種方法
1、一般根據是否需要回溯可以把遞迴分為簡單遞迴和複雜遞迴,簡單遞迴就是根據遞迴式來找出遞推公式(這也就引申出分治思想和動態規劃)
2、複雜遞迴一般就是模擬系統處理遞迴的機制,使用棧或佇列等資料結構儲存回溯點來求解
二、如何用棧實現遞迴與非遞迴之間的轉換
1、遞歸於非遞迴的原理
遞迴與非遞迴的轉換基於以下的原理:
所有的遞迴程式都可以用樹結構表示出來
下面我們以二叉樹來說明,不夠大多數情況下二叉樹已經夠用,而且理解了二叉樹的遍歷,其它的樹遍歷方式就不難了
1)前序遍歷
a)遞迴方式

void
preorder(Bitree T)//先序遍歷二叉樹的遞迴演算法 { if(T!=NULL) { cout<<T->val<<" ";//訪問當前節點 preorder(T->left);//訪問左子樹 preorder(T->right);//訪問右子樹 } }

b)非遞迴方式

void preorder_nonrecursive(Bitree T)//先序遍歷二叉樹的非遞迴演算法
{
    stack<Bitree> s;
    s.push(T);//根指標進棧
    while
(!s.empty()) { while(gettop(s,T)&&T)//向左走到盡頭 { cout<<T->val<<" ";//每向前走一步都訪問當前節點 push(T->left); } s.pop(); if(!s.empty())//向右 { s.pop();//空指標退棧 s.push(T->right); } } }

2)中序遍歷
a)遞迴方式

void inorder(Bitree T)//中序遍歷二叉樹的遞迴演算法
{
    if(T!=NULL)
    {
        inorder(T->left);//訪問左子樹
        cout<<T->val<<" ";//訪問當前節點
        inorder(T->right);//訪問右子樹
    }
}

非遞迴:

void inorder_nocursive(Bitree T)
{
    stack<Bitree> s;
    s.push(T);//根指標入棧
    while(!s.empty())
    {
        while(gettop(s,T)&&T)//向左走到盡頭
        s.push(p->left);
        s.pop();//空指標退棧
        if(!s.empty())
        {
            s.pop();
            visit(T);//訪問當前節點
            s.push(p->right);//向右走一步
        }
    }
}

c)後序遍歷
遞迴方式:

void postorder(Bitree T)//後序遍歷的遞迴演算法
{
    postorder(T->left);//訪問左子樹
    postorder(T->right);//訪問右子樹
    cout<<T->val<<" ";//訪問當前節點
}

非遞迴方式:

typedef struct{
    BTNode* ptr;
    enum {1,1,2}mark;
}PMType;
//有mark域的節點指標型別
void postorder_noncursive(Bitree T)
{
    PMType a;
    stack<PMType> s;
    s.push({T,0});//根節點入棧
    while(!s.empty())
    {
        s.pop();
        switch(a.mark)
        {
        case 0:
        push({a.ptr,1});//訪問左子樹
        if(a.ptr->left)
        {
            s.push({a.ptr->left,0});
        }
        break;
        case 1:
        s.push({a.ptr,2});//修改mark域
        if(a.ptr->right)
        {
            s.push({a.ptr->right,0});//訪問右子樹
        }
        break;
        case 2:
        visit(a.ptr);//訪問節點
        }
    }
}

4)如何實現遞迴與非遞迴的轉換
通常一個函式在呼叫另一個函式之前,要做如下的事情:
a)將實在引數,返回地址等資訊傳遞給被調函式儲存;
b)為被呼叫函式的區域性變數分配儲存區
c)將控制轉移到被調函式的入口
從被呼叫函式返回呼叫函式之前,也要做三件事情
a)儲存被調函式的計算結果
b)釋放被調函式的資料區
c)依照被調函式儲存的返回地址將控制轉移到呼叫函式
所有的這些,不論是變數還是地址,本質上來說都是“資料”,都是儲存在系統所分配的棧中的。
遞迴呼叫時資料都是儲存在棧中的,有多少個數據就要設定多少個棧,而且最重要的一點是:控制所有這些棧的棧頂指標都是相同的,否則無法實現同步。
下面來解決第二個問題:在非遞迴中,程式如何知道到底轉移到哪個部分繼續執行?
回到上面說的樹的三種遍歷方式,抽象出來只有三中操作:訪問當前節點,訪問左子樹,訪問右子樹,這三種操作的順序不同,遍歷方式也不同。如果我們再抽像一點,對這三種操作再進行一個概括可以得到:
a)訪問當前節點:對目前的資料進行一些處理
b)訪問左子樹:變換當前的資料可以進行下一次處理
c)訪問右子樹:再次變換當前的資料進行下一次處理(與訪問左子樹所不同的方式)
下面以先序遍歷來說明:

void preorder(Bitree T)//先序遍歷二叉樹的遞迴演算法
{
        if(T)
        {
            cout<<T->val<<" ";//訪問當前節點
            preorder(T->left);//訪問左子樹
            preorder(T->right);//訪問右子樹
        }
}

preorder(T->left);就是把當前資料變換成它的左子樹,訪問右子樹操作以同樣方式理解。
現在回到我們提出的第二個問題:如何確定轉移到哪裡繼續執行?關鍵就在於以下三個地方:
a)確定對當前資料的訪問順序,簡單來說就是就是確定這個遞迴程式可以轉換為哪種方式遍歷的樹結構
b)確定這個遞迴函式轉換為遞迴呼叫樹時的分支是如何劃分的,即確定什麼是這個遞迴呼叫樹的“左子樹”和“右子樹”
c)確定這個遞迴呼叫樹何時返回,即確定什麼結點是這個遞迴呼叫樹的“葉子結點”
2、兩個例子
1)例子一

f(n) = n+1;(n<2)
f[n/2]+f[n/4](n>=2);

這個例子相對簡單一些,遞迴程式如下:

int f_recursive(int n)
{
    int u1,u2,f;
    if(n<2)
    {
        f=n+1;
    }
    else
    {
        u1 = f_recursive((int)(n/2));
        u2 = f_recursive((int)(n/4));
        f=u1*u2;
    }
    return f;
}

下面按照我們上面說的,確定好遞迴呼叫樹的結構,這一步是最重要的。首先什麼叫做葉子結點,我們看到當n<2時,f=n+1,這就是返回的語句,有人問為什麼不是f=u1*u2,這也是返回的語句。
答案是:這條語句是在u1=exmp1((int)(n/2))和f_recursive((int)(n/4))之後執行的,是這兩條語句的父節點。其次什麼是當前結點,由上面的分析,f=u1*u2即是父節點,然後順理成章的u1=exmp1((int)(n/2))和f_recursive((int)(n/4))就分別是左子樹和右子樹了,最後我們可以看到,這個遞迴函式可以表示成後序遍歷的二叉搜尋樹,以上就是樹的分析

下面來分析一下棧的情況,看看我們把什麼資料儲存在棧中,在上面給出的後序遍歷的如果這個過程你沒非遞迴程式我們已經看到了要加入一個標誌域,因此在棧中要儲存這個標誌域;另外,u1,u2和每次呼叫遞迴函式時的n/2和n/4引數都要儲存,這樣就要分別有三個棧分別儲存:標誌域,返回量和引數,不過我們可以做一個優化,因為在向上一層返回的時候,引數已經沒有用了,而返回量也只有在向上返回時才用到,因此可以把這兩個棧合成一個棧
如果對於上面的分析你沒有明白,建議你根據這個遞迴函式寫出它的遞迴棧的變化情況以加深理解,再次重申一點:前期對樹結構和棧的分析是最重要的,如果你的程式出錯,那麼請返回到這一步來再次分析,最好把遞迴呼叫樹和棧的變化情況都畫出來,並且結合一些簡單的引數來人工分析你的演算法到底出錯在哪裡
例子2
遞迴演算法如下:

void swap(int array[],int low,int high)
{
    int temp = array[low];
    array[low] = array[high];
    array[high] = temp; 
}
int partition(int array[],int low,int high)
{
    int p;
    p=array[low];
    while(low<high)
    {
        while(low<high&&array[high]>=p)
        high--;
        swap(array,low,high);
        while(low<high&&array[low]<=p)
        low++;
        swap(array,low,high);
    }
    return low;
}
void qsort(int array[],int low, int high)
{
    int p;
    if(low<high)
    {
        p=partition(array,low,high);
        qsort(array,low,p-1);
        qsort(array,p+1,high);
    }
}

需要說明一下快速排序的演算法:partition函式根據陣列中的某一個數把陣列劃分為兩個部分,左邊的部分均不大於這個數,右邊的數均不小於這個數,然後再對左右兩邊的陣列進行劃分,這裡我們專注於遞迴與非遞迴的轉換,partition函式在非遞迴函式中同樣可以呼叫(其實partition函式就是對當前節點的訪問)
再次進行遞迴呼叫樹和棧的分析
遞迴呼叫樹:
a)對當前節點的訪問是呼叫partition函式
b)左子樹:qsort(array,low,p-1);
c)右子樹:qsort(array,p+1,high);
d)葉子節點:當low

void qsort_nonrecursive(int array[],int low,int high)
{
    int m[50],n[50],cp,p;
    //初始化棧和棧頂指標
    cp = 0;
    m[0] = low;
    n[0] = high;
    while(m[cp]<n[cp])
    {
        while(m[cp]<n[cp])//向左走到盡頭
        {
        p=partition(array,m[cp],n[cp]);//對當前節點的訪問
        cp++;
        m[cp] = m[cp-1];
        n[cp] = p-1;
        }
        //向右走一步
        m[cp+1] = n[cp]+2;
        n[cp+1] = n[cp-1];
        cp++;
    }
}