上一篇文章简单的介绍了数据结构有哪些,要遵循的一些规则,今天就介绍二叉树。
一、基本概念
每个节点最多有两颗子树,左子树和右子树,次序不能颠倒。
性质:
1、非空二叉树的第n层上至多有2^(n-1)个元素。
2、深度为h的二叉树至多有2^h-1个结点。
满二叉树:所有终端都在同一层次,且非终端结点的度数为2。
在满二叉树中若其深度为h,则其所包含的结点数必为2^h-1。
完全二叉树:除了最大的层次即成为一颗满二叉树且层次最大那层所有的结点均向左靠齐,即集中在左面的位置上,不能有空位置。
对于完全二叉树,设一个结点为i则其父节点为i/2,2i为左子节点,2i+1为右子节点。
二、存储结构
顺序存储
将数据存放在一块固定的数组中。
#define LENGTH 100
typedef char datatype;
typedef struct node{
datatype data;
int lchild,rchild;
int parent;
}Node;
Node tree[LENGTH];
int length;
int root;//根节点
虽然在比遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树。二叉树通常以链表存储。
链式存储
typedef char datatype;
typedef struct binnode
{
datatype data;
pBinNode lchild;
pBinNode rchild;
}BinNode,*pBinNode;//本身就是指向节点的指针
三、二叉树的遍历
遍历即将所有的节点访问切仅访问一次。按照根节点位置的不同分为前序遍历、中序遍历、后序遍历。
遍历方式:
前序遍历:根节点——》左子树——》右子树
中速遍历:左子树——》根节点——》右子树
后序遍历:左子树——》右子树——》根节点
eg:求下面树的三种遍历
前序遍历:abdefgc
中序遍历:debgfac debfgac(错误结果)
后序遍历:edgfbca
下面是关于遍历一些宽展知识:
已知二叉树的前序遍历和中序遍历,可以确定一颗二叉树。
已知二叉树的中序遍历和后序遍历,可以确定一颗二叉树。
但是已知二叉树的前序和后序遍历无法确定一颗二叉树。
四、遍历的实现
递归实现(前序遍历为例子,其他的只是输出的位置稍有不同)
void preorder(pBinNode t)
{
if(t)
{
printf("%d",t->data);
preorder(t->lchild);
preorder(t->rchild);
}
}
非递归实现:
因为当遍历过根节点之后还要回来,所有必须将其存起来。考虑到先进后出的特点。选用栈存储。数量确定,以顺序栈存储。
#define SIZE 100
typedef struct seqstack
{
pBinNode data[SIZE];
int tag[SIZE];//后序遍历使用
int top;//top为数组下标
}seqstack;
void push(seqstack *s,pBinNode t)
{
if(s->top == SIZE)
{
prinrf("the stack is full\n");
}
else
{
s->top++;
s->data[s->top]=t;
}
}
pBinNode pop(seqstack *s)
{
if(s->top == -1)
{
return NULL;
}
else
{
s->top --;
return s->data[s->top+1];
}
}
1、前序遍历
//前序遍历输出数据
void preorder_dev(pBinNode t)
{
seqstack s;
s.top = -1;//top在这里表示了数组中的位置,所有空为-1
if(!t)
{
printf("the tree is empty\n");
}
else
{
while(t || s.top != -1)
{
while(t)//只要节点不为空就应该入栈保存,与其左右节点无关
{
printf("%c",t->data);
push(&s,t);
t = t->lchild;
}
t = pop(&s);
t = t->rchild;
}
}
}
上面程序解析
1、判断二叉树t是否为空,不为空直接打印根节点数据,并压入栈,同时指向当前节点的左子树。
2、上个节点的左子树变成当前节点,使用内层循环打印数据,压栈,指向当前节点的左子树,循环判断。
3、当前节点没有左子树时,退出内层循环执行外层循环,从栈中得到上一层循环节点(出栈),并指向右子树进行内层循环,判断是否为空,打印数据并指向左子树,重复2的操作。
4、循环执行2.和 3.操作,知道二叉树为空。
2、中序遍历
void midorder(pBinNode t)
{
seqstack s;
s.top = -1;
if(!t)
{
printf("the tree is empty\n");
}
else
{
while(t || s.top != -1)
{
push(&s,t);
t->lchild;
}
t = pop(&s);
printf("%s",t->data);
t= t->rchild;
}
}
3、后序遍历
因为后序遍历最后还要要访问根节点一次,所有要访问根节点两次。采取夹标志位的方法解决这个问题。
void postorder_dev(pBinNode t)
{
seqstack s;
s.top = -1;
if(!t)
{
printf("the tree is empty\n");
}
else
{
while(t || s.top != -1)//栈空了的同时t也为空
{
while(t)
{
push(&s,t);
s.tag[s.top] = 0;//设置访问标记,0位第一次访问,1位第二次访问
t = t->lchild;
}
if(s.tag[s.top] == 0)//第一次访问时,转向同层右子树
{
t = s.data[s.top];//左走到底时t是为空的,必须有着步!
s.tag[s.top] =1;
t = t->rchild;
}
else
{
//找到栈中下一个第一次访问的节点,退出循环时并没有pop所以为其左子节点。
while(s.tag[s.top] == 1)
{
t = pop(&s);
printf("%c",t->data);
}
t = NULL;//必须将t置空。跳过向左走,直接向右走。
}
}
}
}
4、层次遍历:即每一层从左向右输出
元素需要存储有先进先出的特性,所以选用队列存储。
//队列定义
#define MAX 1000
typedef struct seqqueue{
pBinNode data[MAX];
int front;
int rear;
}seqqueue;
void enter(seqqueue *q,pBinNodet){
if(q->rear == MAX){
printf("the queue is full!\n");
}else{
q->data[q->rear] = t;
q->rear++;
}
}
bintree del(seqqueue *q){
if(q->front == q->rear){
return NULL;
}else{
q->front++;
return q->data[q->front-1];
}
}
遍历实现
void level_tree(bintree t){
seqqueue q;
bintree temp;
q.front = q.rear = 0;
if(!t){
printf("the tree is empty\n");
return ;
}
enter(&q,t);
while(q.front != q.rear){
t=del(&q);
printf("%c ",t->data);
if(t->lchild){
enter(&q,t->lchild);
}
if(t->rchild){
enter(&q,t->rchild);
}
}
}
5、利用前序遍历的结果生成二叉树
//递归调用,不存点,想的时候只关注于一个点,因为还会回来的,不要跟踪程序运行,否则容易多加循环
void createtree(bintree *t){
datatype c;
if((c=getchar()) == '#')
*t = NULL;
else{
*t = (bintree)malloc(sizeof(BinNode));
(*t)->data = c;
createtree(&(*t)->lchild);
createtree(&(*t)->rchild);
}
}
6、二叉树的查找
bintree search_tree(pBinNode t,datatype x){
if(!t){
return NULL;
}
if(t->data == x){
return t;
}else{
if(!search_tree(t->lchild,x)){
return search_tree(t->rchild,x);
}
return t;
}
}
7、统计节点个数
int count_tree(pBinNode t)
{
if(t){
return (count_tree(t->lchild)+count_tree(t->child)+1);
}
return 0;
}
8、比较两个树是否相同
int is_equal(pBinNode t1,pBinNode t2)
{
if(!t1 && !t2)//都为空就相等
{
return 1;
}
//有一个为空或数据不同就不判断了
if(t1 && t2 && t1->data == t2->data)
{
if(is_equal(t1->lchild,t2->lchild))
{
if(is_equal(t1->rchild,t2->rchild))
{
return 1;
}
}
}
return 0;
}
9、求二叉树的深度
int hight_tree(pBinNode t){
int h,left,right;
if(!t){
return 0;
}
left = hight_tree(t->lchild);
right = hight_tree(t->rchild);
h = (left>right?left:right)+1;
return h;
}
五、一个例子
假设有棵树,长下面这个样子,它的前序遍历,中序遍历,后续遍历都很容易知道。
前序遍历:GDAFEMHZ
中序遍历:ADEFGHMZ
后序遍历:AEFDHZMG
问题:假设仅仅知道前序和后序遍历,如何求后序遍历?比如已知一棵树的前序遍历是“GDAFEMHZ”,中序遍历是“ADEFGHMZ”应该如何求后序遍历?
步骤:
第一步:root最简单,前序遍历的第一个节点G就是root。
第二步:继续观察前序遍历GDAFEMHZ,除了知道G是root,剩下的节点必然是root的左右子树数据,没办法找到更多的信息。
第三步:观察中序遍历ADEFGHMZ。其中root节点G左侧ADEF必然是root左子树,G右侧的HNZ是root的右子树。
第四步:观察左子树ADEF,左子树的根节点必然是大树root的letfchild。在前序遍历中,大树的root的leftchild位于root之后,所以左子树的根节点为D。
第五步:同样的道理,root的右子树点HMZ中根节点也可以通过前序遍历求得。在前序遍历中,一定要先把root和root的所有左子树节点遍历完之后才会遍历右子树,并且遍历的右子树的第一个节点就是右子树的根节点M(在前序遍历中HMZ排列顺序第一个)。
如何知道哪里是前序遍历中的左子树和右子树的分界线呢?通过中序遍历去数节点的个数。在上一次中序遍历中,root左侧是ADEF,所以有4个节点位于root左侧。那么在前序遍历中,必然是第1个是G,第2到第5个有A、D、E、F过程,第6个就是root的右子树的根节点了,是M。
第六步:观察发现,上面的过程是递归的。先找到当前树的根节点,然后划分为左子树,右子树,然后进入左子树重复上面的过程,然后进入右子树重复上面的过程,左后就可以还原一棵树了。
第七步:其实,如果仅仅要求写后序遍历,甚至不要专门占用空间保存还原后的树。只需要稍微改动第六步,就能实现要求。仅需要把第六步的递归的过程改动为如下:
1确定根,确定左子树,确定右子树
2在左子树中递归
3在右子树中递归
4打印当前根。
参考一个网上的讨论,具体程序是:
//下面代码是手写,如果有错误的地方请指正
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
struct TreeNode
{
struct TreeNode* left;
struct TreeNode* right;
char elem;
};
TreeNode* BinaryTreeFromOrderings(char* inorder,char* preorder,int length)
{
if(length == 0)
{
return NULL;
}
TreeNode* node = new TreeNode;//Noice that [new] should be written out;
node->elem = *preorder;
int rootIndex = 0;
for(;rootIndex<length;rootIndex++)//a variation of the loop
{
if(inorder[rootIndex] == *preorder)
break;
}
node->left = BinaryTreeFromOrderings(inorder,preorder+1,rootIndex);
node->right = BinaryTreeFromOrderings(intorder+rootIndex+1,preorder + rootIndex+1,length-(rootIndex+1));
cout<<node->elem<<endl;
return node;
}
int main(int argc,char** argv)
{
char* pr ="GDAFEMHZ";
char* in = "ADEFGHMZ";
BinaryTreeFromOrderings(in,pr,8);
printf("\n");
return 0;
}
其实上面的代码不够简洁。题目只要求输出后序遍历,并没有要求建树。所有,不需要计算出noe->left与node->right,也不需要去return node。改进版本如下:
struct TreeNode
{
struct TreeNode* left;
struct TreeNode* right;
char elem;
};
void BinaryTreeFromOrderings(char* inorder,char* preorder,int length)
{
if(length == 0)
{
return ;
}
TreeNode* node = new TreeNode;//Noice that [new] should be written out;
node->elem;
int rootIndex=0;
for(;rootIndex<length;rootIndex ++)
{
if(inorder[rootIndex] == *preorder)
break;
}
//left
BinaryTreeFromOrderings(inorder,preorder+1,rootIndex);
//right
BinaryTreeFromOrderings(inorder+rootIndex+1,preorder+rootIndex+1,length- (rootIndex+1));
cout<<node->elem<endl;
return;
}
int main(int argc,char** agrv)
{
printf("Hello World\n");
char* pr = "GDAFEMHZ";
char* in = "ADEFGHMZ";
BinaryTreeFromOrderings(in,pr,8);
printf("\n");
return;
}
再考虑下,题目只要求输出后序遍历,所以其实连建立node也是没有必要的。可以直接把当前节点的value保存在一个char中,然后输出。
#include <stdio.h>
#include <iostream>
using namespace std;
struct TreeNode
{
struct TreeNode* left;
struct TreeNode* right;
char elme;
};
void BinaryTreeFromOrderings(char* inorder,char *preorder,int length)
{
if(length == 0)
{
return;
}
char node_value= *preorder;
int rootIndex = 0;
for(;rootIndex<length;rootIndex++)
{
if(inorder[rootIndex]==*preorder)
break;
}
//left
BinaryTreeFromOrderings(inorder,preorder +1,rootIndex);
//right
BinaryTreeFromOrderings(inorder+rootIndex+1,preorder+rootIndex+1,length-(rootIndex+1));
cout<<node_value<<endl;
return;
}
int main(int argc,char* argv[])
{
printf("Hello World\n");
char* pr = "GDAFEMHZ";
char* in = "ADEFGHMZ";
BinaryTreeFromOrderings(in,pr,8);
printf("\n");
return;
}