文章目录
7. 树
7.1 树的逻辑结构
树的类型定义
数据对象D:
D是具有相同特性的数据元素的集合
数据关系R:
若D 为空集,则称为空树,否则:
(1)在D中存在唯一的称为根的数据元素root。
(2)当n>=1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,...Tm,其中每一棵子集本身又是一棵符合本定义的树,称为根root的子树。
- 查找类的操作:
Root(T)//求树的根结点
Value(T,cur_e)//求当前结点的元素值
Parent(T,cur_e)//求当前结点的双亲结点
LeftChild(T,cur_e)//求当前结点的最左孩子
RightSibling(T,cur_e)//求当前结点的最右兄弟
TreeEmpty(T)//判定树是否为空树
TreeDepth(T)//求树的深度
TraverseTree(T,visit())//遍历
- 插入类的操作
InitTree(&T)//初始化置空树
CreateTree(&T,definition)//按定义构造树
Assign(T,cur_e,value)//给当前结点赋值
InsertChild(&T,&p,i,c)//将以c为根的树插入为结点p的第i棵子树
- 删除类的操作
ClearTree(&T)//将树清空
DestroyTree(&T)//销毁树的结果
DeleteChild(&T,&p,i)//删除结点p的第i棵子树
- 树型结构和线性结构的比较
线性结构 | 树型结构 |
---|---|
第一个数据元素(无前驱) | 根结点(无前驱) |
最后一个数据元素(无后继) | 多个叶子结点(无后继) |
其它数据元素(一个前驱,一个后继) | 其它数据元素(一个前驱,多个后继) |
7.1.1 例题
- 树一定有唯一的根结点(X)
- 树的根结点有若干棵子树,则除树的根结点外的任一结点只能属于一棵子树(√)
- 树中结点最多只能有一个前驱,但可能有多个后继(√)
7.2 树的基本术语
-
结点:数据元素+若干指向子树的分支。
-
结点的度:分支的个数。
-
树的度:树中所有结点的度的最大值
-
叶子结点:度为零的结点
-
分支结点:度大于零的结点
-
有向树:
(1)有确定的根
(2)树根和子树根为有向关系
- 有序树:
子树之间存在确定的次序关系
- 无序树:
子树之间不存在确定的次序关系
- 结点的层次:
假设根结点的层次为1,第l层的结点的子树根结点的层次为l+1
- 树的深度:
树中叶子结点所在的最大层次
- 森林:m棵互不相交的树的集合。
任何一棵非空树是一个二元组:Tree=(root,F)
其中:root被称为根结点。
F被称为子树森林。
7.2.1 例题
-
树的度:3
n 0 n_0 n0=6
n 1 n_1 n1=2
n 2 n_2 n2=1
n 3 n_3 n3=2
n n n=11
- 结点F的祖先结点包括:E,C,J
- 结点G 的子孙结点包括:D,H,I,B
- 从结点J到结点H的路径包括:结点J,G,D,H;路径:JG,GD,DH
- 树的深度为:4
- 非叶子/非终端结点包括J,C,G,E,D,内部结点包括C,G,E,D
- 树中每个结点有唯一的双亲结点,根结点除外(x)
7.3 二叉树的性质
7.3.1 性质1
在二叉树的第i层上至多有 $ 2^{i-1} $个结点
用归纳法证明:
归纳基:i = 1 层时,只有一个根结点: 2 i − 1 = 2 0 = 1 2^{i-1}=2^0=1 2i−1=20=1
归纳假设:假设第 i -1层的结点数 = 2 i − 2 2^{i-2} 2i−2;
归纳证明:二叉树上每个结点至多有两棵子树,
则第 i 层的结点数 = 2 i − 2 × 2 = 2 i − 1 2^{i-2}\times2=2^{i-1} 2i−2×2=2i−1
7.3.2 性质2
深度为$ k $的二叉树至多含 2 k − 1 2^{k}-1 2k−1个结点(k>=1)
证明:
基于性质1,深度为 k 的二叉树上的结点数至多为
2 0 + 2 1 + . . . 2 k − 1 = 2 k − 1 2^0+2^1+...2^{k-1}=2^k-1 20+21+...2k−1=2k−1
7.3.3 性质3
对于任何一棵二叉树,若它含有 n 0 n_0 n0个叶子结点, n 2 n_2 n2个度为2的结点,则必存在关系式: n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1。
对于任何一棵二叉树,若它含有n0 个叶子结点、n2 个度为 2 的结点,则必存在关系式:n0 = n2+1。
证明:
设 二叉树上结点总数 n = n0 + n1 + n2
又 二叉树上分支总数 b = n1+2*n2
而 b = n-1 = n0 + n1 + n2 - 1
因此, n0 = n2 + 1 。
7.3.4 性质4
满二叉树:指的是深度为 k k k,且含有 2 k − 1 2^k-1 2k−1个结点的二叉树。
完全二叉树:树中所含的 n n n个结点和满二叉树中编号为1至 n n n的结点相对应。
具有n个结点的完全二叉树的深度为: [ l o g 2 n ] + 1 [log_2n]+1 [log2n]+1(向下取整)
证明:
设完全二叉树的深度为 k ,
则根据第2条性质得 2 k − 1 2^{k-1} 2k−1≤ n < 2 k 2^k 2k,
即 k-1 ≤ l o g 2 ( n ) log_2(n) log2(n) < k ,
因为 k 只能是整数,因此, k = ⌊ l o g 2 ( n ) ⌋ + 1 k=\lfloor log_2(n)\rfloor +1 k=⌊log2(n)⌋+1
7.3.5 性质5
若对含n个结点的完全二叉树从上到下且从左至右进行1至n的编号,则对完全二叉树中任意一个编号为 i i i的结点:
(1)若 i = 1 i=1 i=1,则该结点是二叉树的根,无双亲,否则,编号为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋的结点为其双亲结点。
(2)若 2 i > n 2i>n 2i>n,则该结点无左孩子,否则,编号为 2 i 2i 2i的结点为其左孩子结点。
(3)若 2 i + 1 > n 2i+1>n 2i+1>n则该结点无右孩子,否则编号为 2 i + 1 2i+1 2i+1的结点为其右孩子结点。
7.3.6 例题
- 在结点个数为n(n>1)的各种形态的树中,深度最小的树有 2 层。
- 在一棵深度为6的完全二叉树中,最少可以有多少结点,最多可以有多少个结点? 最少:32 最多:63
- 在完全二叉树中,结点总数 n n n为999,求叶子结点数 n 0 n_0 n0为多少? 500
- 在完全二叉树中,结点总数为n,求叶子结点数为多少?
n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1
n = 2 n 0 + n 1 − 1 n=2n_0+n1-1 n=2n0+n1−1
在完全二叉树中, n 1 n_1 n1只能为1或0
当 n 1 n_1 n1为1时, n n n只能是偶数, n 0 = n / 2 n_0=n/2 n0=n/2
当 n 1 n_1 n1为0时, n n n只能是奇数, n 0 = ( n + 1 ) / 2 n_0=(n+1)/2 n0=(n+1)/2
所以 n 0 = ⌊ ( n + 1 ) / 2 ⌋ n_0=\lfloor(n+1)/2\rfloor n0=⌊(n+1)/2⌋ (向下取整)
7.4 二叉树的存储表示
7.4.0 二叉树的顺序存储表示
#define MAX_TREE_SIE 100
//二叉树的最大结点树
typedef TElemType SqBiTree[MAX_TREE_SIZE];
//0号单元存储根结点
SqBiTree bt;
7.4.1 二叉树的二叉链表存储表示
typedef struct BiTNode{//结点结构
TElemType data;
struct BiTNode *lchild,*rchild;//左右孩子指针
}BiTNode,*BiTree;
7.4.2二叉树的三叉链表存储表示
typedef struct TriTNode{//结点结构
TElemType data;
struct TriTNode *lchild,*rchild;//左右孩子指针
struct TriTNode *parent;//双亲指针
}TriTNode, *TriTree;
7.4.3 二叉树双亲链表存储表示
typedeg strcut BPTNode{//结点结构
TElemType data;
int *parent;//指向双亲的指针
char LRTag;//左、右孩子标志域
}BPTNode;
typedef struct BPTree{//树结构
BPTNode nodes[MAX_TREE_SIZE];
int num_node;//结点数目
int root;//根结点的数模
}BPTree;
7.4.1 例题
- 如果二叉树用二叉链表表示,有多少个空链域?
空链域:
2
n
0
+
n
1
=
n
0
+
n
0
+
n
1
=
n
2
+
1
+
n
0
+
n
1
=
n
+
1
2 n0+n1=n0+n0+n1=n2+1+n0+n1=n+1
2n0+n1=n0+n0+n1=n2+1+n0+n1=n+1
- 如果二叉树用三叉链表表示,有多少个空链域?
每个结点的双亲指针都不空,但根结点的双亲指针为空,所以有n+2个空链域。
7.5 二叉树的遍历
介绍二叉树的各种递归和非递归的遍历方法,以及如何建立二叉树的存储结构。
二叉树遍历的搜索路径
对“二叉树”而言,可以有三条搜索路径:
1.先上后下的按层次遍历;
2.先左(子树)后右(子树)的遍历;
3.先右(子树)后左(子树)的遍历。
先左后右的遍历算法
先(根)序的遍历算法
中(根)序的遍历算法
后(根)序的遍历算法
先(根)序的遍历算法
若二叉树为空树,则空操作;否则,
(1)访问根结点;
(2)先序遍历左子树;
(3)先序遍历右子树。
−
+
a
∗
b
−
c
d
/
e
f
-+a*b-cd/ef
−+a∗b−cd/ef
中(根)序的遍历算法
若二叉树为空树,则空操作;否则,
(1)中序遍历左子树;
(2)访问根结点;
(3)中序遍历右子树。
a
+
b
∗
c
−
d
−
e
/
f
a+b*c-d-e/f
a+b∗c−d−e/f
后(根)序的遍历算法
若二叉树为空树,则空操作;否则,
(1)后序遍历左子树;
(2)后序遍历右子树;
(3)访问根结点。
a
b
c
d
−
∗
+
e
f
/
−
abcd-*+ef/-
abcd−∗+ef/−
//递归
void Preorder (BiTree T, void( *visit)(TElemType &e))
{ // 先序遍历二叉树
if (T) {
visit(T->data); // 访问结点
Preorder(T->lchild, visit); // 遍历左子树
Preorder(T->rchild, visit); // 遍历右子树
}
}
//先序非递归
Status InOrderTraverse(BiTree T, Status (*Visit)(TElemType e))
{ InitStack(S); Push(S,T); //根指针进栈
while (!StackEmpty (S)) {
while(GetTop(S, p) && p) Push (S,p->lchild);
Pop(S, p); //空指针退栈
if (!StackEmpty(S)){ //访问节点,退后一步
Pop (S, p); if (!Visit(p->data)) return ERROR;
Push(S,p->rchild);
} //if
} //while
return OK;
}
//层次遍历算法的非递归描述
void translevel(BinNode *bt)
{ struct BinNode *b;
q.front=0; q.rear=0;
if (!bt) return;
q.elem[q.rear]=bt; q.rear=q.rear+1;
while (q.front < q.rear)
{ b=q.elem[q.front]; q.front=q.front+1;
printf("%c ",b->data);
if (b->lch!=0)
{ q.elem[q.rear]=b->lch;
q.rear=q.rear+1;
}
if (b->rch!=0)
{ q.elem[q.rear]=b->rch;
q.rear=q.rear+1;
}
}
}
7.5.1 例题
先序遍历:JCEMFNGDHIB
中序遍历:MFNECJHDIBG
后序遍历:NFMECHBIDGJ
- 若二叉树先序遍历的扩展序列为 A B ∗ D ∗ E C ∗ ∗ F ∗ ∗ ∗ AB*D*EC**F*** AB∗D∗EC∗∗F∗∗∗,其中*代表空链域,则二叉树的后序遍历序列为CFEDBA。
- 已知一棵二叉树的中序遍历序列为DBACFEGM,后序遍历序列为DAFGECBM,画出这棵二叉树,并给出先序遍历序列。
先序遍历序列:MBDCAEFG
7.5 二叉树遍历算法的应用
7.5.1 建立二叉树的存储结构
Status CreateBiTree(BiTree &T){
Scanf(&ch);
if(ch=='#')T=NULL;
else{
if(!(T=(BiTNode *)malloc(sizeof(BiTNode))))
exit(OVERFLOW);
T->data=ch;
CreateBiTree(T->lchild);//构造左子树
CreateBiTree(T->rchild);//构造右子树
}
return OK;
}//CreateBiTree
7.5.2 统计二叉树中叶子结点个数
算法基本思想:
先序(或中序或后序)遍历二叉树,在遍历过程中查找叶子结点,并计数。
由此,需在遍历算法中增添一个“计数”的参数,并将算法中“访问结点”的操作改为:若是叶子,则计数器增1。
void CountLeaf(BiTree T,int& count){
if(T){
if((!T->lchild)&&(!T->rchild)_
count++;
CountLeaf(T->lchild,count);
CountLeaf(T->rchild,count);
}
}
7.5.3 求二叉树的深度
算法基本思想:
首先分析二叉树的深度和它的左、右子树深度之间的关系。
从二叉树深度的定义可知,二叉树的深度应为其左、右子树深度的最大值加1。由此,需先分别求得左、右子树的深度,算法中“访问结点”的操作改为:二叉树的深度为其左、右子树深度的最大值加1。
int Depth(BiTree T){
if(!T)depthval=0;
else{
depthleft=Depth(T->lchild);
depthright=Depth(T->rchild);
depthval=1+(depthleft>depthright?depthleft:depthright);
}
return depthval;
}
7.5.4 复制二叉树(后序遍历)
//生成一个二叉树的结点
//(其数据域为item,左指针域为lptr,右指针域为rptr)
BiTNode *GetTreeNode(TElemType item, BiTNode *lptr, BiTNode *rptr )
{
if (!(T = (BiTNode*)malloc(sizeof(BiTNode))))
exit(1);
T-> data = item;
T-> lchild = lptr; T-> rchild = rptr;
return T;
}
BiTNode *CopyTree(BiTNode *T) {
if (!T ) return NULL;
if (T->lchild )
newlptr = CopyTree(T->lchild);//复制左子树
else newlptr = NULL;
if (T->rchild )
newrptr = CopyTree(T->rchild);//复制右子树
else newrptr = NULL;
newT = GetTreeNode(T->data, newlptr, newrptr);
return newT;
} // CopyTree
7.5.5 交换二叉树的左右子树
Void exchange (BinNode *T )
{ BinNode *q; //中间变量
if ( T )
{ q = T->lchild ;
T->lchild= T->rchild;
T->rchild = q;
exchange(T->lchild);
exchange(T->rchild);
}
}
7.6 线索二叉树
内容:介绍线索二叉树的特性、中序线索二叉树的遍历和构建。
先序序列:
A B C D E F G H K
指向该线性序列中的“前驱”和
“后继” 的指针,称作“线索”。
包含 “线索” 的存储结构,称作 “线索链表”。
与其相应的二叉树,称作 “线索二叉树” 。
对线索链表中结点的约定:
在二叉链表的结点中增加两个标志域,
并作如下规定:
若该结点的左子树不空,
则Lchild域的指针指向其左子树,
且左标志域的值为“指针 Link”;
否则,Lchild域的指针指向其“前驱”,
且左标志的值为“线索 Thread” 。
若该结点的右子树不空,
则rchild域的指针指向其右子树,
且右标志域的值为 “指针 Link”;
否则,rchild域的指针指向其“后继”,
且右标志的值为“线索 Thread”。
如此定义的二叉树的存储结构称作“线索链表”。
typedef enum{Link,Thread}PointerThr;
typedef struct BiThrNode{
TElemType data;
struct BiThrNode *lchild,*child;//左右指针
PointerThr LTag,RTag;//左右标志
}BiThrNode,*BiThrTree;
- 中序遍历序列CBHFAEGD
7.6.1 变量中序线索二叉树
由于在线索链表中添加了遍历中得到的“前驱”和“后继”的信息,从而简化了遍历的算法。
for ( p = firstNode(T); p; p = Succ(p) )
Visit (p);
中序遍历的第一个结点 ?
左子树上处于“最左下”(没有左子树)的结点。
在中序线索化链表中结点的后继 ?
若无右子树,则为后继线索所指结点;
否则为对其右子树进行中序遍历时访问的第一个结点。
Status InOrderTraverse_Thr(BiThrTree T, Status (*Visit)(TElemType e))
{ p = T->lchild; // p指向根结点
while (p != T) { // 空树或遍历结束时,p==T
while (p->LTag==Link) p = p->lchild; // 第一个结点
if (!Visit (p->data)) return ERROR;
while (p->RTag==Thread && p->rchild!=T) {
p = p->rchild; Visit(p->data); // 访问后继结点
}
p = p->rchild; // p进至其右子树根
}
return OK;
} // InOrderTraverse_Thr。
7.6.3 构造中序线索二叉树
void InThreading(BiThrTree p) {
if (p) { // 对以p为根的非空二叉树进行线索化
InThreading(p->lchild); // 左子树线索化
if (!p->lchild) // 建前驱线索
{ p->LTag = Thread; p->lchild = pre; }
if (!pre->rchild) // 建后继线索
{ pre->RTag = Thread; pre->rchild = p; }
pre = p; // 保持 pre 指向 p 的前驱
InThreading(p->rchild); // 右子树线索化
} // if
} // InThreading
Status InOrderThreading(BiThrTree &Thrt, BiThrTree T) // 构建中序线索链表
{ if (!(Thrt = (BiThrTree)malloc(sizeof( BiThrNode)))) exit (OVERFLOW);
Thrt->LTag = Link; Thrt->RTag =Thread; Thrt->rchild = Thrt; // 添加头结点
if (!T) Thrt->lchild = Thrt;
else {
Thrt->lchild = T; pre = Thrt;
InThreading(T);
pre->rchild = Thrt; // 处理最后一个结点
pre->RTag = Thread;
Thrt->rchild = pre;
}
return OK;
} // InOrderThreading
7.6.4 例题
- 已知二叉树,画出中序线索链表
- 引入二叉线索树的目的是©
A. 为了能方便地找到双亲
B. 为了能在二叉树中方便地进行插入和删除
C. 加快查找结点的前驱或后继的速度
D. 使二叉树的遍历结果唯一
-
在遍历中序线索二叉树时,某结点既有左子树又有右子树,那么它的前驱是其(D)
A. 右子树中最左下的结点
B. 右子树中最右下的结点
C. 左子树中最左下的结点
D. 左子树中最右下的结点
7.7 树和森林的存储结构
7.7.1 双亲表示法(树)
#define MAX_TREE_SIZE 100
typedef struct PTNode{//结点结构
Elem data;
int parent;//双亲位置域
}PTNode;
typedef struct{//树结构
PTNode nodes[MAX_TREE_SIZE];
int r,n;//根结点的位置和结点个数
}PTree;
7.7.2 孩子链表表示法(树)
typedef struct CTNode { // 孩子结点结构
int child;
struct CTNode *next;
} *ChildPtr;
typedef struct { // 双亲结点结构
Elem data;
int parent; // 双亲位置域
ChildPtr firstchild; // 孩子链的头指针
} CTBox;
typedef struct { // 树结构
CTBox nodes[MAX_TREE_SIZE];
int n, r; // 结点数和根结点的位置
} CTree;
7.7.3 孩子兄弟表示法
左链为孩子,右链为兄弟
typedef struct CSNode{
Elem data;
struct CSNode *firstchild, *nextsibling;
} CSNode, *CSTree;
树的各种操作均可由对应的二叉树的操作来完成。
和树对应的二叉树,其左、右子树的概念已改变为:
左是孩子,右是兄弟。
int TreeDegree(CSTree T)
{
int h1,h2;
if(!T) return 0;
else{
h1=TreeDegree(T->firstchildr
h2=TreeDegree(T->nextsibling);
if(T->fch&&!T->fch->nextsibling) return h1+h2+1;
else return h1+h2; ,
}
}
7.7.4树和二叉树的相互转换
7.7.4.1树转换成二叉树
兄弟相连留长子
1.加线: 在兄弟之间加一条连线
2.抹去:对于每个结点, 除了左孩子外,去除与其余孩子间的连线
3.旋转:以树的根节点为轴心,将整树顺时针转45度
7.7.4.2 二叉树转换成树
左孩右右连双亲,去掉原来右孩线
1加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子………沿分支找到的所有右孩子,都与p的双亲用线连起来
2.抹线:抹掉原二叉树中双亲与右孩子之间的连线
3.调整:将结点按层次排列,形成树结构
7.7.5 森林转换成二叉树的转换规则:
树变二叉根相连
①将各棵树分别转换成二叉树
②将每棵树的根结点用线相连
③以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
7.7.6 由二叉树转换为森林的转换规则
去掉全部右孩线,孤立二叉再还原
①抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树
②还原:将孤立的二叉树还原成树
7.8 树和森林的遍历
-
先根(次序)遍历:
若树不空,则先访问根结点,然后依次先根遍历各棵子树
-
后根(次序)遍历:
若树不空,则依次后根遍历各棵子树,然后访问根结点
-
按层次遍历
若树不空,则自上而下自左至右访问树中的每个结点
先根遍历时顶点的访问次序:
A B E F C D G H I J K
后根遍历时顶点的访问次序:
E F B C I J K H G D A
层次遍历时顶点的访问次序:
A B C D E F G H I J K
7.8.1 森林的遍历
森林有三部分构成:
- 森林中第一棵树的根结点
- 森林中第一颗树的子树森林
- 森林中其它树构成的森林
7.8.1.1 森林的先序遍历
若森林不为空,则
访问森林中第一棵树的根结点
先序遍历森林中第一棵树的子树森林
先序遍历森林中(除第一棵树之外)其余树构成的森林。
依次从左至右对森林中的每一棵树进行先根遍历。
7.8.1.2 森林的中序遍历
若森林不空,则
中序遍历森林中第一棵树的子树森林
访问森林中第一棵树的根结点
中序遍历森林中(除第一棵树之外)其余树构成的森林。
依次从左至右对森林中的每一棵树进行后根遍历。
7.8.1.3 树的遍历、森林的遍历和二叉树遍历的对应关系
森林 | 树 | 二叉树 |
---|---|---|
先序遍历 | 先根遍历 | 先序遍历 |
中序遍历 | 后根遍历 | 中序遍历 |
7.8.2 例题
- 求森林的先序和中序遍历
先序:ACEMFNGDHIB
中序:MFNECAHDIBG
- 求树的先根和后根遍历序列
先根:GCEMDHFNIB
后根:MECFHNDIBG
7.9 树和森林的算法
typedef struct CSNode{
Elem data;
strcut CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
7.9.1 建立树的存储结构
假设以二元组$(F,C) $的形式自上而下、自左而右
依次输入树的各边,建立树的孩子-兄弟链表。
“#” | “A” |
---|---|
“A” | “B” |
“A” | “C” |
“A” | “D” |
“C” | “E” |
“C” | “F” |
“F” | “G” |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mRmUv4zD-1652196080251)(C:\Users\86153\AppData\Roaming\Typora\typora-user-images\image-20210610194936114.png)]
void CreatTree(CSTree &T){
T=NULL;
for(scanf(&fa,&ch);ch!='';scanf(&fa,&ch)){
p=GetTreeNode(ch);//创建结点
EnQueue(Q,p);//指针入队列
if(fa=='#')T=p;
else{
GetHead(Q,s);//取队列头元素(指针值)
while(s->data!=fa){//查询双亲结点
DeQueue(Q,s);
GetHead(Q,s);
}
if(!(s->firstchils)){
s->firstchild=p;
r=p;
}
else{
r->nextsibling=p; //链接第一个孩子结点
r=p;//链接其他孩子结点
}
}
}
}
7.9.2 求树的深度
int TreeDepth(CSTree T){
if(!T)return 0;
else{
h1=TreeDepth(T->firstchild);
h2=TreeDepth(T->nextsibling);
return(max(h1+1,h2));
}
}//TreeDepth
7.9.3 求树中的叶子结点树
void CountLeaf(CSNode T,int &count){
if(T){
if(!T->fch)count++;
CountLeaf(T->fch,count);
CountLeaf(T->nsib,count);
}//if
}//CountLeaf
7.9.4 输出树中所有从根到叶子的路径
void OutPath(Bitree T,Stack &S){
//输出森林中所有从根到叶的路径
while(T){
Push(S,T->data);
else OutPath(T->firstchild,s);
Pop(S);
T=T->nextsibling;
}//while
}OutPath
7.10 赫夫曼树
-
路径长度:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上分支的数目称作路径长度。
-
结点的路径长度:从根结点到该结点的路径上分支的数目。
-
树的路径长度:树中每个结点的路径长度之和。
-
树的带权路径长度:树中所有叶子结点的带权路径长度之和
W P L ( T ) = Σ w k l k ( 所 有 叶 子 结 点 ) WPL(T)=\Sigma w_kl_k(所有叶子结点) WPL(T)=Σwklk(所有叶子结点) -
最优二叉树(或赫夫曼树):假设有n个权值 w 1 , w 2 , . . . w n {w_1,w_2,...w_n} w1,w2,...wn,试构造一棵有n个叶子结点的二叉树,每个叶子结点的权值为 w i w_i wi,其中带权路径长度 W P L WPL WPL最小的二叉树称作最优二叉树(或赫夫曼树)。
W
P
L
(
T
)
=
7
×
2
+
5
×
2
+
2
×
3
+
4
×
3
+
9
×
2
=
60
WPL(T)= 7\times2+5\times2+2\times3+4\times3+9\times2=60
WPL(T)=7×2+5×2+2×3+4×3+9×2=60
W P L ( T ) = 7 × 4 + 9 × 4 + 5 × 3 + 4 × 2 + 2 × 1 = 89 WPL(T)=7\times4+9\times4+5\times3+4\times2+2\times1=89 WPL(T)=7×4+9×4+5×3+4×2+2×1=89
7.10.1 赫夫曼树的特点
- 赫夫曼树没有度为1的结点
- 赫夫曼树中结点总数 n n n为 2 n 0 − 1 2n_0-1 2n0−1
证明:
n
=
n
0
+
n
1
+
n
2
=
2
n
0
−
1
n=n_0+n_1+n_2=2n_0-1
n=n0+n1+n2=2n0−1
7.10.2 例题
- 试证明具有 n 0 n_0 n0个叶子结点的哈夫曼树的分支总数为 2 ( n 0 − 1 ) 2(n_0-1) 2(n0−1)。
证明:
由于哈夫曼树是一个二叉树,而叶子结点树为 n 0 n_0 n0,
由二叉树的性质3可知度为2的结点树为 n 0 − 1 n_0-1 n0−1。
哈夫曼树的分支都是从度为2的结点发出的,所以哈夫曼树的分支总数为 2 ( n 0 − 1 ) 2(n_0-1) 2(n0−1)
7.10.3 赫夫曼树的构造
以二叉树为例:
(1)根据给定的 n 个权值 {w1, w2, …, wn},构造 n 棵二叉树的集合
F = {T1, T2, … , Tn},
其中每棵二叉树中均只含一个带权值为 wi 的根结点,其左、右子树
为空树;
(2)在 F 中选取其根结点的权值为最小的两棵二叉树,分别作为左、
右子树构造一棵新的二叉树,并置这棵新的二叉树根结点的权值
为其左、右子树根结点的权值之和;
(3)从F 中删去这两棵树,同时加入刚生成的新树;
(4)重复 (2) 和 (3) 两步,直至 F 中只含一棵树为止。
7.10.4 例题
- 用于通信的电文由字符集{a, b, c, d, e, f, g, h}中的字符构成,这8个字母在电文中出现的概率分别为{0.07, 0.19, 0.02, 0.06, 0.32, 0.03, 0.21, 0.10},为这8个字母设计哈夫曼编码。