如何遍历一个二叉树——非递归实现

文章介绍了如何通过图形化思考来理解二叉树的遍历,特别是前序遍历的概念。作者强调了每个节点会被访问三次的重要性,并提供了使用两个栈(一个存储节点地址,一个记录访问状态)实现前序遍历的代码设计。在代码中,通过判断节点的访问状态来决定是访问节点、遍历左子树还是右子树,并在第三次访问时回溯。
摘要由CSDN通过智能技术生成

一、思路分析

思考如何遍历二叉树的时候,不能从扁平化的代码入手,要从图形入手,直观而易于理解的图形让编码者能够快速找到建构程序的方法。

那么看下面这个二叉树的示意图

计算机不可能直接去遍历这个图形,它不像我们人类一样可以一眼望去识别图像,它必须按照某种顺序阅读这个图形。我们给他一个小箭头,让它用这个小箭头遍历这个图形。小箭头指到哪个结点,计算机程序就可以读哪个结点的数据

如上图,现在箭头指向那个结点,计算机就可以读出这个结点的数据——38 。

如果对于每一个结点,我们让程序先读这个结点的数据,再读这个结点左子树的数据,再读这个结点右子树的数据,最终达到这个结点对应子树数据被全部遍历的效果。那么这种遍历方式被称为前序遍历。

好,如果用前序遍历的方式进行遍历,那么我们发现对于任何一个结点,小箭头会并且只会到达它三次,三次之后,小箭头再也不会访问这个结点了

比方说,储存着38这个数据的结点(下面称为结点38),小箭头一共会到达它三次。

第一次:小箭头从结点50移到结点38,这之后小箭头会跳向结点30

第二次:小箭头从结点30返回结点38,这之后小箭头会跳向结点40

第三次:小箭头从结点40返回结点38,这之后小箭头会返回结点50

需要说明的是,如果这个结点的左孩子指针为空,那我们就理解成小箭头跳到了空域上又跳了回来;如果这个结点的右孩子指针为空,也理解为小箭头跳到了空域又跳了回来。在这个前提下,我们可以认为,所有结点都会到达三次

这是一个非常重要的结论,我的代码架构就是根据这个结论展开的!

二、代码设计

结点是这样设计的,这一步乏善可陈。

typedef struct node
{
	int num;
	int count; 
	struct node* left;
    //左孩子指针
	struct node* right;
    //右孩子指针
	struct node* father;
    //指向父亲的指针
	
}Tree;

把一个递归问题转化为一个递归问题,大概率需要使用栈这一数据结构。我使用了两个栈,stack栈储存着从根结点到当前结点的所有地址,stack[0]即是根节点地址,以此类推。isread_stack栈储存着当前路径下各个结点的数据是否被遍历的信息,isread_stack[i]的值如果是1,就意味着这个结点小箭头已经到达过一次了,接下来小箭头就要按第二次到达的流程行动了;isread_stack[i]的值如果是2,就意味着这个结点已经到达过两次了,那么接下来小箭头要按第三次到达的流程行动。对于每一个结点,小箭头都会到达三次,设计一个数据结构去储存小箭头到达了几次是至关重要的!

Tree* stack[NUM];
int isread_stack[NUM];
int top=-1;

注意,这两栈用了一个栈顶指针,也就是说stack[i]是第i层的某个被访问结点的地址,而isread_stack[i]正好记录了它是第几次被访问。

其实你也可以开一个结构体数组来当栈,一次实现我这两个栈的功能。不过本质上,都是一样的!

现在我们可以写程序了,刚开始要把栈初始化,把根结点的地址和被访问状态(也就是被访问了几次了)信息压入两个栈中。这里以前序遍历代码为例:

void read_tree1(Tree* root)
{
	top=-1;
	stack[++top]=root;
	isread_stack[top]=0;
	//栈初始化,把根结点的地址和被访问状态信息压入两个栈中 
	//改写代码时,这两个栈一定要calloc或开全局变量,不然程序会崩溃!!! 
	
	Tree* tmp;
	//一个临时变量指针,在这里显得突兀,其实不重要,先别管它 
	
	while(1)
	{
		//如果是第一次到达 
		if (isread_stack[top]==0)
		{
			printf("num=%d count=%d\n",stack[top]->num,stack[top]->count);
			isread_stack[top]=1;
			
			//遍历左子树
		    if (stack[top]->left!=NULL)
		    {
			    tmp=stack[top]->left;
			    stack[++top]=tmp;
			    continue;
		    }
		}
		//如果是第二次到达 
		else if (isread_stack[top]==1)
		{
			isread_stack[top]=2;
			
			//遍历右子树
		    if (stack[top]->right!=NULL)
		    {
			    tmp=stack[top]->right;
			    stack[++top]=tmp;
				continue;
		    } 
		}
		//如果是第三次到达 
		else if (isread_stack[top]==2&&top!=0)
		{
			isread_stack[top]=0;
			top--;
		}
		//如果是第三次到达且这个结点是根结点
		//意味着树遍历完了,函数该结束了,退出循环吧 
		else if (isread_stack[top]==2&&top==0)
		{
			break;
		}
	}
}

