二叉树遍历非递归算法

二叉树的遍历

•三种遍历

​ • 先序遍历: 根节点–>左子树–>右子树

​ • 中序遍历: 左子树–>根节点–>右子树

​ • 后序遍历: 左子树–>右子树–>根节点

•两类算法

​ • 递归算法(具体看我上一篇文章)

​ ♥直观,易读

​ ♥效率低下

​ • 非递归算法

​ ♥ 如果一个算法可以使用递归或循环来进行完成,在不影响代码阅读的情况下,可以使用循环,来降低复杂度。

下面介绍二叉树的遍历,把递归遍历循环遍历代替:

一、先序遍历非递归算法

算法构思:

我们既然要先序遍历二叉树,就要先了解如何先序遍历二叉树

image-20221206175545467

• 先序遍历:

​ 根节点–>左子树–>右子树 : A BDG CEF

▪ 先序遍历二叉树的过程

​ •访问根节点

​ • 然后先序遍历左子树

​ • 最后先序遍历右子树

构建思路 :

先遍历根节点 , 然后再遍历其左子树和右子树,这是一个层级,把其左子树或右子树再次当做一个独立的树去调用(每个节点被传入调用的时候,都当了一次根节点,所以才有机会被输出),目的就是输出访问一遍.

从先序遍历的递归算法得出循环算法的思路:

通过观察先序遍历,我们先遍历输出根节点, 在这一个层级内,我们接下来是去遍历处理左子树, 处理完之后,再去遍历处理右子树。递归调用,当然容易理解,但是此时要去用循环来按照递归的顺序方法输出,并且按照循环的方式来进行。

image-20221206175545467

我们现在去用人脑来进行遍历,先遍历 A , 输出A之后,此时当时我们当然要去处理左子树,但是后续我们怎么去处理右子树呢?

所以我们需要做标记,怎么做标记呢?当然是先将根A的右孩子 C 入栈,以便后续处理完左子树之后,再出栈右孩子,进而处理右子树。

根据栈的特性,先进后出, 后进先出 , 我们处理完根之后, 要先处理左子树,但是后续我们还要处理右子树,为了避免找不到右子树的信息,所以根节点处理完后,先把右孩子入栈,

然后再把左孩子入栈。此时栈顶是根节点的左孩子,所以我们可以先处理左孩子了,根据先序遍历的规则,我们再结合递归的思想,用循环代替的话,栈顶的节点也可以看做一棵子树的根,我们就可以出栈此节点,然后再接着遍历其孩子,每一个层级都是有先后顺序的。等左子树处理完,栈里就剩一个根节点的右孩子,我们就可以将其出栈,并处理右子树了。就这样环环相扣,循环的好处是,省去了递归每次保存临时变量和其他现场的步骤,一定程度上提高了效率。

下面进行框架构建:

♥算法步骤:

if(当前b树不空)
{
    根节点b进栈;
    while(栈不空)
    {
        出栈节点p并访问之;*p节点有右孩子,将其右孩子进栈;*p节点有左孩子,将其左孩子进栈;        
    }
}    

代码实操:

//传入二叉树
void PreOrder1(BTNode *b)
{
    //定义存储数组栈,用来调剂各个节点的访问顺序
    BTNode *St[MaxSize];
	//定义指针p来进行对节点进行操作和访问
    BTNode *p;
    //栈顶初始为-1
    int top=-1;
    //先让根节点入栈,栈顶累加
    top++;
    St[top] =b ;
    //接下来就是为了先序遍历二叉树循环处理二叉树了
	//栈不为空时循环
    while(top>-1)
    {
        //根节点已经入栈,我们先处理根节点,所以退栈并访问之
        p = St[top];
        top--;
        printf("%c",p->data);
        //接下来就是处理本层级的左子树和右子树了
        //由于是先序遍历,所以先处理完左子树,再处理右子树
        //根据栈的特性,先进后出,所以右子树后处理,所以先把根节点的右孩子入栈
        if(p->rchild!=NULL)
        {
            top++;
            St[top]=p->rchild;
        }            
        //下面我们就可以遍历左子树了
        //根据栈的后进先出特性,此时如果存在左孩子,就将其入栈
        //当接下来循环回去的时候,我们就可以再次此层级的左孩子当做左子树的根,进行访问
        if(p->lchild!=NULL)
        {
            top++;
            St[top]=p->lchild;
        }    
    }        
}    

二、中序遍历(左-根-右)非递归算法

中序遍历二叉树的过程

​ • 中序遍历左子树

​ • 访问根节点

​ • 中序遍历右子树

构建思路:

我们要用循环来代替递归,所以我们就要深入进入,看如何去中序遍历二叉树了.

image-20221206175545467

我们既然要左-根-右去访问二叉树,所以就需要先处理左子树,但是我们能直接处理左子树的根节点吗?当然是不能的,我们根据递归的思想,左子树依然是一棵树,当其存在左孩子的时候,我们能坐视不管,直接访问根节点吗?当然是不能的,所以我们需要顺着根节点的左孩子这一条枝,一直走,直到走到有一个节点,其没有左孩子了,我们此时才能将其作为我们的第一个遍历的节点.

