文章目录
树的概念与定义
1.树的概念*
树是n(n≥0)个结点的有限集合T。当n=0时,称
为空树;当n>0时,该集合满足如下条 件:
(1) 其中必有一个称为根(root)的特定结点,它没
有直接前驱,但有零个或多个直接后继。
(2) 其余n-1个结点可以划分成m(m≥0)个互不相
交的有限集T1,T2,T3,…,Tm,其中Ti又是一棵树,
称为根root的子树。每棵子树的根结点有且仅有一
个直接前驱,但有零个或多个直接后继。
**
2.有关树的一些术语:**
1.结点:包含一个数据元素及若干指向其它结点的分
支信息。
2.树的度—— 一棵树中最大的结点度数
3.双亲—— 孩子结点的上层结点叫该结点的双亲
4.兄弟—— 同一双亲的孩子之间互成为兄弟
5.祖先—— 结点的祖先是从根到该结点所经分支上的所有结点
6.子孙—— 以某结点为根的子树中的任一结点都成为该结点的子孙
7.结点的层次—— 从根结点算起,根为第一层,它的孩子为第二层……
8.堂兄弟—— 其双亲在同一层的结点互称为堂兄弟。
9.深度—— 树中结点的最大层次数
10.有序树—— 如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
11.森林—— m(m0)棵互不相交的树的集合
如图:
BCD是A的孩子节点,树的深度是4
二叉树
1.二叉树的定义与基本操作
➢定义:我们把满足以下两个条件的树型结构叫做
二叉树(Binary Tree):
(1)每个结点的度都不大于2;
(2)每个结点的孩子结点次序不能任意颠倒。
➢二叉树的五种基本形态:
2.二叉树的基本操作:
(1)Initiate(bt)
(2) Create(bt)
(3) Destory(bt)
(4) Empty(bt)
(5) Root(bt)
(6) Parent(bt,x)
(7) LeftChild(bt,x)
(8) RightChild(bt,x)
(9) Traverse(bt): 遍历操作。按某个次序
依次访问二叉树中每个结点一次且仅一
次。
(10) Clear(bt)
3.二叉树的性质
性质1 在二叉树的第 i 层上至多有 2^(i-1)个 结点(i>=1)
用数学归纳法证明:
归纳基础:i=1时,有2^(i-1)=2^0=1。因为第1层上只有一个根结点,所以命题成立。
归纳假设:假设对所有的 j ( 1<=j < i ) 命题成立,即第j层上至多有 2^(j-1) 个结点,证明j=i时命题亦成立。
归纳步骤:根据归纳假设,第 i-1 层上至多有2^(i-2)个结点。由于二叉树的每个结点至多有两个孩子,故第 i 层上的结点数至多是第 i-1 层上的最大结点数的2倍。即 j=i 时,该层上至多有2×2^(i-2)=2^(i-1)个结点,故命题成立。
性质2 深度为k的二叉树至多有2^k-1个结点(k≥1)。
至多,即视之为满二叉树
证明:计算等比数列 2^0+2^1+…+2^(k-1)=2^k-1
性质3 在任意-棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则no=n2+1。
回顾 m叉树 的性质
1. 设m叉树中,度数为 i 的结点树为 Ni, 则总结点数为: N = N0 + N1 + … + Nm;
2. N = 分支数 + 1 , 1 为根结点
3. 于是 N = N0 + N1 + …+ Nm = 0*(N0) + 1*(N1) + … + m*(Nm) + 1
4. 对应于现在所讨论的二叉树,于是有 N = N0 + N1 + N2 = N1 + 2*(N2) + 1,于是等到结论 N0 = N2 + 1
详细证明
证明:因为二叉树中所有结点的度数均不大于2,所以结点总数(记为n)应等于0度结点数、1度结点(记为n1)和2度结点数之和:
n=no+n1+n2 (式子1)
另一方面,1度结点有一个孩子,2度结点有两个孩子,故二叉树中孩子结点总数是:
nl+2n2
树中只有根结点不是任何结点的孩子,故二叉树中的结点总数又可表示为:
n=n1+2n2+1 (式子2)
由式子1和式子2得到:
no=n2+1
4.两种特殊的二叉树:
满二叉树,每层结点都是满的,即每层结点都具有
最大结点数。
*完全二叉树
*
5.二叉树的存储结构
1.二叉树的存储结构有两种:顺序存储结构和链式
存储结构。
2.二叉树的结构是非线性的,每一结点最多可有
两个后继。
.顺序存储结构:是用一组连续的存储单元来存
放二叉树的数据元素 。如
链式存储结构:对于任意的二叉树来说,每个结点最多有两个孩子,一个双亲结点。
二叉树的二叉链表节点的结构
typedef struct Node
{ DataType data;
struct Node *LChild;
struct Node *RChild;
} BiTNode, *BiTree;
6.二叉树的遍历与线索化
➢二叉树的遍历:指按一定规律对二叉树中的每个
结点进行访问且仅访问一次。
➢二叉树的基本结构由根结点、左子树和右子树组成
用L、D、R分别表示遍历左子树、访问根结
点、遍历右子树,那么对二叉树的遍历顺序
就可以有:
①DLR ②LDR ③LRD
④DRL ⑤RDL ⑥RLD
➢先序、中序、后序遍历是递归定义的,即在其子
树中亦按上述规律进行遍历。
*
*三种遍历方法的递归定义
:**
◆先序遍历(DLR)操作过程:
若二叉树为空,则空操作,否则依次执行如下操
作:
(1)访问根结点;
(2)按先序遍历左子树;
(3)按先序遍历右子树。
中序遍历(LDR)操作过程
若二叉树为空,则空操作,否则依次执行如下操作:
(1)按中序遍历左子树;
(2)访问根结点;
(3)按中序遍历右子树。
后序遍历(LRD)操作过程:
若二叉树为空,则空操作,否则依次执行如下操作:
(1)按后序遍历左子树;
(2)按后序遍历右子树;
(3)访问根结点。
如下图的二叉树,其先序、中序、后序遍历的序列
为:
先序遍历: A、B、D、F、G、C、E、H 。
中序遍历: B、F、D、G、A、C、E、H 。
后序遍历: F、G、D、B、H、E、C、A 。
1) 先序遍历算法
void PreOrder(BiTree root)
/*先序遍历二叉树, root为指向二叉树(或某一子树)
根结点的指针*/
{if(root!=NULL)
{ Visit(root ->data); /*访问根结点*/
PreOrder(root ->LChild);/*先序遍历左子树*/
PreOrder(root ->RChild);/*先序遍历右子树*/
}
}
2) 中序遍历
void InOrder(BiTree root)
/*中序遍历二叉树, root为指向二叉树(或某一子树)
根结点的指针*/
{if(root!=NULL)
{
InOrder(root->LChild); /*中序遍历左子树*/
Visit(root->data); /*访问根结点*/
InOrder(root->RChild); /*中序遍历右子树*/
}
}
3) 后序遍历
void PostOrder(BiTree root)
{if(root!=NULL)
{
PostOrder(root ->LChild);/*后序遍历左子树*/
PostOrder(root ->RChild);/*后序遍历右子树*/
Visit(root ->data); /*访问根结点*/
}
}
➢以中序遍历为例来说明中序遍历二叉树的递归过程
*二叉树遍历算法应用
**1.前序遍历二叉树:**
(1)若二叉树为空,则为空操作,返回空。
(2)访问根结点。
(3)前序遍历左子树。
(4)前序遍历右子树。
a.二叉树前序遍历的递归算法:
void PreOrderTraverse(BiTree BT)
{
if(BT)
{
printf("%c",BT->data); //访问根结点
PreOrderTraverse(BT->lchild); //前序遍历左子树
PreOrderTraverse(BT->rchild); //前序遍历右子树
}
}
b.使用栈存储每个结点右子树的二叉树前序遍历的非递归算法:
(1)当树为空时,将指针p指向根结点,p为当前结点指针。
(2)先访问当前结点p,并将p压入栈S中。
(3)令p指向其左孩子。
(4)重复执行步骤(2)、(3),直到p为空为止。
(5)从栈S中弹出栈顶元素,将p指向此元素的右孩子。
(6)重复执行步骤(2)~(5),直到p为空并且栈S也为空。
(7)遍历结束。
使用栈的前序遍历的非递归算法:
void PreOrderNoRec(BiTree BT)
{
stack S;
BiTree p=BT->root;
while((NULL!=p)||!StackEmpty(S))
{
if(NULL!=p)
{
printf("%c",p->data);
Push(S,p);
p=p->lchild;
}
else
{
p=Top(S);
Pop(S);
p=p->rchild;
}
}
}
c.使用二叉链表存储的二叉树前序遍历非递归算法:
void PreOrder(pBinTreeNode pbnode)
{
pBinTreeNode stack[100];
pBinTreeNode p;
int top;
top=0;
p=pbnode;
do
{
while(p!=NULL)
{
printf("%d\n",p->data); //访问结点p
top=top+1;
stack[top]=p;
p=p->llink; //继续搜索结点p的左子树
}
if(top!=0)
{
p=stack[top];
top=top-1;
p=p->rlink; //继续搜索结点p的右子树
}
}while((top!=0)||(p!=NULL));
}
2.中序遍历二叉树:
(1)若二叉树为空,则为空操作,返回空。
(2)中序遍历左子树。
(3)访问根结点。
(4)中序遍历右子树。
a.二叉树中序遍历的递归算法:
void InOrderTraverse(BiTree BT)
{
if(BT)
{
InOrderTraverse(BT->lchild); //中序遍历左子树
printf("%c",BT->data); //访问根结点
InOrderTraverse(BT->rchild); //中序遍历右子树
}
}
b.使用栈存储的二叉树中序遍历的非递归算法:
(1)当树为空时,将指针p指向根结点,p为当前结点指针。
(2)将p压入栈S中,并令p指向其左孩子。
(3)重复执行步骤(2),直到p为空。
(4)从栈S中弹出栈顶元素,将p指向此元素。
(5)访问当前结点p,并将p指向其右孩子。
(6)重复执行步骤(2)~(5),直到p为空并且栈S也为空。
(7)遍历结束。
使用栈的中序遍历的非递归算法:
void IneOrderNoRec(BiTree BT)
{
stack S;
BiTree p=BT->root;
while((NULL!=p)||!StackEmpty(S))
{
if(NULL!=p)
{
Push(S,p);
p=p->lchild;
}
else
{
p=Top(S);
Pop(S);
printf("%c",p->data);
p=p->rchild;
}
}
}
c.使用二叉链表存储的二叉树中序遍历非递归算法:
void InOrder(pBinTreeNode pbnode)
{
pBinTreeNode stack[100];
pBinTreeNode p;
int top;
top=0;
p=pbnode;
do
{
while(p!=NULL)
{
top=top+1;
stack[top]=p; //结点p进栈
p=p->llink; //继续搜索结点p的左子树
}
if(top!=0)
{
p=stack[top]; //结点p出栈
top=top-1;
printf("%d\n",p->data); //访问结点p
p=p->rlink; //继续搜索结点p的右子树
}
}while((top!=0)||(p!=NULL));
}
3.后序遍历二叉树:
(1)若二叉树为空,则为空操作,返回空。
(2)后序遍历左子树。
(3)后序遍历右子树。
(4)访问根结点。
a.二叉树后序遍历的递归算法:
void PostOrderTraverse(BiTree BT)
{
if(BT)
{
PostOrderTraverse(BT->lchild); //后序遍历左子树
PostOrderTraverse(BT->rchild); //后序遍历右子树
printf("%c",BT->data); //访问根结点
}
}
b.使用栈存储的二叉树后序遍历的非递归算法:
算法思想:首先扫描根结点的所有左结点并入栈,然后出栈一个结点,扫描该结点的右结点并入栈,再扫描该右结点的所有左结点并入栈,当一个结点的左、右子树均被访问后再访问该结点。因为在递归算法中,左子树和右子树都进行了返回,因此为了区分这两种情况,还需要设置一个标识栈tag,当tag的栈顶元素为0时表示从左子树返回,为1表示从右子树返回。
(1)当树为空时,将指针p指向根结点,p为当前结点指针。
(2)将p压入栈S中,0压入栈tag中,并令p指向其左孩子。
(3)重复执行步骤(2),直到p为空。
(4)如果tag栈中的栈顶元素为1,跳至步骤(6)。
(5)如果tag栈中的栈顶元素为0,跳至步骤(7)。
(6)将栈S的栈顶元素弹出,并访问此结点,跳至步骤(8)。
(7)将p指向栈S的栈顶元素的右孩子。
(8)重复执行步骤(2)~(7),直到p为空并且栈S也为空。
(9)遍历结束。
使用栈的后序遍历非递归算法:
void PostOrderNoRec(BiTree BT)
{
stack S;
stack tag;
BiTree p=BT->root;
while((NULL!=p)||!StackEmpty(S))
{
while(NULL!=p)
{
Push(S,p);
Push(tag,0);
p=p->lchild;
}
if(!StackEmpty(S))
{
if(Pop(tag)==1)
{
p=Top(S);
Pop(S);
printf("%c",p->data);
Pop(tag); //栈tag要与栈S同步
}
else
{
p=Top(S);
if(!StackEmpty(S))
{
p=p->rchild;
Pop(tag);
Push(tag,1);
}
}
}
}
}
c.使用二叉链表存储的二叉树后序遍历非递归算法:
void PosOrder(pBinTreeNode pbnode)
{
pBinTreeNode stack[100]; //结点的指针栈
int count[100]; //记录结点进栈次数的数组
pBinTreeNode p;
int top;
top=0;
p=pbnode;
do
{
while(p!=NULL)
{
top=top+1;
stack[top]=p; //结点p首次进栈
count[top]=0;
p=p->llink; //继续搜索结点p的左子树
}
p=stack[top]; //结点p出栈
top=top-1;
if(count[top+1]==0)
{
top=top+1;
stack[top]=p; //结点p首次进栈
count[top]=1;
p=p->rlink; //继续搜索结点p的右子树
}
else
{
printf("%d\n",p->data); //访问结点p
p=NULL;
}
}while((top>0));
}
B 线索化二叉树:
线索化二叉树的结点结构图:
线索化二叉树的结点类型说明:
typedef struct node
{
DataType data;
struct node *lchild, *rchild; //左、右孩子指针
int ltag, rtag; //左、右线索
}TBinTNode; //结点类型
typedef TBinTNode *TBinTree;
在线索化二叉树中,一个结点是叶子结点的充分必要条件是其左、右标志均为1.
中序线索化二叉树及其对应的线索链表如下图:
(1)中序线索化二叉树的算法:
void InOrderThreading(TBinTree p)
{
if(p)
{
InOrderThreading(p->lchild); //左子树线索化
if(p->lchild)
p->ltag=0;
else
p->ltag=1;
if(p->rchild)
p->rtag=0;
else
p->rtag=1;
if(*(pre)) //若*p的前驱*pre存在
{
if(pre->rtag==1)
pre->rchild=p;
if(p->ltag==1)
p->lchild=pre;
}
pre=p; //另pre是下一访问结点的中序前驱
InOrderThreading(p->rchild); //右子树线索化
}
}
(2)在中序线索化二叉树下,结点p的后继结点有以下两种情况:
①结点p的右子树为空,那么p的右孩子指针域为右线索,直接指向结点p的后继结点。
②结点p的右子树不为空,那么根据中序遍历算法,p的后继必是其右子树中第1个遍历到的结点。
中序线索化二叉树求后继结点的算法:
TBinTNode *InOrderSuc(BiThrTree p)
{
TBinTNode *q;
if(p->rtag==1) //第①情况
return p->rchild;
else //第②情况
{
q=p->rchild;
while(q->ltag==0)
q=q->lchild;
return q;
}
}
中序线索化二叉树求前驱结点的算法:
TBinTNode *InOrderPre(BiThrTree p)
{
TBinTNode *q;
if(p->ltag==1)
return p->lchild;
else
{
q=p->lchild; //从*p的左孩子开始查找
while(q->rtag==0)
q=q->rchild;
return q;
}
}
(3)遍历中序线索化二叉树的算法
void TraversInOrderThrTree(BiThrTree p)
{
if(p)
{
while(p->ltag==0)
p=p->lchild;
while(p)
{
printf("%c",p->data);
p=InOrderSuc(p);
}
}
}
**将一棵树转换为二叉树的方法:
**
❖树中所有相邻兄弟之间加一条连线。
树转换为二叉树的例子:
❖对树中的每个结点,只保留其与第一个孩子结点
之间的连线,删去其与其它孩子结点之间的连线。
❖以树的根结点为轴心,将整棵树顺时针旋转一
定的角度,使之结构层次分明。
结论:
➢树中的任意一个结点都对应于二叉树中的一个结
点。
➢树中某结点的第一个孩子在二叉树中是相应结点
的左孩子,树中某结点的右兄弟结点在二叉树中是
相应结点的右孩子。
➢树的根结点没有兄弟,所以变换后的二叉树的根
结点的右孩子必然为空。
树与二叉树的对应关系及转换方法
哈夫曼树
基本概念:
路径:指从一个结点到另一个结点之间的分支序列。
路径长度:指从一个结点到另一个结点所经过的分支
数目。
结点的权:给树的每个结点赋予一个具有某种实际意
义的实数,我们称该实数为这个结点的权。
带权路径长度:在树形结构中,我们把从树根到某
一结点的路径长度与该结点的权的乘积,叫做该结
点的带权路径长度。
树的带权路径长度:为树中所有叶子结点的带权路
径长度之和,通常记为: WPL= wi×li
i=1
n
其中n为叶子结点的个数, wi为第i个叶子结点的
权值,li为第i个叶子结点的路径长度。
例如下图所示的具有不同带权路径长度的二叉树
WPL(a)=7×2+5×2+2×2+4×2=36
WPL(b)=4×2+7×3+5×3+2×1=46
WPL©=7×1+5×2+2×3+4×3=35
哈夫曼树
➢哈夫曼树又叫最优二叉树,它是由n个带权叶子结
点构成的所有二叉树中带权路径长度WPL最短的二
叉树。
➢
构造哈夫曼算法的步骤:
(1)用给定的n个权值{w1
,w2
, … ,wn
}对应的n个
结点构成n棵二叉树的森林F={T1
,T2
, …,Tn
},其中
每一棵二叉树Ti (1≤i≤n)都只有一个权值为wi的
根结点,其左、右子树为空。
(2)在森林F中选择两棵根结点权值最小的二叉树,
作为一棵新二叉树的左、右子树,标记新二叉树的
根结点权值为其左右子树的根结点权值之和。
(3)从F中删除被选中的那两棵二叉树,同时把新
构成的二叉树加入到森林F中。
(4)重复(2)、(3)操作,直到森林中只含有
一棵二叉树为止,此时得到的这棵二叉树就是哈夫
曼树。
例如
有一份电文中共使用了5个字符:a,b,c,d,e,它们的出现频率一次为4、7、5、2、9,试画出对应的哈夫曼树,并求出每个字母的哈夫曼编码。
27
/
11 16
/ \ /
5 6 7 9
/
2 4
a、b、c、d、e的哈夫曼编码依次为011、10、00、010、11
哈夫曼编码
➢哈夫曼树最典型的应用是在编码技术上的应用。
利用哈夫曼树,我们可以得到平均长度最短的编码。
使用变长编码虽然可以使得程序的总位数达到最
小,但机器却无法解码。
➢如对编码串0010110该怎样识别呢?
➢
**前缀编码
:任意一个编码不能成为其它任意编码的前缀。
我们可以设计出最优的前缀编码。
首先以每条指令的使用频率为权值构造哈夫曼树。
➢例如**:传送数据
state,seat,act,tea,cat,set,a,eat,如何使传
送的长度最短?
➢先看字符出现的次数,然后将出现次数当作权。
➢规定二叉树的构造为左走0,右走1
构造满足哈夫曼编码的最短最优性质:
(1)若di≠dj(字母不同),则对应的树叶不同。
因此前缀码(任一字符的编码都不是另一个字符编
码 )不同,一个路径不可能是其他路径的一部分,
所以字母之间可以完全区别。
(2)将所有字符变成二进制的哈夫曼编码,使带
权路径长度最短,相当总的通路长度最短。
哈夫曼编码算法的实现
➢由于哈夫曼树中没有度为1的结点,则一棵有n个
叶子的哈夫曼树共有2×n-1个结点,可用一个大小
为2×n-1的一维数组来存放各个结点。
➢每个结点同时还包含其双亲信息和孩子结点信息,
所以构成一个静态三叉链表。