重构
二叉树的重构是指在已知二叉树的先序遍历,中序遍历,后序遍历中的任意两者的情况下,恢复二叉树的结构。
如果已知该二叉树为真二叉树,则即使在不知道其中序排列(即知道先序排列、后序排列)的情况下,也能唯一确定二叉树的结构。否则,必须要有其中序排列才能唯一确定二叉树的结构。
三种排列的定义
前序排列的结构为根结点(对于子树,就是父结点)、左子树、右子树。对于其中的左子树和右子树,也可以类似递归地地划分。
中序排列的结构为左子树、根结点(对于子树,就是父结点)、右子树。对于其中的左子树和右子树,也可以类似递归地地划分。
后序排列的结构为左子树、右子树、根结点(对于子树,就是父结点)。对于其中的左子树和右子树,也可以类似递归地地划分。
以下讨论在给出中序排列的情况下,如何确定二叉树结构。
1.前序排列+中序排列
以下是结点结构体和函数声明
struct node//node表示树的结点
{
char data;//data意为数据域
struct node *left_child;//left_child意为左子树的根结点
struct node *right_child;//right_child意为右子树的根结点
};
//preorder为当前子树的先序排列首元素的地址,inorder为当前子树的中序排列首元素的地址
//length为当前子树的先序排列的长度,parent_node为当前要插入的结点的双亲结点的地址
struct node *reconstruct_binary_tree1(char *preorder,char *inorder,int length,struct node *parent_node,int n)//重构二叉树
- 找到子树的父结点,先给一个新的结构体分配内存,将父结点所代表的元素存入结构体的数据域。
//将根结点录入二叉链表
struct node *cur_inser_node=(struct node *)malloc(sizeof(struct node));//先给要插入二叉树的结点分配内存空间
(cur_inser_node)->data=*preorder;//将父结点所代表的元素存入结构体的数据域
(cur_inser_node)->left_child=NULL;
(cur_inser_node)->right_child=NULL;
- 将父结点与它的双亲结点相连。
if(n==0)//n=0说明此时插入的结点是二叉树的树根(也就是整棵二叉树的根结点),它是没有双亲结点的
{
parent_node=cur_inser_node;//之所以要进行这一步赋值是防止在下下一个if判断中出现对值为0的指针的访问
}
if(parent_node->left_child==NULL||parent_node->right_child==NULL)//若当前要插入的结点的双亲结点的地址为空
{
if(n==1)//n=1说明正在重构parent_node的左子树
{
parent_node->left_child=cur_inser_node;//让双亲结点向左指向当前要插入的父结点
}
else if(n==2)//n=2说明正在重构parent_node的右子树
{
parent_node->right_child=cur_inser_node;//让双亲结点向右指向当前要插入的父结点
}
}
- 找到根结点在中序排列中的位置并记录下来。
char *root_node=preorder;//当前子树的前序排列首元素即为当前子树的父结点
char *border=inorder;//border为中间变量指针,接下来会用它来记录当前子树的根结点在中序排列中的位置,从而得到当前子树的左子树和右子树
while(*border!=*root_node)//寻找当前子树的根结点在中序排列中的位置
{
border++;
}//在跳出while循环后,border即为中序排列中根结点元素的地址
- 中序排列剩下的两部分分别为根结点的左子树和右子树,即得到根结点的左子树和右子树的中序排列。根据这两个中序排列又可以在先序排列中分离出左子树和右子树的先序排列。至此,已得到两棵子树的先序排列和中序排列。
- 根结点的两棵子树也是二叉树。所以对第2步中得到的两棵子树递归地执行1、2步。即不断对新找到的子树执行1、2步直到所有结点都被插入二叉链表。
left_length=(int)(border-inorder);//left_length为当前结点的左子树的先序排列的长度
right_length=(int)(length-left_length-1);//左子树和右子树先序排列的长度之和为length-1,因为在此之前已经往二叉链表中插入了一个结点
if(left_length>0)//重构左子树
{
reconstruct_binary_tree1(preorder+1,inorder,left_length,cur_inser_node,1);
}
if(right_length>0)//重构右子树
{
reconstruct_binary_tree1(preorder+1+left_length,inorder+left_length+1,right_length,cur_inser_node,2);
}
-
根结点的左子树的结构为
根结点的右子树的结构为
-
该二叉树的结构为
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct node//node表示树的结点
{
char data;//data意为数据域
struct node *left_child;//left_child意为左子树的根结点
struct node *right_child;//right_child意为右子树的根结点
};
//preorder为当前子树的先序排列首元素的地址,inorder为当前子树的中序排列首元素的地址
//length为当前子树的先序排列的长度,parent_node为当前要插入的结点的双亲结点的地址
struct node *reconstruct_binary_tree1(char *preorder,char *inorder,int length,struct node *parent_node,int n)
{
int left_length=0,right_length=0;//left_length为当前根结点左子树的先序排列的长度,right_length为当前根结点左子树的先序排列的长度
struct node *cur_inser_node=(struct node *)malloc(sizeof(struct node));//先给要插入二叉树的结点分配内存空间
char *root_node=preorder;//当前子树的前序排列首元素即为当前子树的父结点
char *border=inorder;//border为中间变量指针,接下来会用它来记录当前子树的根结点在中序排列中的位置,从而得到当前子树的左子树和右子树
(cur_inser_node)->data=*preorder;//保存当前子树的根结点(这里的根结点不一定指树根)
(cur_inser_node)->left_child=NULL;
(cur_inser_node)->right_child=NULL;
if(n==0)//n=0说明此时插入的结点是二叉树的树根(也就是整棵二叉树的根结点),它是没有双亲结点的
{
parent_node=cur_inser_node;//之所以要进行这一步赋值是防止在下一个if判断中出现对值为0的指针的访问
}
if(parent_node->left_child==NULL||parent_node->right_child==NULL)//若当前要插入的结点的双亲结点的地址为空
{
if(n==1)//n=1说明正在重构parent_node的左子树
{
parent_node->left_child=cur_inser_node;//让双亲结点指向当前要插入的结点
}
else if(n==2)//n=2说明正在重构parent_node的右子树
{
parent_node->right_child=cur_inser_node;//让双亲结点指向当前要插入的结点
}
}
if(length==1)//若前子树的先序排列的长度为1,说明当前要插入的结点没有左右子树,此时不需要再继续重构
{
return cur_inser_node;
}
while(*border!=*root_node)//寻找当前子树的根结点在中序排列中的位置
{
border++;
}
left_length=(int)(border-inorder);//left_length为当前结点的左子树的先序排列的长度
right_length=(int)(length-left_length-1);//左子树和右子树先序排列的长度之和为length-1,因为在此之前已经往二叉链表中插入了一个结点
if(left_length>0)//重构左子树
{
reconstruct_binary_tree1(preorder+1,inorder,left_length,cur_inser_node,1);
}
if(right_length>0)//重构右子树
{
reconstruct_binary_tree1(preorder+1+left_length,inorder+left_length+1,right_length,cur_inser_node,2);
}
return cur_inser_node;
}
int main()
{ //preorder为先序序列,inorder为中序序列,postorder为后序序列
char *preorder_sequence="ABDHIEJCFG";
char *inorder_sequence="HDIBJEAFCG";
char *postorder_sequence="HIDJEBFGCA";
int length=strlen(inorder_sequence);
struct node *root_node=NULL;//这里的root_node是指整棵二叉树的树根
//已先序排列和中序排列重构二叉树
root_node=reconstruct_binary_tree1(preorder_sequence,inorder_sequence,length,root_node,0);
return 0;
}
函数的参数计算原理
if(left_length>0)//重构左子树
{
reconstruct_binary_tree1(preorder+1,inorder,left_length,cur_inser_node,1);
}
if(right_length>0)//重构右子树
{
reconstruct_binary_tree1(preorder+1+left_length,inorder+left_length+1,right_length,cur_inser_node,2);
}
有读者可能会问:为什么在重构父结点的左子树时,函数传递的前两个参数是preorder+1、inorder?
我们知道,函数reconstruct_binary_tree1第一个参数的意义是当前子树的先序排列中首元素的地址,
//preorder为当前子树的先序排列首元素的地址,inorder为当前子树的中序排列首元素的地址
//length为当前子树的先序排列的长度,parent_node为当前要插入的结点的双亲结点的地址
struct node *reconstruct_binary_tree1(char *preorder,char *inorder,int length,struct node *parent_node,int n)
而先序排列中首元素即为当前子树的父结点。在此之前,我们已将该父结点(它的地址即为preorder)嵌入了二叉树。根据前序排列的结构,可知preorder的下一位元素就是左子树的父结点,所以重构左子树时,传入的第一个参数变成了preorder+1。
同理也可推得为什么在重构右子树时,传入函数的第一个参数变成了preorder+1+left_length。
2.中序排列+后序排列
原理和1类似,只是在函数传递的参数略有变化。在此不做细致分析,直接给出代码。
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct node//node表示树的结点
{
char data;//data意为数据域
struct node *left_child;//left_child意为左子树的根结点
struct node *right_child;//right_child意为右子树的根结点
};
//postorder为当前子树的后序排列末尾元素的地址,inorder为当前子树的中序排列首元素的地址
//length为当前子树的后序排列的长度,parent_node为当前要插入的结点的双亲结点的地址
struct node *reconstruct_binary_tree2(char *postorder,char *inorder,int length,struct node *parent_node,int n)
{
int left_length=0,right_length=0;//left_length为当前根结点左子树的先序排列的长度,right_length为当前根结点左子树的先序排列的长度
struct node *cur_inser_node=(struct node *)malloc(sizeof(struct node));//先给要插入二叉树的结点分配内存空间
char *root_node=postorder;//当前子树的前序排列的后序排列的末尾元素即为当前子树的父结点
char *border=inorder;//border为中间变量指针,接下来会用它来记录当前子树的根结点在中序排列中的位置,从而得到当前父结点的左子树和右子树
(cur_inser_node)->data=*postorder;//保存当前子树的父结点
(cur_inser_node)->left_child=NULL;
(cur_inser_node)->right_child=NULL;
if(n==0)//n=0说明此时插入的结点是二叉树的树根(也就是整棵二叉树的根结点),它是没有双亲结点的
{
parent_node=cur_inser_node;//之所以要进行这一步赋值是防止在下一个if判断中出现对值为0的指针的访问
}
if(parent_node->left_child==NULL||parent_node->right_child==NULL)//若当前要插入的结点的双亲结点的地址为空
{
if(n==1)//n=1说明正在重构parent_node的左子树
{
parent_node->left_child=cur_inser_node;//让双亲结点指向当前要插入的结点
}
else if(n==2)//n=2说明正在重构parent_node的右子树
{
parent_node->right_child=cur_inser_node;//让双亲结点指向当前要插入的结点
}
}
if(length==1)//若前子树的先序排列的长度为1,说明当前要插入的结点没有左右子树,此时不需要再继续重构
{
return cur_inser_node;
}
while(*border!=*root_node)//寻找当前子树的根结点在中序排列中的位置
{
border++;
}
left_length=(int)(border-inorder);//left_length为当前结点的左子树的先序排列的长度
right_length=(int)(length-left_length-1);//左子树和右子树先序排列的长度之和为length-1,因为在此之前已经往二叉树中插入了一个结点
if(left_length>0)//重构左子树
{
reconstruct_binary_tree2(postorder-1-right_length,inorder,left_length,cur_inser_node,1);
}
if(right_length>0)//重构右子树
{
reconstruct_binary_tree2(postorder-1,inorder+left_length+1,right_length,cur_inser_node,2);
}
return cur_inser_node;
}
int main()
{ //preorder为先序序列,inorder为中序序列,postorder为后序序列
char *preorder_sequence="ABDHIEJCFG";
char *inorder_sequence="HDIBJEAFCG";
char *postorder_sequence="HIDJEBFGCA";
int length=strlen(inorder_sequence);
struct node *root_node=NULL;//这里的root_node是指整棵二叉树的树根
//已中序排列和后序排列重构二叉树
root_node=reconstruct_binary_tree2(postorder_sequence+length-1,inorder_sequence,length,root_node,0);
return 0;
}
遍历
遍历(Traversal)是指沿着某条搜索路线,依次对树中每个结点均做一次且仅做一次访问。
遍历是二叉树上最重要的运算之一,是二叉树上进行其它运算的基础。
根据访问根结点的操作所发生的位置可将二叉树的遍历分为三类:
① NLR:前序遍历(Preorder Traversal 亦称(先序遍历))
——访问根结点的操作发生在遍历其左右子树之前。
② LNR:中序遍历(Inorder Traversal)
——访问根结点的操作发生在遍历其左右子树之中(间)。
③ LRN:后序遍历(Postorder Traversal)
——访问根结点的操作发生在遍历其左右子树之后。
通过三种遍历顺序遍历二叉树后分别得到三种结点排列:前序排列、中序排列、后序排列。其结构已在之前提过。
前序遍历
//先序遍历
int preorder_traversal(struct node *root_node)
{
if(root_node==NULL)
{
return 0;
}
printf("%c",root_node->data);
preorder_traversal(root_node->left_child);
preorder_traversal(root_node->right_child);
return 0;
}
中序遍历
//中序遍历
int inorder_traversal(struct node *root_node)
{
if(root_node==NULL)
{
return 0;
}
inorder_traversal(root_node->left_child);
printf("%c",root_node->data);
inorder_traversal(root_node->right_child);
return 0;
}
后序遍历
//后序遍历
int postorder_traversal(struct node *root_node)
{
if(root_node==NULL)
{
return 0;
}
postorder_traversal(root_node->left_child);
postorder_traversal(root_node->right_child);
printf("%c",root_node->data);
return 0;
}