【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++;
    }
}

猜你喜欢

转载自blog.csdn.net/flowing_wind/article/details/81200863