递归的三个典型应用(汉诺塔+二叉树遍历)粘贴出来以求得加深对递归的理解

阅读更多

1.汉诺塔问题:

void hannoi (int n, char A, char B, char C)

{

    if (n == 1)

    {

        cout << "Move disk " << n << " from " << A << " to " << C << endl;

 

    }

    else

    {

        hannoi (n-1, A, C, B);

        cout << "Move disk " << n << " from " << A << " to " << C << endl;

        hannoi (n-1, B, A, C);

    }

}

 

2.二叉树的三种遍历:

(1)先序遍历

Status PreOrderTraverse (BiTree &T){

if (T){

std::cout<< T->data <

PreOrderTraverse (T->lchild);

PreOrderTraverse (T->rchild);

}

else return OK;

}

 

(2)中序遍历

Status InOrderTraverse (BiTree &T){

if (T){

InOrderTraverse (T->lchilde);

std::cout<< T->data <

InOrderTraverse (T->rchild);

}

else return OK;

}

 

(3)后序遍历

Status PostOrderTraverse (BiTree &T){

if (T){

PostOrderTraverse (T->lchild);

PostOrderTraverse (T->rchild);

std::cout<< T->data <

}

else return OK;

}

 

个人总结:感觉递归算法是非常强大的工具,尤其用在汉诺塔,树的遍历等,这样涉及大量重复性操作的问题求解中。

比如汉诺塔问题,要想将n个盘子由A移动到C,则先将前n-1个移动到B,然后再把第n个盘子由A移动到C,然后再把剩下的盘子由B移动到C。用了递归算法,只需重复调用算法即可,将负责的问题简单化。

再比如以上的三种二叉树的遍历操作,先序遍历,必定是先遍历根节点,然后遍历根的左子树,再遍历根的右子树。中序遍历,则是先遍历根的左子树,再遍历根节点,再遍历根的右子树,而在遍历左子树的过程中,将左子树当做一个单独的树,也按照先遍历左子树,根,右子树的顺序遍历,这样层次遍历下去,完成递归,在遍历左右子树的过程中不断又调用了递归方法。可以说是很好很强大的算法。

但以上四个算法,其实都还有可以改进的地方,那就是遍历过程所涉及的操作,以上四个算法就单纯的把它写死了,就是简单的输出,这样的弊端是:比如某天想要在遍历二叉树的过程中,将原来的值输出后,把原来的值加1,那么就不得不更改三个遍历算法,每个算法都要做改动,这样其实程序的可扩展性很差劲。所以最好的方法是,在遍历的时候,指定访问函数,通过访问函数对元素值就行处理:输出,更改,删除等等,这样某天想在遍历的时候,附加某些操作的时候,只需将其绑定的访问函数做更改即可即可,其他代码不需要再变动。

 

改动过后的以上四个算法如下所示:

1'汉诺塔问题

 

int i = 0;

void Move (char x, int k, char z){

std::cout << "The "<< ++i <<" step : Move " << k <<" from "<< x <<" to " << z <

}

 

void Hanoi (int n, char x, char y, char z)

{

if (1 == n){

Move (x, n, z);

}

else{

Hanoi (n-1, x, z, y);

Move (x, n, z);

Hanoi (n-1, y, x, z);

}

}

 

这样通过加入一个Move方法,在遍历的过程中对元素进行处理,增加了可扩展性。

如果要移动n个盘子,则总共需要2^n - 1步。

 

2'二叉树的先序遍历

 

void PrintElemType (TElemType e){

std::cout<< e <

}

 

Status PreOrderTraverse (BiTree &T. void (*Visit) (TElemType e)){

if (T){

Visit (T->data);

              PreOrderTraverse (T->lchild, Visit);

                PreOrderTraverse (T->rchild, Visit);

}

else return OK;

}

 

这样通过加入visit访问函数,使得后续遍历时想对元素做附加操作的话,只需通过更改特定的绑定函数就可以方便的完成。

另外这里用到的指针函数是一个非常差方便的概念,在NS3里面大量用到,需要注意。

 

3'二叉树的中序遍历

 

 

Status InOrderTraverse (BiTree &T, void (*Visit)(TElemType e)){

    if (T){

           InOrderTraverse (T->lchild, Visit);

   Visit (T->data);

           InOrderTraverse (T->rchild, Visit);          

    }else return OK;

}

 

