标签:
数是有n个节点组成的有限集合(记为T)。其中
性质1 树中的节点数等于所有节点的度数加1
证明:略
性质2 度为m的树中第i层上之多有
证明:采用数学归纳法证明
对于第一层,因为树中的第一层上只有一个节点,即整个树的根节点,而由i=1代入
假设对于第(i-1)层(i>1)命题成立,即度为m的树中第(i-1)层上至多有
推广:当一棵m次树的第i层有
性质3 高度为h的m次树至多有
证明:由树的性质2可知,第i层上最多节点数为
整棵树的最多节点数=每一层最多节点数之和=
所以,满n次树的另外一种定义为:当一棵高度为h的m次树上的节点数等于
性质4 具有n个节点的m次树的最小高度为
略
树的运算主要分为3大类
1.先根遍历
2.后根遍历
3.层次遍历
从根节点开始,从上到下,从左到右访问树中的每一个节点
树的存储要求既要存储节点的数据元素本身,又要存储节点之间的逻辑关系。有关树的存储结构有很多,常用的有3种:双亲存储结构,孩子链存储结构和孩子兄弟链存储结构
这种存储u结构是一种顺序存储结构,用一组连续空间存储树的所有节点。同时在每个节点中辐射一个伪指针指示其双亲节点的位置
定义如下:
typedef struct
{
ElemType data; //存放节点的值
int parent; //存放双亲的位置
}PTree[MaxSize];
该存储结构利用了每个节点(根节点除外)只有唯一双亲的性质。在这种存储结构中,求某个节点的双亲节点十分容易,但求某个节点的孩子节点时需要遍历整个结构
在这种存储结构中,每个节点不仅包含数据值,还包含指向其所有孩子节点的指针。由于树中每个节点的子树个数(即节点的度)不同,若按照各个节点的度设计变长结构,则每个节点的孩子节点指针域个数增加使算法实现非常麻烦。孩子链存储结构按树的度(即树中所有节点度的最大值)设计节点的孩子节点指针域个数。
如下:
typedef struct node
{
ElemType data; //节点的值
struct node * sons[MaxSons]; //指向孩子节点
}TSonNode;
其中,MaxSons为最多的孩子节点个数,或为该树的度。
孩子链存储结构的优点是查找某个节点的孩子节点非常方便,其缺点是查找某节点的双亲节点比较费时,另外,当树的度比较大时,存在较多的空指针域
孩子兄弟链存储结构是为每个节点设计3个域:一个数据元素域,一个指向该节点的第一个孩子节点的指针域,一个指向该节点的下一个兄弟节点的指针域
定义如下:
typedef struct tnode
{
ElemType data; //节点的值
struct tnode * hp; //指向兄弟节点
struct tnode * vp; //指向孩子节点
}TSBNode;
由于树的孩子兄弟链存储结构固定有俩个指针域,并且这个俩个指针是有序的(即兄弟域和孩子域不能混淆),所以孩子兄弟链存储结构实际上是把该树转换称二叉树的存储结构。把树转换称二叉树所对应的结构恰好就是这种孩子兄弟链存储结构。所以,孩子兄弟链存储结构的最大优点是可以方便地实现树和二叉树的相互转换。但是,孩子兄弟链存储结构的确定也和孩子链存储结构的缺点一样,就是从当前节点查找其双亲节点比较麻烦,需要从树的根节点开始起遍历查找
//错误做法
int TreeHeight(TSBNode * t)
{
if(t == NULL)
return (0);
else if(t.vp == NULL)
return (1);
else
{
return 1+TreeHeight(t.vp);
}
}
//正确做法
int TreeHeight(TSBNode * t)
{
if(t == NULL)
return (0);
else if(t.vp == NULL)
return (1);
else
{
p = t->vp;
while(p!=NULL)
{
m = TreeHeight(p);
if(max<m)
max = m;
p = p->hp;
}
return (max+1);
}
}
显然,和树的定义一样,二叉树的定义也是一个递归定义。二叉树的结构简单,存储效率高,其运算算法也相对简单,而且任何m次树都可以转化为二叉树结构,因此二叉树具有很重要的地位。
二叉树和度为2的树(2次树)是不同的,其差别在于,对于非空树:
- 度为2的树中至少有一个节点的度为2,而二叉树没有这种要求;
- 度为2的树不区分左,右子树,而二叉树是严格区分左右子树的
在一棵二叉树中,如果所有分支点都有左孩子点和右孩子节点,并且叶子节点都集中在二叉树的最下一层,这样的二叉树称为满二叉树。也可以从树和树高度之间的关系来定义满二叉树,即一棵高度为h且有
满二叉树的特点:
若二叉树中最多只有最下面俩层的节点的度数小于2,并且最下面一层的叶子节点都依次排列在该层最左边的位置上,则这样的二叉树称为完全二叉树。
不难看出,满二叉树是完全二叉树的一种特例,并且完全二叉树与等高度的满二叉树对应位置的节点有同一编号。
完全二叉树的特点如下:
性质1 非空二叉树上叶子节点数等于双分支节点数加1
证明:设二叉树上叶子节点数为
节点的分支数=单分支节点数+2*双分支节点数,即总分支数=
又因为二叉树中除根节点以外,每个节点都有唯一的一个分支指向它,因为二叉树中总的分支数=总节点数-1
综上所述:
即
性质2 非空二叉树上第i层上至多有
由树的性质2可推出
性质3 高度为h的二叉树至多有
由树的性质3推出
性质4 对完全二叉树中编号为i的节点(n为节点数)有:
1.若
2.若n为奇数,则每个分支节点都既有左孩子节点,又有右孩子节点;若n为偶数,则编号最大的分支节点(编号为n/2)只有左孩子节点,没有右孩子节点
3.左编号为i的节点有左孩子节点,则左孩子节点的编号为2i;若编号为i的节点有右孩子节点,则右孩子节点的编号为2i+1。
4.除根节点外,若一个节点的编号为i,则它的双亲节点的编号为(i/2),也就是说,当i为偶数时,其双亲节点的编号为i/2,它是双亲节点的左孩子节点,当i为奇数时,其双亲节点的编号为(i-1)/2,它是双亲节点的右孩子节点
5.具有n个节点的完全二叉树的高度为略
由完全二叉树的定义和树的性质3推出
任何一个森林或一棵树都可以唯一地对应一棵二叉树,而任意的一棵二叉树也能唯一地对应一个森林或一棵树。
略
二叉树的存储结构主要由顺序存储结构和链式存储结构俩种
二叉树的顺序存储结构就是用一组地址连续的存储单元来存放二叉树的数据元素。
二叉树的顺序存储结构中节点存放次序是:对该树中每个节点进行编号,其编号从小到大的顺序就是节点存放在连续存储单元的先后次序。若二叉树存储到一维数组中,则该编号就是下标值+1。树中各节点的编号与等高度的完全二叉树中对应位置上节点的编号相同。其编号过程是:首先把树根节点的编号定为1,然后按照从上到下,从左到右的顺序,对每一节点进行编号。当某节点是编号为i的双亲节点的左孩子时,则它的编号应为2i;当它是右孩子节点时,则它的编号应为2i+1
优缺点
对于完全二叉树来说,采用顺序存储方式是十分适合的,它能够充分利用存储空间。但对于一般的二叉树,特别是对于那些单分支节点较多的二叉树来说是很不适合的,因为可能只有少数存储单元被利用,尤其是对退化的二叉树(即每个分支节点都是单分支的),空间浪费更是惊人。由于顺序存储结构这种固有的缺陷,使得二叉树的插入,删除等运算十分不方便。
typedef struct node
{
ElemType data;
struct node * lchild;
struct node * rchild;
}BTNode;
为了方便,假设二叉树均采用二叉链存储结构进行存储,每个节点值为单个字符
假设用括号表示法表示的二叉树字符串str是正确的,用ch扫描str,其中只有4类字符:
如此循环直达str处理完毕。
typedef struct node
{
ElemType data;
struct node * lchild;
struct node * rchild;
}
void CreateBTNode(BTNode * &b,char * str)
{
BTNode * St[MaxSize],*p;
int top=-1,j=0,k=1;
char ch;
ch = str[j];
b=NULL;
while(ch!=‘\0‘)
{
switch(ch)
{
case "(":k=1;top++;St[top]=p,k=1; //遇到左括号,上一次的p肯定跟接下来的括号内的东西有关,也就是说左括号左边的字符要成为栈顶元素
break;
case ")":top--;
break;
case ",":k=2;
break;
default:
p = (BTNode *)malloc(sizeof(BTNode ));
p->data = ch;
p->lchild = p->rchild = NULL;
if(b==NULL)
b = p;
else
switch(k)
{
case 1:St[top]->lchild = p;break;
case 2:St[top]->rchild = p;break;
}
break;
}
j++;
ch = str[j];
}
}
采用递归算法f(b,x)在二叉树b中查找值为x的节点,找到后返回其指针,否则返回null
BTNode * FindNode(BTNode *b,ElemType x)
{
if(b == NULL)
return NULL;
else if(b->data = x)
return b;
else
BTNode *a;
a = FindNode(b->lchild,x); //递归思想,先去到最底下找左子树,然后下面找右子树
if(a->data == e)
return a;
else
return FindNode(b->rchild,x)
}
求二叉树的高度的递归模型f(b)
int BTNodeHeight(BTNode *b)
{
int lchildh,rchildh;
if(b == NULL)
return (0);
else
lchildh = BTNodeHeight(b->lchild);
rchildh = BTNodeHeight(b->rchild);
return lchildh > rchildh?(lchildh+1):(rchildh+1);
}
//先序遍历的递归算法
void PreOrder(BTNode * b)
{
if(b!=NULL)
{
printf("%c",b->data);
PreOrder(b->lchild);
PreOrder(b->rchild);
//个人觉得利用栈的思想去理解非常好,就是一层一层往下,然后再一层一层返回
}
}
//中序遍历的递归算法
void InOrder(BTNode * b)
{
if(b!=NULL)
{
//中序遍历,先左子树,再中间,再右子树
InOrder(b->lchild);
printf("%c",b->data);
InOrder(b->rchild);
}
}
//后续遍历的递归算法
void PostOrder(BTNode * b)
{
if(b!=NULL)
{
//中序遍历,先左子树,再中间,再右子树
InOrder(b->lchild);
InOrder(b->rchild);
printf("%c",b->data);
}
}
例7.8 假设二叉树采用二叉链存储结构存储,试设计一个算法,输出一棵给定二叉树的所有叶子节点
void DispLeaf(BTNode * b)
{
if(b!=NULL)
{
if(b->lchild == NULL && b->rchild == NULL)
printf("%c",b->data);
else
DispLeaf(b->lchild);
DispLeaf(b->rchild);
}
}
例7_9 假设二叉树b采用二叉链存储结构,设计一个算法level()求二叉树中节点值为x的节点的层数。
int Level(BTNode *b,ELemType x,int h)
{
int p;
if(b==NULL)
return (0);
else
{
if(b->data == x)
return h;
else
{
p = Level(b->lchild,x,h+1);
if(p != 0)
return p;
else
return Level(b->rchild,x,h+1);
}
}
}
由先序遍历过程可知,先访问根节点,再访问左子树,最后访问右子树。因此,先将根节点进栈,在栈不为空时循环:由于栈的特点先进后出,所以先把右子树扔进去,再把左子树扔进去,然后每次都取出栈顶的元素,退栈,取子树然后存进去
void PreOrder1(BTNode * b)
{
BTNode * St[MaxSize],*p;
int top = -1;
if(b!=NULL)
{
top++;
St[top] = b;
while(top>-1)
{
p = St[top];
top--;
printf(p->data)
if(p->rchild != NULL)
{
top++;
St[top] = p->rchild;
}
if(p->lchild != NULL)
{
top++;
St[top] = p->lchild;
}
}
printf("\n");
}
}
由中序遍历过程可知,中序序列的开始节点是一棵二叉树的最左下节点,其基本思路是:先找到二叉树的开始节点,访问它,再处理其右子树。由于二叉链中指针的链接是单向的,因此采用一个栈保存需要返回的节点指针·
个人理解:就是先把一个接着一个的左节点丢进栈里,在丢完最后一个的时候,取出它输出。然后这时候思路就要取出父节点输出,也就是栈顶节点,然后输出后取出栈顶节点的右孩子节点,但是此时必须判断这个右孩子节点有没有自己的左孩子,所以又是一系列的循环。总结起来就是:每一个点都要遍历左子树,然后输出栈顶节点后,遍历栈顶节点的右孩子的左子树。如果理解不了建议看代码
void InOrder1(BTNode * b)
{
BTNode * St[MaxSize],*p;
int top = -1;
if(b!=NULL){
p = b;
while(top>-1 || p!=NULL)
{
while(p!=NULL)
{
top++;
St[top] = p;
p = p->lchild;
}
//左子树遍历完成
if(top>-1)
{
p = St[top];
top--;
printf(p->data);
p = p->rchild;
}
}
printf("\n");
}
}
略
任何n个不同节点的二叉树,都可由它的中序序列和先序序列唯一确定
任何n个不同节点的二叉树,都可由它的中序序列和后序序列唯一地确定
其实就是充分利用二叉树的空节点,就好像叶子节点,不是左右有俩个空节点嘛,就利用它们来写一些信息,这些信息可以是这个节点的前驱节点,或者是后继节点。但是有一点必须谨记,是按某种遍历方式的前驱节点和后继节点,就好比说你用中序遍历和后续遍历得出来的结果可能是不一样的。
当然,此时就必须为每个节点增加多一些信息,分别是ltag和rtag,这可以称之为线索。所以,在二叉树的每个节点上加上线索的二叉树称作线索二叉树。那么我们就必须设计一个算法,将普通的二叉树按某种方式遍历使其变成线索二叉树,这个过程称为二叉树的线索化
需要认识到以下几点:
节点的定义:
typedef struct node
{
ElemType data;
int ltag,rtag;
struct node * lchild;
struct node * rchild;
}TBTNode;
线索化的思路:
先采用中序遍历的递归,对左子树操作,然后对右子树操作。至于线索化呢,其实也不难,想一想线索化是要干什么,就是增加前驱节点和后继节点。那么总结这句话之前的这俩点,我们对每个节点要做的事就是:
那么整个流程就涉及到一个上个访问的节点,此时我们就必须有一个全局变量来记住上次访问的节点。还有一点要记住的就是记得修改ltag和rtag.
对应的线索化二叉树的算法如下
TBNode * pre;
void Thread(TBNode * &p)
{
if(p!=NULL)
{
Thread(p->lchild); //先去到最底下的子树
if(p->lchild == NULL)
{
p->ltag = 1;
p->lchild = pre;
}
else
p->ltag = 0;
if(pre->rchild == NULL)
{
pre->rtag = 1;
pre->rchild = p;
}
else
pre->rtag = 0;
pre = p;
Thread(p->rchild);
}
}
整个过程行云流水,应该很容易理解(递归思想).当然上面只是整个线索话的核心算法,在我们调用这个方法还必须做一些相应的准备工作:
TBTNode * CreateThread(TBNode * b)
{
TBTNode * root;
root = (TBTNode *)malloc(sizeof(TBTNode));
root->ltag=0;root->rtag=0;
root->rchild=b;
if(b==NULL)
root->lchild=root;
else
{
root->lchild=b;
pre=root;
Thread(b);
pre->rchild=root;
pre->rtag = 1;
root->rchild = pre;
}
return root;
}
好像这一篇blog有点太长了,关于哈夫曼树另外写了一篇:
标签:
原文地址:http://blog.csdn.net/qq122627018/article/details/51766265