【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++;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章