浅谈关于递归的一些感悟

来源:互联网 发布:黑客引导页html源码 编辑:程序博客网 时间:2024/05/22 12:35

最近在写数据结构中关于树的一些操作,其中最基础的就是构造一棵二叉树,然后去1.遍历二叉树,2.求出二叉树的层数,3.求出二叉树中的叶子结点数,4.交换二叉树的左右子树。
1.遍历二叉树的递归方法思想最为简单,以先序遍历为例,就是先访问根结点,然后以同样方法访问左子树,然后以同样方法访问右子树。(其实这里给的算法描述和二叉树的定义很像,根结点最多有两个孩子结点,左子树右子树又是一棵二叉树。同样在离散数学中许多定义都是递归定义),贴上代码就是

Status PreOrderTraverse(BiTree T,Visit visit){//第二个参数是访问函数指针类型,typedef Status (*Visit)(TNodeElemType elem)    //先序遍历    if(T){        if(visit(T->data))            return ERROR;        PreOrderTraverse(T->lchild,visit);        PreOrderTraverse(T->rchild,visit);    return OK;}

中序遍历和后序遍历与之别无二致,只需要改动一下访问函数的位置。其实最能表现出递归特点(就是符合人们对递归的看法)的是后序遍历,这个我们稍后再来讨论。
2.求出二叉树的层数
其实在想这个问题时,我还是很鸡的不太懂递归的奥义。我直接打算参照遍历的递归算法,来改动下用于使用。就是在遍历过程中不断向下探索到叶子结点,然后记录叶子结点的深度,最后在所有叶子结点中取最大值。然而这个想法很难实现,最后我还是先用非递归的方法去实现了,具体实现就是层次遍历,一直遍历到最底层。

int GetDepth(BiTree T){    //返回一个链式存储的二叉树的深度    //按层次遍历    int depth=0;    bool flag=true;    queue<BiTree>myqueue1,myqueue2;    BiTree p;    if(T){        //非空树        myqueue1.push(T);//树根入队1        while(flag==true){            depth++;            flag=false;            while(!myqueue1.empty()){                p=myqueue1.front();                myqueue1.pop();                if(p->lchild!=NULL||p->rchild!=NULL){//如果左右子树有一个不空,则深度加1                    flag=true;                    if(p->lchild!=NULL)                        myqueue2.push(p->lchild);//左子树树根入队2                    if(p->rchild!=NULL)                        myqueue2.push(p->rchild);//右子树树根入队2                }            }            while(!myqueue2.empty()){//子树树根移到队1,直至队2空                p=myqueue2.front();                myqueue1.push(p);                myqueue2.pop();            }        }    }    return depth;}

这个大家就随便看看吧。思想就是如上所说。然而这种方法在树的深度极大时(几万层),空间复杂度都显然的太大,而且速度想必也很慢。后来我又用递归方法重新写了一遍。这回我想清楚怎么使用递归了。要求一棵树的深度,就是求它的左子树和右子树中深度较大的一个深度加一,而求左右子树的深度又是使用这种方法。代码如下:

int GetDepth1(BiTree T){    //递归方法计算二叉树深度    if(T==NULL)//空树,深度为0         return 0;    return ( GetDepth1(T->lchild)>GetDepth1(T->rchild)?//树的深度是左右子树深度较大的那个 +1                 GetDepth1(T->lchild):GetDepth1(T->rchild) )                +1; }

先不讨论时间复杂度和空间复杂度,代码量就减少了一大半。而一旦想清楚了递归的解决问题的过程,剩下的两个问题的递归算法写起来也轻而易举。
3.求出二叉树中的叶子结点数
递归算法:1.空树返回0。2.如果树没有左孩子和右孩子,那么它是叶子结点,返回1。3.一棵不是空树的树的叶子结点数是左子树和右子树叶子结点数之和。代码如下:

int GetLeafNum1(BiTree T){    //用递归方式求二叉树中叶子结点个数    if(T==NULL)        return 0;     if(T->rchild==NULL&&T->lchild==NULL)//左右子树均为空,是叶子结点         return 1;    return GetLeafNum1(T->rchild)+GetLeafNum1(T->lchild); //树的叶子结点数就是左右子树叶子结点数和  }

这里相较于求层数只是多了几个帮助结束的判断条件,相信还是不难理解的。
4.交换二叉树的左右子树
这个递归算法也很简单:1.交换树根的左右子树。2.使用同样方法交换左子树左右子树。3.使用同样方法交换右子树左右子树。代码如下:

void ExchangeTree1(BiTree T){    //用递归方法交换一棵树的左右子树    if(T){//是否不是空树         if(T->lchild!=NULL||T->rchild!=NULL){//左右子树至少存在一个             //交换树的左右子树             BiTree temp;            temp=T->lchild;            T->lchild=T->rchild;            T->rchild=temp;        }else//左右子树都不存在则什么都不做             return;         //交换左子树的左右子树         ExchangeTree1(T->lchild);        //交换右子树的左右子树         ExchangeTree1(T->rchild);     }}

实际过程中为了避免不必要的交换和结束还加了一个判断。
*
下面进入正题,这篇文章其实是谈递归而不是说二叉树的。
一直以来,我都觉得递归和迭代是两个相对的概念,迭代是直接从第一步做到最后一步,而递归则需要将问题细分到原子,然后从原子再倒推回去。然而事实则是递归和迭代并没有什么实质的关联。递归事实上是递和归的组合,有时候递归函数不需要返回值,那么递归就只剩递而没有归了。比如上面的交换左右子树的函数,在交换完根结点的左右子树之后,事实上就等于把树根给扔了,下面的操作和树根一点关系都没有,一直向下递到所有子树被交换。然而说这样的递归没有归并不准确,虽然没有用到返回值,但是在交换完最后一层的左右子树后,还是要返回到最近一层的调用,去继续交换根的右子树的左右子树。
那么到底怎么去看待递归函数呢。其实递归函数只不过是自己调用自己。也就是在使用这个函数过程中又去内存开辟了一段空间使用这个函数,然后整个函数使用完后可能有返回值,就返回给调用它的函数,然后函数空间被释放;如果没有返回值就等于干完了它的活,直接释放掉就可以了。还是画个图来表示吧。
这是个普通的3层递归。
所以说在写递归函数时,只要有个清晰的思路,把递归函数看成普通的函数,写起来应该还是比较简单的。
*
文章也许有不足之处或错误的地方,欢迎指正。

0 0