4'二叉树的后序遍历

 

Status PostOrderTraverse (BiTree &T, void (*Visit)(TElemType e)){

    if (T){

          PostOrderTraverse (T->lchild, Visit);

  PostOrderTraverse (T->rchild, Visit);          

  Visit (T->data);

    }else return OK;

}

 

5''二叉树中序非递归遍历:递归遍历和非递归遍历的根本区别在于,递归时系统替你管理栈,非递归要自己管理。

 

typedef struct{

BiTree *base;

BiTree *top;

int stacksize;

}SqStack;

 

Status InitSqStack (SqStack &S){

S.base = (BiTree *) malloc (sizeof (BiTree) * STACK_INIT_SIZE);

if (!S.base) exit (OVERFLOW);

S.top = S.base;

S.stacksize = STACK_INIT_SIZE;

return OK;

}

 

bool IsStackEmpty (SqStack &S){

if (S.base == S.top) return true;

return false;

}

 

Status GetTop (SqStack &S, BiTree &e){

if (S.top == S.base) return ERROR;

e = *(S.top - 1);

return OK;

}

 

Status Pop (SqStack &S, BiTree &e){

if (S.top == S.base) return ERROR;

e = *--S.top;

return OK;

}

 

Status Push (SqStack &S, BiTree &e){

if (S.top >= S.base + STACK_INIT_SIZE){

S.base = (BiTree *) realloc (sizeof (BiTree) * (STACK_INIT_SIZE + STACK_INCREMENT));

if (!S.base) exit (OVERFLOW);

S.top = S.base + STACK_INIT_SIZE;

S.stacksize += STACK_INCREMENT;

}

*S.top++ = e;

return OK;

}

 

//中序遍历二叉树,非递归的第一种方法。

