二叉树介绍
二叉树是一类重要的数据结构。二叉树常被用于实现二叉查找树和二叉堆。通常子树被称作“左子树”(left
subtree)和“右子树”(right
subtree)。
一种二叉树结点定义:
struct bit_node
{
chardata;
structbit_node *lchild,*rchild;
};
遍历是对树的一种最基本的运算,所谓遍历二叉树,就是按一定的规则和顺序走遍二叉树的所有结点,使每一个结点都被访问一次,而且只被访问一次。由于二叉树是非线性结构,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。
设L、D、R分别表示遍历左子树、访问根结点和遍历右子树,则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。
递归方法遍历:
二叉树本身是一种递归定义的数据结构,所以递归遍历的方法是简单明了的方法。
DLR(先根次序遍历)
void traverse_bitree_dlr(struct bit_node*node)
{
if(node!= NULL)
{
printf("%3c",node->data);
traverse_bitree_dlr(node->lchild);
traverse_bitree_dlr(node->rchild);
}
}
LDR(中根次序遍历)
void traverse_bitree_ldr(struct bit_node*node)
{
if(node!= NULL)
{
traverse_bitree_ldr(node->lchild);
printf("%3c",node->data);
traverse_bitree_ldr(node->rchild);
}
}
LRD(后根次序遍历)
void traverse_bitree_lrd(struct bit_node*node)
{
if(node!= NULL)
{
traverse_bitree_lrd(node->lchild);
traverse_bitree_lrd(node->rchild);
printf("%3c",node->data);
}
}
使用递归遍历的优点是算法简单明了,缺点也十分明显:对于栈的消耗比较大。尤其是在嵌入式应用中,嵌入式处理器资源往往有限。每次递归调用,都会涉及到通用寄存器、SP指针、PC指针等的压栈。当树的深度比较大时,对于栈的消耗会变得非常严重,很有可能造成栈的溢出。
非递归方法遍历:
因此,二叉树的非递归遍历方法就显得非常有实际应用价值。下面是非递归遍历的算法,这里使用了数据结构栈,利用其先进后出的特点,用结点入栈出栈过程手工模拟递归调用过程中的栈操作。
前序遍历
思想:使用数据结构栈,1保存结点左侧分支上的所有结点,并遍历结点;2出站时,切换至右子树;3重复1、2。
void traverse_bitree_dlr_nr(struct bit_node*root)
{
structbit_node *p;
structbit_node *stack[100];
inti = -1;
p= root;
while(p!= NULL || i != -1)
{
/*从当前结点开始搜索左子树,直至为空,并将分支上的左子树入栈*/
while(p!= NULL)
{
printf("%3c",p->data); /*先入根结点再入左子树,入栈前遍历,前序*/
stack[++i]= p;
p= p->lchild;
}
if(i!= -1)
{
p= stack[i--]; /*取出栈顶结点*/
p= p->rchild; /*切换至右子树*/
}
}
}
中序遍历
思想:使用数据结构栈,1保存结点左侧分支上的所有结点;2出站时遍历结点,切换至右子树;3重复1、2。
相对于前序遍历,中序遍历要求只是将左子树和根结点的顺序对调了一下,这不正符合栈的特点吗?!后进先出本身就是一个顺序反转过程。入栈时遍历,就是前序,出栈时遍历不就是中序遍历吗?!
void traverse_bitree_ldr_nr(struct bit_node*root)
{
structbit_node *p;
structbit_node *stack[100];
inti = -1;
p= root;
while(p!= NULL || i != -1)
{
/*从当前结点开始搜索左子树,直至为空,并将分支上的左子树入栈*/
while(p!= NULL)
{
stack[++i]= p;
p= p->lchild;
}
if(i!= -1)
{
p= stack[i--]; /*取出栈顶结点*/
printf("%3c",p->data); /*先出左子树,再出根结点,出栈后遍历,中序*/
p= p->rchild; /*切换至右子树*/
}
}
}
后序遍历
后序遍历是三种遍历中最复杂的。
第一种思想:
和前序、中序思想一样,使用栈手工模拟递归调用过程中栈操作过程。
后序遍历要求根结点在左右子树均遍历完成后才能遍历,更确切的条件就是,根结点要在其右子树遍历完成后紧接着遍历。这样就需要保存上一次遍历的结点信息。再者,栈中保存了未遍历的树的全部信息。后序遍历要求:先左子树,再右子树最后根结点,所以在右子树没有遍历完之前,根结点不能出栈。这一点其实也是递归后序遍历中所存在的过程。在具体代码中,相对于前序和中序又存在一个:右子树没有遍历时,结点需再次入栈的过程
出栈时如果满足为叶子节点,或者该结点的右孩子已经遍历,则遍历。
具体代码如下:
void traverse_bitree_lrd_nr(struct bit_node*root)
{
structbit_node *cur, *pre;
structbit_node *stack[100];
inti = -1;
cur= root;
pre= NULL;
while(cur!= NULL || i != -1)
{
/*从当前结点开始搜索左子树,直至为空,并将分支上的左子树入栈*/
while(cur!= NULL)
{
stack[++i]= cur;
cur= cur->lchild;
}
if(i!= -1)
{
cur= stack[i--]; /*取出栈顶结点*/
if(cur->rchild== NULL) /*当前结点不存在右子树*/
{
pre= cur; /*记录本次遍历的结点*/
printf("%3c",cur->data);
cur= NULL; /*该结点已经遍历,为下一次从栈中取数据做准备*/
}
else /*当前结点存在右子树*/
{
if(cur->rchild == pre) /*如果该结点的右子树已经遍历,则遍历该结点*/
{
pre= cur; /*记录本次遍历的结点*/
printf("%3c",cur->data);
cur= NULL; /*该结点已经遍历,为下一次从栈中取数据做准备*/
}
else /*如果该结点的右子树没有遍历,则结点入栈,并切换至右子树*/
{
stack[++i]= cur;
cur= cur->rchild;
}
}
}
}
}
第二种思想:
完全利用栈的先进后出的特点,对于任意结点,先入栈右子树,再入栈左子树。出栈时如果满足为叶子节点,该结点的左孩子(只有左孩子)刚刚遍历过或者右孩子刚刚遍历,则遍历。这样就保证了遍历时先左子树,再右子树,最后根结点的顺序。
void traverse_bitree_lrd_nr_second(structbit_node *root)
{
structbit_node *cur, *pre = NULL;
structbit_node *stack[100];
inti = -1;
stack[++i]= root;
while(i != -1)
{
cur= stack[i];
if((cur->lchild == NULL && cur->rchild == NULL) || (pre != NULL&& (pre == cur->lchild || pre == cur->rchild)))
{
printf("%3c",cur->data);
i--;
pre= cur;
}
else
{
if(cur->rchild != NULL)
{
stack[++i]= cur->rchild;
}
if(cur->lchild != NULL)
{
stack[++i]= cur->lchild;
}
}
}
}