如果是第一次到达某个结点,做三件事:

1、访问本结点的数据(因为我们以前序遍历为例子)

2、把该结点访问状态信息改为已经被访问过一次(也就是isread_stack[top]=1)

3、看看左孩子指针是不是非空,如果非空,进入左孩子进行访问,(把左孩子的地址压入栈中,然后continue);否则,跳过这一步,进入下一轮循环。

如果是第二次到达某个结点,做两件事:

1、把该结点访问状态信息改为已经被访问过两次(也就是isread_stack[top]=2)

2、看看左孩子指针是不是非空,如果非空,进入右孩子进行访问,(把右孩子的地址压入栈中,然后continue);否则,跳过这一步,进入下一轮循环。

如果是第三次到达某个结点,做两件事:

1、该结点的访问次数改为0(isread_stack[top]=0)。这看起来怪异,其实是因为要为下一次访问作准备。

2、top--

当然,如果回到的是根结点,退出就是了。

注意,如果你觉得第三步有点反人类,或许你可以这样设计:

第一次访问某结点,压入左孩子地址时,顺势把左孩子的访问次数改为0.我没试过,或许是可行。很大可能是可行的!

三、示例代码

#include <stdio.h>
#include <stdlib.h>
#define NUM 100

typedef struct node
{
	int num;
	int count; 
	struct node* left;
	struct node* right;
	struct node* father;
	
}Tree;

Tree* stack[NUM];
int isread_stack[NUM];
int top=-1;

void build_tree(Tree** proot,int num);
void read_tree1(Tree* root);

int main(void)
{
	int n;
	scanf("%d",&n);
	
	Tree* root;
	
	int i;
	int num;
	for (i=1;i<=n;i++)
	{
		scanf("%d",&num);
		build_tree(&root,num);
	}
	
	read_tree1(root);
	 
	return 0;
}

void build_tree(Tree** proot,int num)
{
	Tree* c;
	Tree* tmp;
	top=-1;
	
	if (*proot==NULL)
	{
		c=(Tree*)calloc(1,sizeof(Tree));
		c->left=NULL;
		c->right=NULL;
		c->num=num;
		c->count=1;
		*proot=c;
	}
	else
	{
		stack[++top]=*proot;
		while(1)
		{
			if (num==stack[top]->num)
			{
				(stack[top]->count)++;
				break;
			} 
			else if (num<stack[top]->num)
			{
				if (stack[top]->left==NULL)
				{
					c=(Tree*)calloc(1,sizeof(Tree));
		            c->left=NULL;
		            c->right=NULL;
		            c->num=num;
		            c->count=1;
		            stack[top]->left=c;
		            break;
				}
				else
				{
					tmp=stack[top]->left;
				    stack[++top]=tmp;
				    continue;
				}
			}
			else
			{
				if (stack[top]->right==NULL)
				{
					c=(Tree*)calloc(1,sizeof(Tree));
		            c->left=NULL;
		            c->right=NULL;
		            c->num=num;
		            c->count=1;
		            stack[top]->right=c;
		            break;
				}
				else
				{
					tmp=stack[top]->right;
				    stack[++top]=tmp;
				    continue;
				}
			}
	    }
	}
}

void read_tree1(Tree* root)
{
	top=-1;
	stack[++top]=root;
	isread_stack[top]=0;
	//栈初始化,把根结点的地址和被访问状态信息压入两个栈中 
	
	Tree* tmp;
	//一个临时变量指针,在这里显得突兀,其实不重要,先别管它 
	
	while(1)
	{
		//如果是第一次到达 
		if (isread_stack[top]==0)
		{
			printf("num=%d count=%d\n",stack[top]->num,stack[top]->count);
			isread_stack[top]=1;
			
			//遍历左子树
		    if (stack[top]->left!=NULL)
		    {
			    tmp=stack[top]->left;
			    stack[++top]=tmp;
			    continue;
		    }
		}
		//如果是第二次到达 
		else if (isread_stack[top]==1)
		{
			isread_stack[top]=2;
			
			//遍历右子树
		    if (stack[top]->right!=NULL)
		    {
			    tmp=stack[top]->right;
			    stack[++top]=tmp;
				continue;
		    } 
		}
		//如果是第三次到达 
		else if (isread_stack[top]==2&&top!=0)
		{
			isread_stack[top]=0;
			top--;
		}
		//如果是第三次到达且这个结点是根结点
		//意味着树遍历完了,函数该结束了,退出循环吧 
		else if (isread_stack[top]==2&&top==0)
		{
			break;
		}
	}
}

这里的build_tree是一个构建二叉树的函数,我就不赘述了。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值