我们可以去思考,此节点在同一层次是属于根或左或右节点呢? 其没有左孩子了,我们才将其作为第一个节点来访问,说明当一个节点没有左孩子的时候,我们不得不去访问此层次的根节点了,根据先序遍历,访问完根节点,接下来是访问其右孩子。如果此时其右孩子没有子孙了,才能断定此层已经遍历完,下面该访问上一层级的根节点了(因为此时我们这一层级处理完之后,根据先序遍历的递归处理思想)我们相当于是处理完了上一级的根节点的左子树,接下来才该处理上一层级的根节点,然后是右节点,以此类推。

根据以上思路,构建规范框架:

(1)所有左下孩子进栈,体现先访问左子树的特点。

(2)当所有左下孩子进栈后,栈顶节点p没有左孩子(即没有左子树)或者其左子树均已访问,所以可以访问p节点.

(3)当访问p节点后,转向其右孩子,采用同样的方式中序遍历右子树.

(4)我们左孩子先入栈是为了后续按照层次进行访问, 当其左子树处理完之后,才可以处理栈顶元素

代码框架:

if(当前b树不空)
{
    p=b;
    while(栈不空或者p!=NULL)
    {
        while(p有左孩子)
        {
            将p进栈;
            p=p->lchild;
        }    
        if(栈不空)
        {
            出栈p并访问之;
            p=p->rchild;
        }
    }
}    

代码实操:

//传入要遍历的二叉树
void InOrder(BTNode *b)
{
    //因为要分层次进行处理,需要用到数组栈结构
    BTNode *St[MaxSize];
    //定义操作节点的指针
    BTNode *p;
    //初始化栈
    int top=-1;
    //先传入树根,树根是联系此二叉树的根
    p = b;
    //下面开始进行中序遍历
    //当栈里不空,或者传入的树不空,那就接着进行遍历
    while(top>-1 || p!=NULL)
    {
        //上面思路讲的很清楚,要分层次遍历,中序遍历是左-根-右,所以直到节点没有左孩子
        //我们才能访问此节点,因为根据递归的思想,在同一层次,是左-根-右,此时没有左孩子,才能访问根
        //我们按层次,先将根的左孩子依次入栈,当一层一层处理,当处理完本层,也相当于处理完了上层的左子树,才能处理上一层的根
        while(p!=NULL)            
        {
            //将所有左下孩子进栈,直到遇到没有左孩子的节点
            top++;
            St[top]=p;
            p = p->lchild;
        }
        //此时上一层循环跳出,p此时指向的那个节点没有左孩子
        //所以此时应该出栈的节点就是栈顶节点
        //如果此时栈里还有节点,那我们就出栈
        if(top>-1)
        {
            p = St[top];
            top--;
            printf("%c",p->data);
            //出栈完此层的根节点,根据中序遍历规则,我们当然是去处理其右孩子
            p = p->rchild;
        }            
        //如果此节点只有一个右孩子,右孩子没有子孙
        //循环上去的时候,我们把其右孩子入栈,然后判断出右孩子没有左孩子,就跳出循环,
        //然后就访问此节点,再发现其没右孩子,就说明这一层已经完成,也说明上一层级的左子树访问完了        
    }        
}    

三、后序遍历(左-右-根)非递归算法

后续遍历二叉树的过程:

先访问左子树,再访问右子树,最后处理根

image-20221209141453332

构建思路:

我们依然是先遍历左孩子,此时我们传入树b ,然后我们此时先遇到是是A, 此时A是树根,不能先处理,因为其左子树还未处理,所以先将A入栈,接着B也入栈,D入栈,此时判断到D没有左孩子,因为我们遍历的顺序是左-右-根, 此时其没有左孩子,所以在这一层级内, 此时D作为此层级的根, 需要先访问其右孩子G , 然后再访问此层级的根节点D, 此时我们的逻辑是没有问题的 .

但是我们会注意到, 我们访问了两次D ,

• 一次是第一次访问到D ,D由于是作为根节点,所以需先访问其右孩子G

• 第二次是访问完G之后, 需访问此层级的根节点D了

但是我们如何判断我们是第一次访问到D ,还是第二次访问到D ,这两次的操作是不同的,所以我们需要从中提取规范性或标志性的信息,从而去让代码去做对应的操作.

难点:

▪ 如何判断一个节点* b 的右孩子节点已访问过

条件:

▪ 在后续遍历中,*b的右孩子节点一定刚好在 *b之前访问.

方法:

▪ 用p保存刚刚访问过的节点(初始值为NULL);

▪ 若 b->rchild == p 成立,说明这是第二次访问到*b, 说明 *b的左右树均已访问,现应访问 *b.

代码框架:

if(当前b树不空)
{
	do
    {
    	while(b!=NULL,b有左孩子,将进栈)
		出栈节点b(出栈不代表访问,出栈是要操作以此节点为根的层级了);
        if(b的右子树已访问)
            则访问b并退栈
		else
            b = b->rchild;
    }while(栈不空);        
}    

