树的基本概念
树的定义
数是有n个节点组成的有限集合(记为T)。其中
- 如果n=0,它是一颗空树,这是树的特例
- 如果n>0,这n个节点中存在(且仅存在)一个节点作为树的根节点,简称为根;其余节点可分为m(m>=0)个互不交集的有限集T1,T2,Tn,其中每个子集本身又是一颗符合本定义的树,称为根的子树
- 树的定义是递归的
- 树是一种非线性结构。
树的逻辑表示方法
- 树形表示法
- 文氏图表示方法
- 凹入表示法
- 括号表示法
树的基本术语
- 节点的度与树的度:树中某个节点的子树的个数称为该节点的度。树中各节点的度的最大值称为树的度,通常将度为m的树称为m次树
- 分支节点与叶子节点:度不为0的节点称为非终端节点,又叫分支节点。度为零的节点称为终端节点或叶子节点。
- 路径与路径长度:对于任意俩个节点ki和kj,若树中存在一个节点序列ki,ki1,ki2,....,kin,kj,使得序列中除ki外的任一节点都是其在序列中的前一个节点的后继节点,则称该节点序列为ki到kj的一条路径。路径长度等于路径所通过的节点数目减1(即路径上分支数目)。
- 孩子节点,双亲节点和兄弟节点
- 节点的层次和树的高度:树中的每个节点都处在一定的层次上。节点的层次从树根开始定义,根节点为第一层,它的孩子节点为第二层,以此类推,一个节点所在的层次为其双亲节点所在的层次加1.树中节点的最大层次称为树的高度(或树的深度)
- 有序树和无序树:若树中各节点的子树是按照一定的次序从左向右安排的,且相对次序是不能随意变换的,则称为有序树,否则称为无序树
- 森林:n(n>0)个互不相交的树的集合称为森林。森林的概念与树的概念十分相近,因为只要把树的根节点删去就成了森林。反之,只要给n棵独立的树加上一个节点,并把这n棵树作为该节点的子树,则森林就变成了树。
树的性质
性质1 树中的节点数等于所有节点的度数加1
证明:略
性质2 度为m的树中第i层上之多有mi?1(i≥1)个节点
证明:采用数学归纳法证明
对于第一层,因为树中的第一层上只有一个节点,即整个树的根节点,而由i=1代入mi?1,得mi?1=m1?1=1,也同样得到只有一个节点,显然结论成立。
假设对于第(i-1)层(i>1)命题成立,即度为m的树中第(i-1)层上至多有mi?2个节点,则根据树的度的定义,度为m的树中每个节点至多有m个孩子节点,所以第i层上的节点数至多为第(i-1)层上节点数的m倍,即至多为mi?2?m=mi?1个,这与命题相同,故命题成立。
推广:当一棵m次树的第i层有mi?1个节点(i≥1)时,称该层是满的,若一颗m次树的所有叶子节点在同一层,除该层外其余每一层都是满的,称该树为满m次树。显然,满m次数是所有相同高度的m次树中节点总数最多的树。也就是说,对于n个节点,构造的m次树为满m次树或者接近满m次树,此时树的高度最小
性质3 高度为h的m次树至多有mh?1m?1个节点
证明:由树的性质2可知,第i层上最多节点数为mi?1,显然当高度为h的m次树(即度为m的树)为满m次树时,整棵m次树具有最多节点数,因此有:
整棵树的最多节点数=每一层最多节点数之和=m0+m1+m2+...+mh?1=mh?1m?1
所以,满n次树的另外一种定义为:当一棵高度为h的m次树上的节点数等于mh?1m?1时,称该树为满m次树。
性质4 具有n个节点的m次树的最小高度为
略
例7_1
例7_2
树的基本运算
树的运算主要分为3大类
- 寻找满足某种特定关系的节点,如寻找当前节点的双亲节点等
- 插入或删除某个节点,如在树的当前节点上插入一个新节点或删除当前节点的第i个孩子节点
- 遍历树中每个节点
树的遍历
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;
由于树的孩子兄弟链存储结构固定有俩个指针域,并且这个俩个指针是有序的(即兄弟域和孩子域不能混淆),所以孩子兄弟链存储结构实际上是把该树转换称二叉树的存储结构。把树转换称二叉树所对应的结构恰好就是这种孩子兄弟链存储结构。所以,孩子兄弟链存储结构的最大优点是可以方便地实现树和二叉树的相互转换。但是,孩子兄弟链存储结构的确定也和孩子链存储结构的缺点一样,就是从当前节点查找其双亲节点比较麻烦,需要从树的根节点开始起遍历查找
例题7_3 以孩子兄弟链作为树的存储结构,编写一个求树高度的递归算法
//错误做法
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且有2h?1个节点的二叉树称为满二叉树
满二叉树的特点:
- 叶子节点都在最下一层
- 只有度为0和度为2的节点
若二叉树中最多只有最下面俩层的节点的度数小于2,并且最下面一层的叶子节点都依次排列在该层最左边的位置上,则这样的二叉树称为完全二叉树。
不难看出,满二叉树是完全二叉树的一种特例,并且完全二叉树与等高度的满二叉树对应位置的节点有同一编号。
完全二叉树的特点如下:
- 叶子节点只可能在层次最大的俩层上出现
- 对于最大层次中的叶子节点。都依次排列在该层最左边的位置上
- 如果有度为1的节点,只可能有一个,且该节点只有左孩子而无右孩子
- 按层序编号后,一旦出现某节点(其编号为i)为叶子节点或只有左孩子,则编号大于i的节点均为叶子节点
- 当节点总数n为奇数时,度为1的节点个数n1=0,当节点总数n为偶数时,n1=1。
二叉树的性质
性质1 非空二叉树上叶子节点数等于双分支节点数加1
证明:设二叉树上叶子节点数为n0,单分支节点数为n1,双分支节点数为n2。则总节点数n=n0+n1+n2。由此可推出:
节点的分支数=单分支节点数+2*双分支节点数,即总分支数=n1+2n2
又因为二叉树中除根节点以外,每个节点都有唯一的一个分支指向它,因为二叉树中总的分支数=总节点数-1
综上所述:n1+2n2=n0+n1+n2?1
即n0=n2+1
性质2 非空二叉树上第i层上至多有2i?1个节点(i≥1)
由树的性质2可推出
性质3 高度为h的二叉树至多有2h?1个节点(h≥1)
由树的性质3推出
性质4 对完全二叉树中编号为i的节点(n为节点数)有:
1.若i≤n/2,即2i≤n,则编号为i的节点为分支节点,否则为叶子节点
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;
二叉树的基本运算及其实现
二叉树的基本运算概述
为了方便,假设二叉树均采用二叉链存储结构进行存储,每个节点值为单个字符
- 创建二叉树CreateBTNode(* b,* str):根据二叉树括号表示法字符串str生成对应的二叉链存储结构,后者的根节点为*b
- 查找节点FindNode(* b,x):在二叉树b中寻找Data、域值为x的节点,并返回指向该节点的指针
- 找孩子节点LchildNode(p)和RchildNode(p):分别求二叉树中节点*p的左孩子节点和右孩子节点
- 求高度BTNodeDepth(*b):求二叉树b的高度,若二叉树为空,则其高度为0;否则,其高度等于左子树与右子树的高度中的最大高度加1
- 输出二叉树DispBTNode(*b):以括号表示法输出一棵二叉树
二叉树的基本运算算法实现
1.创建二叉树CreateBTNode(* b,*str)
假设用括号表示法表示的二叉树字符串str是正确的,用ch扫描str,其中只有4类字符:
- 若ch=’(‘:表示前面刚创建的节点*p存在孩子节点,需将其进栈,以便建立它和其孩子节点的关系(如果一个节点刚创建完毕,其后一个字符不是”(“,表示该节点是叶子节点,不需要进栈),然后开始处理该节点的左孩子,因此置k=1,表示其后创建的节点将作为这个节点(栈顶节点)的左节点
- 若ch=”)”:表示以栈顶节点为根节点的子树创建完毕,将其退栈;
- 若ch=”,”:表示开始处理栈顶节点的右孩子节点;
- 其他情况:只能是单个字符,表示要创建一个节点*p,根据k值建立它与栈顶节点之间的联系,当k=1时,表示这个节点是栈顶节点的左孩子节点,当k=2时,表示这个节点是栈顶节点的右孩子节点。
如此循环直达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];
}
}
2.查找节点FindNode(*b,x)
采用递归算法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)
}
3.求高度BTNodeDepth(*b)
求二叉树的高度的递归模型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);
}
二叉树的遍历
二叉树遍历的概念
先序遍历
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
中序遍历
- 中序遍历左子树
- 访问根节点
- 中序遍历右子树
后续遍历
- 后续遍历左子树
- 后续遍历右子树
- 访问根节点
层次遍历
- 访问根节点(第一层)
- 从左到右访问第二层的所有节点
- 从左到右访问第3层的素有节点,,第h层的所有节点
二叉树遍历递归算法
//先序遍历的递归算法
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");
}
}
后序遍历非递归算法
略
二叉树的构造
定理7.1
任何n个不同节点的二叉树,都可由它的中序序列和先序序列唯一确定
定理7.2
任何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有点太长了,关于哈夫曼树另外写了一篇: