5.2 二叉树
比起紫书上二叉树的基础知识,黑书拓展了一些特殊的树形结构。
5.2.1 二叉树的存储
二叉树的基本概念就不再多做介绍,对一些用语不太明白的详见紫书6.3 树和二叉树。
关于二叉树的存储结构,一般用指针来实现:
struct node{
int value; node *l,*r;}
l和r分别指向当前结点的左孩子和右孩子,当新建一个node时用new来申请内存,使用完毕时,用delete进行删除。
二叉树的遍历
主要就是两种遍历方式,宽度优先遍历和深度优先遍历。
宽度优先遍历
紫书中的遍历代码,当时对BFS还不够熟悉,写的反而应该是最详细的。
二叉树的宽度优先遍历和图的宽度优先遍历没有区别,甚至更加简单,因为图的BFS,状态图的BFS还需要判重防止重复的走到某个节点,二叉树按照顺序加入结点则不会出现这样的情况。
struct Tree{
//树结点
bool value_exist;//表示这个结点是否被赋值了
int value;//结点值
Tree *left,*right;//指向左边的子节点,指向右边的子节点
Tree(){
value_exist=NULL;left=NULL;right=NULL;}//构造函数
}*root;//定义一个根节点
//建立树
bool bfs(vector<int>&ans){
//ans即为我们需要得到的答案
ans.clear();
queue<Tree*>q; q.push(root); //建立树,初始只有一个根节点
while (!q.empty()){
Tree*u=q.front(); q.pop();
if (!u->value_exist) return false;
//如果我们现在遍历到的指针没有对应的值,那么代表在结点的建立过程中
//当前指针在建立的过程中被"顺带"地建立了出来,但是没有被赋值
//但是在一个二叉树中,一个结点存在的条件,是它必须要被赋值
//一个没有被赋值过的结点是不能被称为结点的
ans.push_back(u->value);//把队列的值放入到ans里
//因为是按照层序遍历的,所以是按照一层一层的顺序将输入存入队列的
if (u->left!=NULL) q.push(u->left); //先左后右
if (u->right!=NULL) q.push(u->right);
}
return true;
//输入正确,此时每一个被建立出来的结点,都有它对应的值
}
深度优先遍历
又称递归遍历,主要分为先序遍历,中序遍历和后序遍历。
先序遍历:首先访问根结点然后遍历左子树,最后遍历右子树。在遍历左、右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树,如果二叉树为空则返回。
递归公式:RreOrder(T)=T的根节点+RreOrder(T的左子树)+RreOrder(T的右子树)
中序遍历:首先遍历左子树,然后访问根结点,最后遍历右子树,若二叉树为空则结束返回。
递归公式:InOrder(T)=InOrder(T的左子树)+T的根节点+InOrder(T的右子树)
后序遍历:首先遍历左子树,然后遍历右子树,最后访问根结点,在遍历左、右子树时,仍然先遍历左子树,然后遍历右子树,最后遍历根结点,若二叉树为空则结束返回。
递归公式:PostOrder(T)=PostOrder(T的左子树)+PostOrder(T的右子树)+T的根节点
以先序遍历为例,伪代码如下:
void preorder(node *root){
cout<<root->value;//输出根节点的值
preorder(root->1); preorder(root->2);//递归遍历左子树和右子树
}
对于三种遍历方式而言,如果我们知道中序遍历和另一种遍历的结果,那么我们可以得到另一种遍历方式的结果。
hdu 1710 “Binary Tree Traversals”
输入二叉树的先序遍历结果和中序遍历结果,输出后序遍历。
分析:这个问题紫书上也做过了,那为什么我说一定需要中序遍历呢?因为先序遍历子树的第一个结点一定是子树的根节点,然后我们可以根据这个根节点在中序遍历中找到左子树和右子树。
#include<cstdio>
int pre[1010],in[1010],post[1010],k;//先序,中序,后序序列,k表示求序列时遍历到的位置,也用于记录结点个数
struct node{
int value; node *l,*r;//左右子节点
node(int v=0,node *l=NULL,node *r=NULL):value(v),l(l),r(r){
};//构造函数
};//根据两个序列构建树,l和r为建树序列的左右边界,t为先序遍历中该子树根节点的位置,root为当前树的根
void buildtree(int l,int r,int &t,node *&root){
int flag=-1;//flag记录中序遍历中根结点的位置
for (int i=l;i<=r;i++) if (in[i]==pre[t]){
flag=i; break;} if (flag==-1) return;//建树结束
root=new node(in[flag]); t++;//新建根节点,以根节点为基准切割出左子树和右子树进行递归遍历
if (flag>l) buildtree(l,flag-1,t,root->l);//当前结点的左子结点为左子树的根,递归建立左子树
if (flag<r) buildtree(flag+1,r,t,root->r);//当前结点的左子结点为右子树的根,递归建立右子树
}//根据树求先序序列,先赋值根节点,再遍历左子树右子树
void preorder(node *root){
if (root!=NULL){
post[k++]=root->value;//赋值
preorder(root->l); preorder(root->r);
}
}//根据树求中序序列,先遍历左子树,再赋值根节点,再遍历右子树
void inorder(node *root){
if (root!=NULL){
inorder(root->l); post[k++]=root->value;//赋值
inorder(root->r);
}
}//根据树求后序序列,先遍历左子树和右子树,再赋值根节点
void postorder(node *root){
if (root!=NULL){
postorder(root->l); postorder(root->r);
post[k++]=root->value;//赋值
}
}//要记得给建好的树释放空间,否则空间会爆炸
void remove_tree(node *root){
if (root==NULL) return;
remove_tree(root->l); remove_tree(root->r); delete root;//递归释放树的空间
}
int main(){
int n;
while(scanf("%d",&n)){
node* root; int t=1;
for (int i=1;i<=n;i++) scanf("%d",&pre[i]); for (int i=1;i<=n;i++) scanf("%d",&in[i]);
buildtree(1,n,t,root);//根节点的位置一定在先序序列的第一个
k=0; postorder(root);//根据建好的二叉树求后序遍历
for (int i=0;i<k;i++) printf("%d ",post[i]); remove_tree(root);//记得销毁树
} return 0;
}
如果不释放空间会出现内存泄漏的问题。
hdu 1622 “Trees on the level”
分析:就是根据给定的信息让你判断是否能建成一棵树,如果能建成输出层序优先遍历的结果,信息包括结点的键值和路径信息。
这个问题在紫书那里是作为例题的,给出当时的代码就不多作赘述了:
struct Tree{
//树结点
bool value_exist;//表示这个结点是否被赋值了
int value;//结点值
Tree *left,*right;//指向左边的子节点,指向右边的子节点
Tree(){
value_exist=NULL;left=NULL;right=NULL;}//构造函数
}*root;//定义一个根节点
//申请空间
Tree* create_newTree(){
return new Tree();}
//添加新节点
void add(int v,char *s){
Tree *u=root;//从根节点刚开始往下走
for (int i=0;i<strlen(s);i++){
if (s[i]=='L'){
//往左边的子节点走
if (u->left==NULL) u->left=create_newTree();//如果节点不存在,给它建立一个新节点
u=u->left;}
else if (s[i]=='R'){
//往右边的子节点走,至于为什么不直接用else,因为字符串有括号
if (u->right==NULL) u->right=create_newTree();//如果节点不存在,给它建立一个新节点
u=u->right; }
}
// if (u->value_exist) failed=true; 这段代码书上有,但我还不知道用来干嘛
u->value=v; u