代码实操:

//传入要后序遍历的二叉树
void PostOrder1(BTNode *b)
{
	//为了每一层级有序遍历,定义数组栈,进行记录每层的顺次
    BTNode *St[MaxSize];
    //处理节点的指针
    BTNode *p;
    //栈初始化
    int top=-1;
    //注意:此时进行的是左-右-根,此时不能将树根入栈
    //当传入的树不为空,则进行遍历
    if(b!=NULL)
	do
    {
    	//将根节点的所有左节点进栈,方便后续进行层次遍历
        while(b!=NULL)
        {
            top++;
            St[top]=b;
            b=b->lchld;
        }            
        //此时*b指向的节点没有左孩子的时候,说明就该处理此时以*b为根的层级的右孩子了
        //在此层级内,我们此时还未访问遍历节点,所以此时标志已访问的上一个节点的 p置空
        p = NULL;
/**        _____________
        注意:此时引入的p,我们要知道p的作用是什么?是标志此层级,右孩子是否已经访问过
        所以,如果此层级一旦结束,那么p就要置空了,我们需要在每次第一次访问同一层级的根节点前
        将p置空,以免影响不同层级的判断,所以我们就要保证我们在结束一个层次后,进行跳出置空p
**/        
        //经过上面循环的过滤,说明此时b所指向的左子树为空,或者已经处理完了
		flag = 1; //表示*b的左子树已经访问或空,下面该处理此层级的右子树了
		//当栈里不为空或左孩子已经处理完,我们就继续处理此时*b为根节点的右孩子了
        while(top!=-1 && flag==1)
        {
            //栈顶元素即为*b
            b=St[top];
            //下面开始处理*b节点,分两种情况
		   //此时以*b为根的层级,左孩子为空或已经处理完了,该处理右孩子了
            //但是我们需要判断这是已经处理完右孩子或下一步该处理右孩子了
            if(b->rchild == p)            //如果刚才遍历的就是*b的右孩子,说明该遍历根*b了
            {
                //我们此次访问*p,可以理解成上一步已经处理完右孩子了,这次该处理此层级的根了
                //同时我们也可以理解成, 什么时候,需要强制输出一个数
			   //在同一层级遍历,当一个节点,没有左孩子,没有有孩子了,当然要强制输出此层级的根节点了
                //所以第一次输出的必然是,在递归思想内,被单独当做根的节点被输出,我们在跳出或初始化的时候,已经将上一个访问节点置空
                //并且当我们进行回溯输出的时候,不会跳出循环的,因为右孩子是没有子孙的,上一次访问也是空,正好符合输出条件。
                pirntf("%c",b->data);
                top--;
                p = b;
            }   
            //或者是第一次访问b,该处理右孩子了
            else
            {
                //此时作为单独的右孩子,或许应该被输出,但是如果右孩子是右子树呢?
                //我们就需要将其重新看成一个子树进行处理对待,进行分层遍历,所以我们需要跳出此循环
                //我们可以定义一个标志flag, 表明我们此层级已经完成了,需要进行下一层的进栈和访问了
                b = b->rchild;
                //标志跳出循环,该处理以b为根的层级的左子树了
                flag = 0;
            }                
        }            
    }while(top!=-1);        
}  
/*
	最好能走一遍,这样更能理解递归思想,用循环实现递归,牺牲了代码的可读性
/*    

四、例子:路径之逆

♥ 问题:

二叉树采用二叉链存储结构,设计算法输出从根节点到每个叶子节点的路径之逆

解:

采用后续遍历非递归算法,遍历到叶子节点的时候,恰好符合从叶子结点到根节点。

试想一下,后序遍历是左-右-根,

回想后序遍历,当我们不得不输出访问节点时,我们访问的顺序数左-右-根,不得不输出的时候是左右节点都是空,只有根,所以在这一个层次中,我们只能输出此层次的根,此时输出的是二叉树的叶子.

所以我们只需要通过后序遍历的方法,在访问节点的时候,加一个判断是否是叶子节点的判断即可。

代码构思:

void AllPath(BTNode *b)
{
	BTNode *St[MaxSize];
	BTNode *p;
	int flag,i,top=-1;
	if(b!=NULL)
	{
		do
		{
			//将*b所有左节点进栈
			while(b!=NULL)
			{
				top++;
				St[top]=b;
				b=b->lchild;
			}
			p=NULL;
			flag=1;
			//当栈非空处理每一个叶子节点
			while(top!=-1&&flag)
			{
				b = St[top];
				//加判断
					if(b->rchild == p)
					{
						if(b->lchild == NULL && b->rchild == NULL)
						{
							//若叶子,输出栈中所有节点值
							for(i=top;i>0;i--)
							{
								printf("%c->"St[i]->data);
							}
							printf("%c\n",St[0]->data);
						}
						top--;
						p=b;
					}
					else
					{
						b = b->rchild;
						flag = 0;
					}
				
			}
		}
		while(top!=-1);
		printf("\n");
	}
}

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