Status InOrderTraverseNonRecursion (BiTree &T, void (*Visit) (TElemType e)){

InitSqStack (S);

Push (S, T);

BiTree p;

while (!StackEmpty(S)){

while (GetTop (S, p) && p) Push (S, p->lchild);//入栈前不做检查,最后会入栈一个空指针

Pop (S, p);

if (!StackEmpty (S)){

Pop (S, p);

Visit (p->data);

Push (S, p->rchild);

}//if

}//while

return OK;

 

//中序遍历二叉树,非递归的第二种方法。

 

Status InOrderTraverseNonRecursion2 (BiTree &T, void (*Visit) (TElemType e)){

SqStack S;

InitSqStack (S);

BiTree p = T;

while (p || !StackEmpty (S)){

if (p) {//在入栈前先做检查,如果为空,表明已经到头。

Push (S, p);

p = p->lchild;

}

else {

Pop (S, p);

Visit (p->data);

p = p->rchild;

}

}//while

return OK;

}

 

比较以上两种中序遍历二叉树的非递归方法:

1.第一种,在将遍历指针入栈前不做检查,直接入栈,所以会出现将最后空指针加入到栈的情况。

2.第二种,在将遍历指针入栈前作检查,如果为空,表明已经到头,所以不会把空指针入栈。

 

3.第一种,在遍历右子树的时候,将右子树入栈Push (S, p->rchild);然后再返回while循环,继续遍历,指针入栈

4.第二种,p = p->rchild;然后返回while循环,继续判断p指针,以此来遍历右子树

除了这两点,其余的情况基本一样。

 

6.二叉树的先序非递归算法:

 

Status PreOrderTraverseNonRecursion (BiTree &T, void (*Visit) (TElemType e)){

if (!T) return ;

SqStack S;

InitSqStack (S);

Push (S, T);

BiTree p ;

while (!StackEmpty (S)){

Pop (S, p);

Visit (p->data);

if (p->rchild){

Push (S, p->rchild);

}

if (p->lchild){

Push (S, p->lchild);

}

}

return OK;

}

先序非递归遍历二叉树需要注意的是:栈不空的时候,出栈,访问出栈节点的数据,然后如果左子树和右子树不空,分别入栈,在入栈的时候,先入栈右子树,后入栈左子树,这样保证在出栈是,左子树先出。

同样的,和前序递归遍历的区别在于:非递归的时候自己管理栈,而递归的时候,操作系统替你管理栈。

 

7.二叉树的后序非递归算法:

 

Status PostTraverseNonRecursion (BiTree &T, void (*Visit) (TElemType e)){

SqStack S;

InitSqStack (S);

BiTree p = T;

BiTree previsited = NULL;

while (p || !StackEmpty (S)){

while (p) {

Push (S, p);

p = p->lchild;

}

GetTop (S, p);

if (p->rchild == NULL || p->rchild == previsited){

Pop (S, p);

Visit (p->data);

previsited = p;

p = NULL;

}

else{

p = p->rchild;

}

}

return OK;

}

二叉树的后续非递归遍历,核心在于,当一个节点的右子树为空,或者右子树已经访问过了的时候,才访问该节点,在访问该节点的同时,要同时做好三件事:

1.出栈当前节点(已经访问过了,必须出栈) Pop(S, p)  

2.将前一个访问的指针指向该节点

3.当前指针指向空,为下一次循环的出栈做好准备。

如果右子树尚未被访问过,则转到右子树,继续入栈,找最左下边要被访问的第一个元素。

 

-------------------------------扩展:二叉树的线索化和线索二叉树的遍历------------------------------------------

不管是递归还是非递归,二叉树的遍历,都是讲二叉树线性化进行遍历。除了第一个和最后一个节点之外,每个节点都有一个直接前驱和一个直接后继,所以二叉树的前序、中序、后序都是将二叉树先序、中序、后序线性化。

同时考虑到,在含有n个节点的二叉树中,空链域有n+1个,所以考虑将这n+1个链利用起来存储线索。(n+1的由来:n个节点,总共的链域有2n个。又因为n=所有分支数+1=B+1=n1+2n2+1,即n=n1+2n2+1,在这2n个链域中,使用了的有:n1+2n2 = n—1个,所以剩余的链域有2n-(n-1) = n+1个)

 

先来看二叉树的线索化,线索化,就是在遍历的过程中修改空域的指针,使其成为指向前驱和后继的线索。

以中序遍历线索化为例子。

void InThreading (BiThrTree p){//p--指向以p为根的树

if (p){

InThreading (p->lchild);//先找到最左边的节点。

if (!p->lchild) {//前驱线索

p->LTag = Thread;

p->lchild = pre;

}

if (!pre->rchild){//后继线索

pre->RTag = Thread;

pre->rchild = p;

}

pre = p; 

InThreading (p->rchild)

}

}

 

//中序遍历二叉树T,并将其中序线索化,Thrt指向头结点。

Status InOrderThreading (BiThrTree &Thrt; BiThrTree T){

if (!(Thrt = (BiThrTree) malloc (sizeof (BiThrNode)))) exit(OVERFLOW);

Thrt->LTag = Link;

Thrt->RTag = Thread;

Thrt->rchild = Thrt; //右指针回指。

if (!T) Thrt->lchild = Thrt;

else {

Thrt->lchild = T;

pre = Thrt;

InThreading (T); //线索化空链域,pre指向刚刚访问过的节点

pre->RTag = Thread;//线索化最后一个节点

pre->rchild = Thrt;

Thrt->rchild = pre;

}

return OK;

}

 

以上是二叉树中序线索化的过程算法。下面来看如何遍历中序线索二叉树。

//T指向头节点,头节点的左链lchild指向根节点

Status InOrderTraverse_Thr (BiThrTree T, void (*Visit) (TElemType e)){

BiThrTree p = T->lchild;

while (p != T){//空树或者遍历结束时,p==T

while (p->LTag == Link) p = p->lchild; //找到最左边的节点

Visit (p->data);

while (p->RTag == Thread && p->rchild != T){

p = p->rchild;

Visit (p->data);

}

p = p->rchild;

}

return OK;

}

 

最后给出二叉线索树的数据结构表示:

typedef enum PointTag {Link, Thread};

 

typedef struct BiThrNode{

TElemType data;

struct BiThrNode *lchild;

struct BiThrNode *rchild;

PointTag LTag;

PointTag RTag;

}BiThrNode, *BiThrTree;

 

 

 

 

 

 

 

 

你可能感兴趣的:(递归,二叉树遍历)