前言
本章主要详解了树和二叉树(逻辑结构)的概念及其应用,还有二叉树的遍历和增删改查等操作。其中包括满二叉树、完全二叉树、平衡二叉树、二叉排序树和线索二叉树的相关知点,最后介绍了二叉树的应用-哈弗曼树的构造以及最小带权路径长度的求解。
总览:
①树和二叉树的概念
树的的基本概念
树是n(n≥0)个结点的有限集合,n=0时,称为空树
树的逻辑结构
而任意非空树应满足:
1)有且仅有一个特定的称为根
的结点。
2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集合,其中每一个集合本身又是一棵树,称为根结点的子树
。
3)n个结点的树中只有n-1条边。
树的基本术语
祖先结点和子孙结点:
以寻找"K"结点为例,A作为根结点,向下探索一条可到达"K"结点的路径,而该路径上除了"K"结点的其他结点均是"K"的祖先结点(A、B、E),而反过来,K则是A、B、E的子孙结点。
双亲结点和孩子结点:
以"K"为例,离"K"结点最近的祖先结点,即"E"结点("K"的前驱结点)是"K"结点的双亲结点,而反过来,"K"则是"E"的孩子结点
。
兄弟结点:
多个拥有相同的双亲结点的孩子结点,互为兄弟结点
。以"E"作为双亲结点为例,"K"和"L"结点互为兄弟结点。
度:
树中一个结点的子结点的个数称为该结点的度
。而树中最大度数称为树的度。
分支结点和叶子结点:
度大于0的结点称为分支结点;度大于0的结点称为叶子结点
。如图所示,红色结点为分支结点,而绿色结点为叶子结点。
结点的层次和高度、深度:
结点的层次如图所示:
结点的高度:
从叶子结点开始,自底向上逐层累加,以"B"结点为例,高度为3。
结点的深度:
从根结点开始,自顶向下逐层累加,以"B"结点为例,深度为2。树的高度(深度)是树中结点的最大层数。
有序树和无序树:
如果交换B,D结点位置后,左右两颗树的次序不受影响则为无序树;受影响则为有序树。
有序树是从左到右每一个子树是有次序的,而无序树是无次序的
。
路径:
树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成
(如图:ABE)的。树中的分支是有向的,即从双亲结点指向孩子结点,所以路径一定是自上而下的
。
路径长度:
路径上所经历边的个数。
森林:
m(m≥0)棵互不相交的树的集合。
树的性质:
1)树中的结点数等于所有结点的度数加1。
2)度为m的子树中第i层上至少有mi-1个结点(i≥1)。
3)高度为h的m叉树至多有(mh -1)/(m-1)个结点。
4)具有n个结点的m叉树的最小高度为[ log m ( n ( m − 1 ) + 1 ) \log _{\mathbf{m}}\left( \mathbf{n}\left( \mathbf{m}-1 \right) +1 \right) logm(n(m−1)+1)]。
5)设非空二叉树中度为0、1和2的结点个数分别为n0、 n1和n2,则n0=n2+1(叶子结点比二分支结点多一个) 。
6)若完全二叉树有2k个(偶数)个结点,则必有n1=1, n0=k, n2=k-1若完全二叉树2k-1个(奇数)个结点,则必有n1=0, n0=k, n2=k-1。
几种特殊的二叉树
满二叉树:
一棵高度为h,且含有2h-1个结点的二叉树。
特点:
①只有最后一层有叶子结点。
②不存在度为1的结点。
③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父节点为[i/2] (如果有的话) 。
完全二叉树:
当且仅当其每个结点都与高度为h满二义树中编号为1-n的结点一一对应时,称为完全二叉树。
特点:
①只有最后两层可能有叶子结点
。
②最多只有一个度为1的结点。
③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父节点为[i/2] (如果有的话) 。如果一个结点有一个孩子,那么这个孩子一定是左孩子,而不是右孩子。
④i≤[n/2]为分支结点,i>[n/2]为叶子结点。
二叉排序树:
一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
左子树上所有结点的关键字均小于根结点的关键字
;
右子树上所有结点的关键字均大于根结点的关键字
。
平衡二叉树:
树上任一结点的左子树和右子树的深度之差不超过1。
②二叉树的遍历操作和线索二叉树
二叉树的遍历
按某条搜索路径访问树中的毎个结点,树的毎个结点均被访问次,而且只访问一次。
先序遍历:
先访问根,再访问左子树,最后访问右子树。
先序遍历的算法:
若二叉树非空:
1)访问根结点
2)先序遍历左子树
3)先序遍历右子树
void Preorder(BItree T){
if(T!=NULL){
visit(T);
Preoder(T->lchild);
Preoder(T->rchild);
}
}
中序遍历:
先访问左子树,再访问根,最后访问右子树。
中序遍历的算法:
若二叉树非空:
1)中序遍历左子树
2)访问根结点
3)中序遍历右子树
void Inorder(BItree T){
if(T!=NULL){
Inoder(T->lchild);
visit(T);
Inoder(T->rchild);
}
}
后序遍历:
先访问左子树,再访问右子树,最后访问根。
后序遍历的算法:
若二叉树非空:
1)后序遍历左子树
2)后序遍历右子树
3)访问根结点
void Postorder(BItree T){
if(T!=NULL){
Postorder(T->lchild);
Postorder(T->rchild);
visit(T);
}
}
非递归的中序遍历:
算法思想:
1)初始时依次扫描
根结点的所有左侧结点井将它们—进栈;
2)出栈一个结点,访问
它;
3)扫描该结点的右孩子结点并将其进栈;
4)依次扫描右孩子结点的所有左侧结点并一一进栈;
5)反复该过程直到栈空为止。
void Inorder_stack(Bitree T){
Initstack(S);Bitree p=T;
while(p||!IsEmpty(S)){
if(p){
Push(S,p);
p=p->lchild;
}
else{
Pop(S,p);visit(p);
p=p->rchild;
}
}
}
【注意】:扫描只是经过该结点,而访问是访问该结点的内容。
层次遍历:
从上至下,从左到右访问。
层次遍历的算法:
1)初始将根入队并访问根结点,然后出队;
2)若有左子树,则将左子树的根入队;
3)若有右子树,则将右子树的根入队;
4)然后出队一个结点,访问该结点;
5)反复该过程直到栈空为止。
//二叉树的结点(链式存储)
typedef sturct BiNode{
Elemtype data;
struct BiNode *lchild,*rchild;
} Binode,*Bitree;
//链式队列结点
typedef struct LinkNode{
BiNode *data;
struct LinkNode *next;
}LinkNode;
typedef struct{
LinkNode *front,*rear;//队头队尾
}LinkNode;
//层次遍历
void Levelorder(Bitree T){
InitQueue(Q);
Bitree T;
EnQueue(Q,T);
while(!IsEmpty(Q)){
DeQueue(Q,p);
if(p->lchild!=NULL){
EnQueue(Q,p->child);
}
if(p->rchild!=NULL){
EnQueue(Q,p->rchild);
}
}
}
使用遍历序列构造二叉树
★(后)先序遍历序列和中序遍历序列可以确定棵二叉树
,而后序遍历序列和先序遍历序列不可以确定一棵二叉树。
中序遍历序列和先序遍历序列
1)在先序序列中,第一个节点是根结点;
2)根结点将中序遍历序列划分为两部分;
3)然后在先序序列中确定两部分的结点,并且两部分的第一个结点分别为左子树的根和右子树的根;
4)在子树中递归重复该过程,便能唯一确定一棵二叉树。
线索二叉树
线索化的概念:
若无左子树,则将左指针指向其前驱结点;若无右子树,则将右指针指向其后继结点。
线索:线索就是子树空的时候指向的前驱或者后继 。
线索二叉树结点结构:
标志域 l ( r ) t a g { 0 , l ( r ) c h i l d 域指 示 结点的左 ( 右 ) 孩子 1 , l ( r ) c h i l d 域指 示 结点的前 ( 后 ) 继 \text{标志域}\mathbf{l}\left( \mathbf{r} \right) \mathbf{tag}\begin{cases} 0,\mathbf{l}\left( \mathbf{r} \right) \mathbf{child}\text{域指}示\text{结点的左}\left( \text{右} \right) \text{孩子}\\ 1,\mathbf{l}\left( \mathbf{r} \right) \mathbf{child}\text{域指}示\text{结点的前}\left( \text{后} \right) \text{继}\\ \end{cases} 标志域l(r)tag{0,l(r)child域指示结点的左(右)孩子1,l(r)child域指示结点的前(后)继
typedef struct ThreadNode{
Elemtype data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
这种结点结构构成的二叉链表作为二叉树的存储结构,称为线索链表
。
线索二叉树的对比:
中序线索二叉树 | 先序线索二叉树 | 后序线索二叉树 | |
---|---|---|---|
找前驱 | √ | × | √ |
找后继 | √ | √ | × |
中序线索二叉树的前驱结点和后继结点:
前驱结点
若左指针为线索,则其指向结点为前驱结点
若左指针为左孩子,则其左子树的最右侧结点为前驱结点
后驱结点
若右指针为线索,则其指向结点为后驱结点
若右指针为右孩子,则其右子树的最左侧结点为后驱结点
中序线索二叉树线索化的实现:
//全局变量pre,指向当前访问结点的前驱
ThreadNode *pre=NULL;
//线索二叉树
typedef struct ThreadNode{
Elemtype data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;//左、右线索标志
}ThreadNode,*ThreadTree;
void visit(ThreadNode *q){
if(q->lchild==NULL){//左子树为空,建立前驱结点
q->lchild=pre;
q->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=q;//建立前驱结点的后继线索
pre->rtag=1;
}
pre=q;
}
//中序遍历二叉树,一边遍历一边线索化
void Inthread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild);//中序遍历左子树
visit(T);//访问根结点
InThread(T->rchild);//中序遍历右子树
}
}
void CreateInthread(ThreadTree T){
pre=NULL;//pre初始化为NULL
if(T!=NULL){//二叉树非空才能线索化
InTread(T);
if(pre->rchild==NULL)//最后检查pre->rchild是否为NULL,如果是rtag=1;
pre->rtag=1; //处理最后一个结点
}
}
最终的中序线索遍历结果如下图所示:
【注意】:tag=0,表示指向孩子;tag=1表示指向“线索”
。
参考博客:线索二叉树原理及前序、中序线索化
中序线索二叉树的遍历:
ThreadNode *Firstnode(ThreadNode *p){
while(p->ltag==0)
p=p->lchild;
return p;
}//寻找前驱结点
ThreadNode *Nextnode(ThreadNode *p){
if(p->rtag == 0)
return Firstnode(p->rchild);
else
return p->rchild;
}//寻找后继结点
void Inorder(ThreadNode *T){
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p);
}//遍历所有中序线索二叉树的结点
③树和二叉树的存储结构
树的存储结构
双亲表示法
采用一组连续的存储空间来存储毎个结点,同时在毎个节点中增设一个伪指针指示双亲结点在数组中的位置。根结点的下标为0,其伪指针域为-1。
双亲表示法的结构体
#define Max_Tree_size 100
typedef struct{
Elemtype data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[Max_Tree_size];
int n;
}PTree;
孩子表示法
将毎个结点的孩子结点都用单链表连接起来形成一个线性结构,n个结点具有n个孩子链表。
struct CTnode{
int child;//孩子结点在数组中的位置
struct CTnode *next;//下个孩子
};
typedef struct{
Elemtype data;
struct CTnode *firstchild;//第一个孩子
}CTbox;
typedef struct{
CTbox nodes[Max_Tree_size];
int n,r;//结点数和根的位置
}CTree;
孩子兄弟表示法
以二叉链表作为树的存储结构,又称二叉树表示法。
孩子兄弟表示法的结构体
typedef struct CSNode{
Elemtype data;//数据域
struct CSNode *firstchild,*nextsibling;//第一个孩子和右兄弟指针
}CSNode,*CSTree;
下图为孩子兄弟表示法的示例:
树的存储结构表示法的对比
优点 | 缺点 | |
---|---|---|
双亲表示法 | 寻找结点的双亲结点效率高 | 寻找结点的孩子结点效率低 |
孩子表示法 | 寻找结点的孩子结点效率高 | 寻找结点的双亲结点效率低 |
孩子兄弟表示法 | 寻找结点的孩子效率高,方便实现树转换为二叉树 | 寻找结点的双亲结点效率低 |
树、森林与二叉树的转换
树与二叉树的转换
【规则】:每一个结点指针指向它的第一个孩子结点,右指针指向它在树中相邻兄弟结点。
森林与二叉树的转换
【规则】:将每一棵树转换为二又树,将每棵二又树的根依次作为上一棵二又树的右子树。
④二叉排序树
二叉排序树的定义
二叉排序树,又称二叉查找树(BST, Binary Search Tree
)一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:
左子树上所有结点的关键字均小于根结点的关键字;
右子树上所有结点的关键字均大于根结点的关键字。
左子树和右子树又各是一棵二叉排序树。
二叉排序树的查找
算法:
若树非空, 目标值与根结点的值比较:若相等,则查找成功。
若小于根结点,则在左子树上查找,否则在右子树上查找。
查找成功,返回结点指针;查找失败返回NUL。
//二叉排序树结点
typedef struct BSTNodet{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode, *BSTree;
//在二叉排序树中查找值为key的结点
BSTNode *BST_Search (BSTree T,int key){
while(T!=NULL&&key!=T->key){
if(key<T->key)T=T->child;
else
T=T->child;
}
return T;
}
//最坏时间复杂度:o(1)
二叉排序树的插入
若树非空, 目标值与根结点的值比较:若相等,则查找成功。
若小于根结点,则在左子树上查找,否则在右子树上查找。
查找成功,返回结点指针;查找失败返回NUL。
int BST_Insert(BSTree &T,int k){
if(T==NULL){//原树为空,新插入的结点为根结点
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1;//返回1,表示插入成功
}
else if(k==T->key)//树中存在相同关键字的结点,取消插入返回0
return 0;
else if(k<T->key)//插入到T的左子树
return BST_Insert(T->lchild,k);
else //插入到T的右子树
return BST_Insert(T->rchild,k);
}
二叉排序树的构造
//按照str[]中的关键字序列建立二叉排序树
void Creat_BST (BSTree &T,int str[],int n){
T=NULL;//初始时T为空树
int i=0;
while(i<n){ //依次将每个关键字插入到二叉排序树中
BST_Insert(T,str[i]);
i++;
}
}
二叉排序树的删除
先搜索找到目标结点(左子树结点值<根结点值<右子树结点):
①若被删除结点z是叶结点,则直接删除
,不会破坏二叉排序树的性质。
②若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置
。
③若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱)
,这样就转换成了第一或第二种情况。(解释:选择删除结点左子树中的最大结点替换该结点,或者选择删除它的右子树的最小结点来替换该结点。)
查找效率分析
查找长度–在查找运算中,需要对比关键字的次数称为查找长度
,反映了查找操作时间复杂度。
查找成功的平均查找长度ASL(Average Search Length):
上图:ASL= (1 * 1+2 * 2+3 * 4+4 * 1)/8= 2.625
上图:ASL= (1 * 1+2 * 2+3 * 1+4 * 1+5 * 1+6 * 1+7 * 1)/8= 3.75
查找失败的平均查找长度ASL(Average Search Length):
上图:ASL=(3 * 7+4 * 2)/9=3.22
上图:ASL=(2 * 3+3 * 1+4 * 1+5 * 1+6 * 1+7*2)/9=4.22
⑤平衡二叉树
平衡二叉树的定义
平衡二叉树(Balanced Binary Tree),简称平衡树(AVL树)–树上任一结点的左子树和右子树的高度之差不超过1。
结点的平衡因子=左子树的高-右子树的高
,如下图所示:
查找效率分析
平衡二叉树–树上任一结点的左子树和右子树的高度之差不超过1。
假设以n ~h~表示深度为h的平衡树中含有的最少结点数
。则有n o=0, n1=1, n 2=2,并且有n h=n h-1+ n h-2+1。
调整最小不平衡子树
LL:在A的左孩子的左子树中插入导致不平衡
RR:在A的右孩子的右子树中插入导致不平衡
LR:在A的左孩子的右子树中插入导致不平衡
RL:在A的右孩子的左子树中插入导致不平衡
LL:
RR:
LR:
RL:
⑥二叉树的应用–哈夫曼树
带权路径长度:
结点的权:有某种现实含义的数值(如:表示结点的重要性等) 。
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积。
树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL, Weighted Path Length) 。
W
P
L
=
∑
i
=
1
n
w
i
l
i
\mathbf{WPL}=\sum_{\mathbf{i}=1}^{\mathbf{n}}{\mathbf{w}_{\mathbf{i}}\mathbf{l}_{\mathbf{i}}}
WPL=i=1∑nwili
哈夫曼树的构造:
给定n个权值分别为w, w…w,结点,构造哈夫曼树的算法描述如下:
1)将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2)构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和
。
3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中
。
4)重复步骤2)和3) ,直至F中只剩下一棵树为止。
1)每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
2)哈夫曼树的结点总数为2n-1。
3)哈夫曼树中不存在度为1的结点。
4)哈夫曼树并不唯一,但WPL必然相同且为最优
。