关于树已经算接触的很多了,但是从来没有做过一个系统的总结,总是学完忘忘了学,为此做一个基本的总结。
首先,对于基本的树结构来说,有几个名词还是值得注意的,例如深度,层次空树等,对于一些基本的理论内容,本次总结不再赘述,后续另开一章总结,本次总结主要以思想和代码为主。
一、二叉树的存储和基本操作
对于二叉树来演,最基本的还是其存储结构和基本操作。
对于二叉树的基本存储结构,就采用一个简单的结构体;
struct node{
typename data;
node* lchild;
node* rchild;
}
对于insert和建立二叉树,建立二叉树一般采用调用insert来进行插入,从而进行建树操作的完成;
对于完全二叉树,也可以有不一样的存储操作。由于完全二叉树任何节点,只要有左右孩子,其左孩子编号必定是2X,右孩子编号必定为2X+1,所以我们可以利用数组来进行存储操作,并且数组的顺序正好为二叉树的层序遍历。
二、二叉树的遍历
对于二叉树来说,也有老生常谈的四种遍历方法;
1.先序遍历,也就是爹->左孩子->右孩子的遍历顺序;值得注意的是,先序遍历的结果序列,任何一个子树所在的序列,第一个节点必为根节点。
2.中序遍历,也就是左孩子->爹->右孩子的遍历顺序;同样,在该序列情况下,根节点总是在左子树和右子树中间,所以只要知道了根节点,就可以判断出左右子树都是哪些。
3.后序遍历,也就是左孩子->右孩子->爹的遍历顺序;在该序列情况下,任何子树序列,最后一个节点必定为根节点。
4.层序遍历,也就是逐层来进行访问。该种访问方式的实现主要利用队列来实现。将根节点进队列,然后取出将其子节点继续塞入队列,从而进行层序遍历。
void LayerOrder(node* root){
queue<node*>q;
q.push(root);
while(q.empty()){
node* now=q.front();
q.pop();
printf("%d",now->data);
if(now->lchild!=NULL)
q.push(now->lchild);
if(now->rchild!=NULL)
q.push(now->rchild);
}
}
(注意指针拷贝的问题,所以queue内的元素为指针而不是node型,不然就会无法对树进行操作)
对于层序遍历,如果想知道该节点位于第几层,也可以进行节点的改造和遍历过程的改造。
struct node{
int data;
int layer;
node* lchild;
node* rchild;
};
void LayerOrder(node* root){
queue<node*>q;
root->layer=1;
q.push(root);
while(q.empty()){
node* now=q.front();
q.pop();
printf("%d",now->data);
if(now->lchild!=NULL){
now->lchild->layer=now->layer+1;
q.push(now->lchild);
}
if(now->rchild!=NULL){
now->rchild->layer=now->layer+1;
q.push(now->rchild);
}
}
}
值得注意的是这几种遍历方式的混合。中序序列+其余的任何一种遍历方式都可以唯一的确立一个二叉树。原因是只有中序遍历可以根据根节点建立左右子树,而其唯一需要的就是根节点信息,其他三种方法都可以提供给其必要的根节点信息。
所以对于考研或者oj题目来说,有一道题目便是给两个序列,让你求出一个唯一的二叉树。
举个例子:给定一个二叉树的先序遍历序列和中序遍历序列,重建这一棵二叉树。
对于这一类的题目,其实有固定的解法和套路,那就是从给定的两个序列中强推出树的根节点和左右子树,从而进行递归构建,从根节点开始递归构建。
对于先序序列来说,第一个节点必为根节点,所以就可以从中序序列中找到根节点的左右子树,然后在先序序列中找左右子树的根节点,以此往复循环。
代码如下:
node* create(int preL,int preR,int inL,int inR){
if(preL>preR){
return NULL;
}
node* root=new node;
root->data=pre[preL];//先序序列
int k;
for(int k=inL;k<=inR;k++){
if(in[k]==pre[preL]){
break;//寻找中序序列中的根节点,从而找出左右子树;
}
}
int numLeft=k-inL;
root->lchild=create(preL+1,preL+numLeft,inL,k-1);//进行左子树的构建
root->rchild=create(preL+numLeft+1,preR,k+1,inR);//进行左子树的构建
return root;
}
三、二叉树的静态实现
使用指针和结构体的实现被称为动态实现,下面介绍一下数组作为存储结构的静态实现。对于数组来说,进行左右节点的访问途径为使用数组下标进行访问。
struct node{
typename data;
int lchild;
int rchild;
}Node[Maxn];//Maxn为节点上限个数
对于新节点的构建:
初始的index值为0。
int newnode(int v){
Node[index].data=v;
Node[index].lchild=-1;
Node[index].rchild=-1;
return index++;
}
对于节点的搜寻和修改:
int newnode(int v){
Node[index].data=v;
Node[index].lchild=-1;
Node[index].rchild=-1;
return index++;
}
对于节点的插入:
void insert(int &root,int x){
if(root==-1){
root=newnode(x);
return;
}
if(插在左子树)
insert(Node[root].lchild,x);
else
insert(Node[root].rchild,x);
}
对于二叉树的建立:
int Create(int data[],int n){
int root=-1;
for(int i=0;i<n;i++){
insert(root,data[i]);
}
return root;
}
对于静态二叉树来说,四种遍历也大相径庭,所以不再赘述,仍然是采用递归和队列的方式来实现。
三、树的遍历操作
注意一点,对于二叉树来说,其左右子树有严格的先后次序,但是对于普通的树却不是如此,普通的树的子节点和子树并没有严格的先后次序。
对于树的存储,这里采用静态的写法总结。对于动态的指针结构体存储来说,我们只需要利用数组来保存数组即可,和二叉树不同的也就是其内部存储的子节点指针的个数。
其中树的节点构造和二叉树静态存储类似,开一个vector来进行子节点存储;
struct node{
typename data;
vector child;
}Node[maxn];
对于节点的新建,有构建函数:
int index=0;
int newNode(int v){
Node[index].data=v;
Node[index].child.clear();
return index++;
}
对于树的构建来说,往往会对节点进行编号,所以直接将编号作为数组的下标来进行存储,而不用自己指定。同理,子节点的vector数组里的子节点index也是自己图中所给的标号。
对于树来说,有两种遍历方式,先根遍历和层序遍历;
先根遍历和先序遍历相似,唯一不同的在于没有递归边界。
void PreOrder(int root){
print("%d",Node[root].data);
for(int i=0;i<Node[root].child.size();i++){
PreOrder(Node[root].child[i]);
}
}
层序遍历和二叉树的实现方式也类似,也是通过队列来放置节点在数组中的下标,每次取出队首元素来进行访问,之后将其所有的子节点放入队列,直到队列为空。
void LayerOrder(int root){
queue<int> Q;
Q.push(root);
while(!Q.empty()){
int front=Q.front();
Q.pop();
printf("%d",Node[front].data);
for(int i=0;i<Node[front].child.size();i++){
Q.push(Node[front].child[i]);
}
}
}
同样,可以在struct内置layer来进行层数计数,和二叉树层序遍历相同,这里不再赘述。