最近复习数据结构,重点看了下二叉树,这篇文章主要介绍下二叉树的遍历,包括中序遍历、前序遍历和后序遍历;同时利用二叉树的中序遍历和前序遍历或者后序遍历来重建一棵二叉树。
下面先给出对于某个节点的前序、中序、后序遍历的定义:
1、前序遍历:
先访问该节点,然后在递归遍历其左子树,然后再访问其右子树
2、中序遍历:
先递归遍历其左子树,然后访问该节点,然后再递归遍历其右子树
3、后序遍历:
先递归遍历其左子树,然后递归遍历其右子树,然后在访问该节点。
下面先说明如何实现二叉树的三种遍历,然后是如何利用遍历序列来重建二叉树。
分别给出递归和非递归的实现:
首先给出二叉树节点结构如下:
typedef struct node_t
{
char data;//数据域
struct node_t *lchild; //左孩子
struct node_t *rchild;//右孩子
}node, *tree;
二叉树的遍历:
1、前序遍历
a、前序遍历递归实现:
void preorder_traverse(tree root, void(*visit)(node))
{
if(NULL != root)
{
visit(*root); //先访问根节点
preorder_traverse(root->lchild, visit);//先序遍历左子树
preorder_traverse(root->rchild, visit);//前序遍历右子树
}
}
上面是前序遍历的递归实现,很直观,下面给出非递归实现:
b、前序变量的非递归实现:
在非递归实现里面用栈来模拟递归的过程,这里用了一个线性的数组来模拟栈的功能。
void preorder_traverse_norec(tree root, void(*visit(node)))
{
tree stack[32]; //这里用一个数组来模拟栈,假设节点不超过32个
int top = 0;
tree p = root;
for(; top<32; top++)
{
stack[top++] = NULL; //初始化栈
}
top = 0;
while(NULL != p || top > 0)
{
while(p != NULL)
{
visit(*p); //先访问根节点
stack[top++] = p;//把节点压入栈中
p = p->lchild;//将p指向其左孩子
}
p = stack[—top]; //弹出p,需要将NULL弹出
p = p->rchild; //p指向其右孩子
}
}
2、二叉树的中序遍历
a、二叉树中序遍历的递归实现:
中序遍历的递归实现也比较直观,代码如下:
void inorder_traverse(tree root, void(*visit)(node))
{
if(NULL != root)
{
inorder_traverse(root->lchild, visit);//先遍历其左子树
visit(*root);//遍历该节点
inorder_traverse(root->rchild, visit);//最后遍历右子树
}
}
b、中序遍历的非递归实现:
非递归实现同样用栈来模拟,这里还是使用线性数组来模拟栈的功能。
void inorder_traverse_norec(tree root, void(*visit)(node))
{
tree stack[32];
int top = 0;
tree p = root;
for(; top<32; top++)
{
stack[top] = NULL;
}
top = 0;
while(p != NULL || top > 0)
{
while(p != NULL)
{
stack[top++] = p; //将所有左子树压入栈中
p = p->lchild;
}
p = stack[—top]; //弹出空指针
visit(*p); //访问节点
p = p->rchild; //处理其右子树
}
}
3、后序遍历二叉树
a、后序遍历二叉树递归实现:
void postorder_traverse(tree root, void(*visit)(node))
{
if(NULL != root)
{
postorder_traverse(root->lchild, visit); //遍历其左子树
postorder_traverse(root->rchild, visit);//遍历其右子树
visit(*root);//访问节点
}
}
b、后序遍历二叉树的非递归实现:
void postorder_travese_norec(tree root, void(*visit)(node))
{
tree stack[32];
int top = 0;
tree p = root;
tree lastvisit = NULL; //用于标记上次访问的节点
for(; top<32; top++)
{
stack[top] = NULL;
}
top = 0;
while(p != NULL || top > 0)
{
while(p != NULL)
{
stack[top++] = p; //将所有左子树压入栈中
p = p->lchild;
}
p = stack[top - 1]; //注意这里,不是p = stack[—top],因为我们还不知道是否要访问p
if(p->rchild == NULL || lastvisit == p->rchild) //判断p的右子树是否访问过或者是否为空
{
visit(*p);//如果p的右子树为空或者已经访问过,则访问p
lastvisit = p;//标记上次访问的是节点p
top--; //这里p已经访问过,则将栈中的p弹出即可
p = NULL;
}
else
{
p = p->rchild; //如果p的右子树还没有访问过,那么访问其右子树
}
}
}
上面是后序遍历二叉树的非递归实现,与前序和中序的非递归略有不同,主要是要判断某个节点的右子树是否已经访问过,如果没有访问过,则访问该节点的右子树;如果访问过其右节点,则访问该节点。因此代码中利用了临时变量lastvisit来标记上次访问的节点。
上面就是二叉树的前序、中序、后序遍历的递归和非递归实现,递归思想是比较容易理解和实现的,非递归其实就是利用一个stack来保存递归过程中的信息。
那么有了二叉树的这三种遍历序列,我们就可以利用其遍历序列来重构一个二叉树了,当然并不是所有组合(供三种:前序和中序、前序和后序、中序和后序)都能用来重建二叉树,我们知道只有中序遍历参与的序列才可以重构二叉树,也就是前序和后序的遍历组合是无法唯一确定一棵二叉树的,这里不在进行证明。
下面介绍下利用前序和中序、中序和后序遍历序列来重构二叉树。
========================华丽的分割线=========================
有了二叉树的前序和中序或者中序和后序遍历序列就可以利用这些信息来重构一棵二叉树了,这是编程之美3.9里面的一个题目。下面给出具体的实现代码,这里只实现了递归的算法,非递归的还不会写 囧
1、利用前序序列和中序遍历序列重建二叉树
/**
*@brief 利用二叉树的前序和中序遍历序列重构一棵二叉树
*@para preorder:二叉树的前序遍历序列; inorder:二叉树的中序遍历序列
*@para tree_len: 二叉树元素的个数; root:用于保存构建的二叉树
*/
void rebuid_tree_inorder_preorder(char *preorder, char *inorder, int tree_len, tree *root)
{
if(NULL == preorder || NULL == inorder)
{
return;
}
char *temp_inorder = inorder; //用于遍历中序遍历的序列
int nleft = 0;//用于标记左子树的长度
int nright = 0;//用于标记右子树的长度
node temp = malloc(sizeof(node));
temp->data = *preorder;
temp->lchild = NULL;
temp->rchild = NULL;
if(*root == NULL)
{
*root = temp;
}
if(1 == tree_len)
{
return;
}
while(*temp_inorder != *preorder)//查找到左右子树的分界点
{
temp_inorder++;
}
nleft = temp_inorder – inorder;
nright = tree_len – nleft –1;
if(nleft > 0)//递归分析左子树
{
rebuild_tree_inorder_preorder(preorder+1, inorder, nleft, &((*root)->lchild));
}
if(nright > 0)//递归分析右子树
{
rebulid_tree_inorder_preorder(preorder+nleft+1, inorder+nleft+1, nright, &((*root)->rchild));
}
}
2、利用中序序列和后序遍历序列重建二叉树
上面是利用前序遍历和中序遍历的序列来重建二叉树,下面的实现是利用中序序列和后序遍历的序列来重建二叉树:
/**
*@brief 利用二叉树的中序和后序遍历序列重建二叉树
*@para postorder:二叉树后序遍历序列;inorder:二叉树中序遍历序列
*@para tree_len:二叉树节点数;root:用于保存新建的二叉树
*/
void rebuild_tree_postorder_inorder(char *postorder, char *inorder, int tree_len, tree *root)
{
if(NULL == postorder || NULL == inorder)
{
return;
}
char *temp_postorder = postorder + tree_len + 1; //由于是后序遍历,因此要从最后来判断
char *temp_inorder = inorder;
int nleft = 0;
int nright = 0;
node *temp = malloc(sizeof(node));
temp->data = *temp_order;
temp->lchild = NULL;
temp->rchild = NULL;
if(NULL == root)
{
*root = temp;
}
while(*temp_inorder != *temp_postorder)
{
temp_inorder++;
}
nleft = temp_inorder – inorder;
nright = tree_len – nleft –1;
if(nleft > 0)
{
rebuild_tree_postorder_inorder(postorder, inorder, nleft, &((*root)->lchild));
}
if(nright > 0)//需要注意的是重建右子树
{
rebuild_tree_postorder_inorder(postorder+nleft, inorder+nleft+1, nright, &((*root)->rchild));
}
}
上面是利用中序遍历和后序遍历的序列重建二叉树,需要注意的是重建右子树的操作。
对上面的两种方法用实例在验证下:
前序序列:abdcef
中序遍历:dbaecf
后序遍历:dbefca
a、对于前序和中序:
根据前序可以知道a是根节点;根据中序可以知道db是其左子树,ecf是其右子树,则根据这个再循环调用左右子树。此时传入的前序序列应该是bdcef了,而中序遍历对应左子树和右子树分别是db和ecf
然后递归下去。
b、对于中序和后序遍历:
根据后序遍历可以知道a是跟节点,因为他是最后一个遍历的;然后根据中序遍历就知道了左子树是db,右子树是ecf。然后再递归调用。需要注意的是对于后序遍历是从后向前进行判断的,只要注意到了这点基本就没问题了。
这篇文章总结并实现了二叉树的三种遍历:前序遍历、中序遍历和后序遍历,分别利用递归和非递归方式进行了实现。其中递归方式的比较直观和容易理解的,非递归中利用了栈来保存信息,这三种遍历的非递归实现中需要重点关注的是后序遍历的非递归实现。在实现了二叉树的三种遍历后,又实现了利用二叉树的遍历序列来重建二叉树的递归方法,这个问题是编程之美以及其他面试时经常会遇到的问题,值得好好学习研究。以上代码都经过测试验证,可能在敲的过程中会有问题,没有去比对了。