第六章:树(知识点汇总)
6.1 树的定义(逻辑关系一对多)
由n(n>=0)个结点构成的有限集。
n =0 成为空树,
n>1 其余结点可以分为m(m>0)个互不相交的有限集T1,T2,……Tn
其中每个集合本身又是一棵树。并且成为根的子树(SubTree)
根结点只有一个。
子树一定是互补相交的(体现一对多的关系)
6.2 结点的分类
树的结点包含一个数据元素及若干指向其子树支的分支。
性质属性:
- 度
结点拥有子树的数目叫做该结点的度(Degree)其中度为0的叫做叶结点或者终端结点(Leaf);度不为0,叫做分支结点或者非终端结点,其中,除根结点以外的分支结点,又叫内部结点。
树的度是指所有的结点中度的最大值。 - 结点的关系
树的根节点只有一个;结点的子树叫做该结点的孩子结点,反过来,该结点是子树的双亲(Parent),双亲只有一个,同一个双亲的结点叫做兄弟结点(Sibling);以某个结点为根的子树中任一个结点都叫做该结点的子孙 - 树的层次
结点的层次叫做(Level);从根结点开始,根为一层,根的孩子又是一层
双亲在同一层的结点叫做堂兄弟 - 树的深度
树中,结点的最大层次叫做树的深度(Depth)或者高度;如果将树中的各结点的各子树看出是从左向右有次序,不能互换。则称该树是有序的否则是无序树 - 森林
森林就是由m(m>=0)的互不相交的树的集合。一颗树也是森林。
注意: 线性结构第一个元素是无前驱的,最后一个元素无后继,中间元素 一个前驱一个后继
树:根节点唯一,无双亲
叶结点可以多个,无孩子
中间结点只能一个双亲,可以多个孩子。
6.3 树的抽象类型的定义
ADT 树(tree)
Data
树是由一个结点和若干子树构成,树的结点具有相同的数据类型及层次关系
Operation
基本的树的操作:
构建,遍历,清空,取值等
endADT
6.4 树的存储结构
首先由于树的结点孩子结点很多,关系也很多
所有简单的顺序存储结构是无法满足树的实现
我们必须根据我们的目的,来设计对应的树的结构
常见的三种方法:1,双亲表示法;2,孩子表示法;3,孩子兄弟表示法
6.5 二叉树的定义
二叉树(Binary Tree)树的度的最大值是2的树叫做二叉树。
二叉树的定义使用递归的方式定义的,实现遍历的算法也是采用递归。
注意:
1. 每个结点最多只有两个子树,没有子树或者一颗子树都算二叉树
2. 左子树与右子树必须是有顺序的,次序不能颠倒。
3. 即使树的某个结点只有一颗子树也要区分左右子树
二叉树的五种形态:
1. 空二叉树
2. 只有一个根节点的结点数1的二叉树
3. 根结点只有左子树
4. 根结点只有右子树
5. 根结点左右子树都有
注意三个结点的树不区分左右,仅二叉树分析形态时要分左右。
特殊的二叉树
斜树:所有的结点都只有左子树——左斜树 或者都只有右子树——右斜树(其实就是线性表的结构)
满二叉树(完美二叉树)
所有的分支结点都有左右子树,且叶结点在同一层!
特点:叶子只能在最后一层
非叶子结点度只能是2!
同样深度的二叉树,满二叉树的结点个数最大,叶子最多
完全二叉树:
对一颗具有n个结点的二叉树,按照层次编号,
若编号为i的结点与同深度的满二叉树中编号为i的结点位置完全相同
则这棵树叫做完全二叉树
注意:满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
其次完全二叉树所有的结点与同样深度的满二叉树,它们按照
层序编号相同的结点是一一对应的,
注意按层序编号,即使缺了子树,我们也必须编号增加,空挡挂号
性质:叶子只能出现在最小面两层
最下面的叶子一定集中在左部连续
倒数第二层,若有叶子结点,一定都在右部连续
如果结点度为1,则该结点只有左孩子
同样结点数的二叉树,完全二叉树深度最小
6.6 二叉树的性质(5点)
1.在二叉树上第i层结点数最大为2^i-1;
2.深度为k的二叉树,结点的总数最多为2^k-1;
3.对于任意一颗二叉树,若终端结点数为n0,度为2的结点是n2
则n0=n2+1(容易,列出表达式可以推)
4.具有n个结点的完全二叉树深度为[log2 (n)]+1
5.如果对一颗n个结点的完全二叉树按层编号,任意的结点i
如果i=1,则i是二叉树的根节点,如果i>1,则其双亲是结点[i/2]
如果i*2>n,则节点i无左孩子(结点i是叶子结点);否则其左孩子是结点2i;
如果2*i+1>n,则结点i无右结点;否则其右孩子是结点2*i+1;
猜数字游戏就用到了二叉树
6.7 二叉树的存储结构
因为二叉树的特殊,使得我们可以使用顺序存储结构(一般只用于完全二叉树)
但是为了避免对空间的浪费(指针域很多为空,非完美二叉树时)
可以使用二叉链表结构,设计结点三部分,分左右孩子域,加一data
按照自己的需求可以添加一些域,组成类似于循环双链表的结构
6.8 二叉树的遍历
遍历是指从根结点出发,按照某种次序依次访问二叉树中的所有的结点,使得每个结点被访问一次
且仅被访问一次。
四种遍历二叉树的方法
1:前序遍历
2:中序遍历
3:后序遍历
4:层序遍历
以上四种办法都是把树中的结点变成某种意义的线性序列
对应的算法设计:
二叉树使用了二叉链表结构存储在内存中,
前序遍历代码
void PreOrderTraverse(BiTree T){
if(T==NULL){
return ;
}
printf("%c",T->data);
PreOrderTraverse(T->lchild);//先序遍历左子树
PreOrderTraverse(T->rchild);//先序遍历右子树
return ;
}
中序遍历算法
void InOrderTraverse(BiTree T){
if(T==NULL){
return ;
}
InOrderTraverse(T->lchild);//中序遍历左子树
printf("%c",T->data);
InOrderTraverse(T->rchild);//中序遍历右子树
return ;
}
后续遍历
void PosOrderTraverse(BiTree T){
if(T==NULL){
return ;
}
PosOrderTraverse(T->lchild);//后序遍历左子树
PosOrderTraverse(T->rchild);//后序遍历右子树
printf("%c",T->data);
return ;
}
注意
给出二叉树的前序遍历,后序遍历,或者中序遍历其二,叫我们推导另外一个遍历序列时,一些结论:
已经知道前序遍历与中序遍历可以唯一确定一颗二叉树
已经知道后序遍历与中序遍历可以唯一确定一棵二叉树
知道后序遍历和前序遍历 无法唯一确定一颗二叉树
6.9:二叉树的建立,扩展二叉树
我们使用’#'表示字符型数据的树,其结点是空结点
void CreateBiTree(BiTree T){
TElemTpye ch;
scanf("%c",&ch);
if(ch=='#'){
*T = NULL;
}
else{
*T = (BiTree)malloc(sizeof(BiNode));
if(!*T)
exit(OVERFLOW);//分配内存失败
(*T)->data=ch;//输入结点的数据,生成结点
CreateBiTree(&(*T)->lchild);//创建左子树
CreateBiTree(&(*T)->rchild);//创建右子树
}
}
/* 观察可知生成二叉树使用了递归原理,
就像遍历一样,我们把操作变成了输入数据而已,且每次我们都生成一个子树
(所以我们生成树的方法就可以将遍历的方法稍微修改,所以生成二叉树的方法也就由中序后序前序生成。)
*/
6.1 线索二叉树
定义:Threaded Binary Tree
简单点说就是为了知道一个结点的前后关系
我们对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化。
我们必须知道当前的rchild,lchild是指向后继结点还是指向它的孩子呢?
做法就是添加标志域Ltag,rtag布尔型,标记是否是前驱/后继结点
线索二叉树的实现
typedef enum{
Link,Thread
}PointerTag;//Link=0表示指向左右孩子指针,Thread==1表示指向前驱后继的线索
typedef struct BiThrNode{//定义线索二叉树
TElemTpye data;
struct BiThrNode *lchild,*rchild;
PointerTag LTag;
PointerTag RTag;//标记域
}SiThrNode,*BiThrTree
/* 线索化的实质就是将二叉链表的空指针修改为指向前驱后继的线索
线索化的过程就是在遍历的过程修改控制真的过程
中序遍历线索化过程如下: */
void InTreading(BiThrTree p)
{
if(p)
{
InTreading(P->lchild);//左子树线索化
if(!p->lchild){//如果左指针空
p->LTag=Thread;
p->lchild=pre;//左孩子的指针指向前驱;
}
if(!pre->rchild)//因此时的p结点的后继还没访问到只能判断pre的右孩子,空p就是pre的后继
pre->RTag =Thread;//后继线索
pre->rchild = p;//右孩子指针指向后继
}
pre = p;//注意把当前的结点p赋值给pre
InTreading(p->rchild);//递归右孩子线索化
}
发现了吗!!!该代码与中序遍历的递归代码几乎一致,只不过将打印改为了很多判断if,实现线索化的功能
有了线索二叉树,对它进行遍历就相当于操作一个双向链表
与双向链表的结构一样,我们可以在根结点上面添加一个头结点
并使头结点的lchild指向二叉树根结点,rchild指向中序遍历的最后一个结点
同时我们让中序遍历的第一个结点的lchild指向该头结点,最后一个结点的rchild指向该头结点
代码如下:
Status InOrderTraverse_Thr(BiThrTree T){
BiThrTree p;
p = T->lchild;//p指向根节点开始遍历
while(p!=T)//当是空树或者遍历结束,有p==T
{
while(p->LTag==Link)
p = p->lchild;//当LTag==0循环到了中序序列的第一个结点
printf("%c",p->data);
while(p->RTag==Thread && p->rchild!-T)//说明当前的p没有右孩子,右孩子指针指向的是后继,然后循环
{
p = p->rchild;
printf("%c",p->data);
}
p = p->rchild;//p进入右子树,继续循环
}
return OK;
}
因为我们这里的线索二叉树是双向链表的类型结构,所以访问遍历的时间复杂度与双向链表的扫描一样,都是O(n)
6.11 树与二叉树,森林之间的转换
最后我们来谈谈树与森林的遍历
树的遍历分两种形式;
1:先根遍历树,递归
2:后根遍历树,递归
森林的遍历:
1:前序遍历
2:后序遍历
我们发现;
当以二叉链表作为树的存储结构时,树的先根遍历与后根遍历完全可以借用二叉树的前序遍历与中序遍历实现
6.12 哈夫曼树(赫夫曼树、最优树)
美国数学家赫夫曼1952年发明了哈夫曼编码,为了纪念他,我们把
他在编码中用到的特殊的二叉树叫做哈夫曼树,编码方法也叫哈夫曼编码
例如:在英文中,e的出现机率最高,而z的出现概率则最低。
当利用哈夫曼编码对一篇英文进行压缩时,e极有可能用一个比特来表示,
而z则可能花去25个比特(不是26)。用普通的表示方法时,
每个英文字母均占用一个字节,即8个比特。二者相比,e使用了一般编码的1/8的长度,
z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较准确的估算,
就可以大幅度提高无损压缩的比例。)
问题是我们如何得到哈夫曼树:
第一:是先把有权值的叶子结点排序,从小到大,
如A5,B11
第二:是取头两个最小权值的结点作为一个新的结点N1的两个子节点
其中,较小是左孩子。新结点的权值是这两个结点权值之和
第三:将新的结点N1替换掉排序的原先两个结点,然后重复第二步骤
典型的比较嘿嘿!!!!!!!!!
优化以后:
哈曼夫编码
注意编码的时候编码规则必须记住,因为解压的时候要使用相同的规则去解码
记录自己的点点滴滴!加油!