主要内容:
5.1 树和二叉树的定义
5.2 二叉树的性质和存储结构
5.3 遍历二叉树和线索二叉树
5.4 树和森林
5.5 哈夫曼树及其应用
5.1 树和二叉树的定义
非线性结构:
至少存在一个数据元素有两个或两个以上的直接前驱(或直接后继)元素的数据结构
树形结构:
用于描述层次结构的关系,如:人类的族谱、各种社会关系、各类分类编码;操作系统的文件系统、编译程序的语法树;Internet中的DNS(域名系统)
一、树的定义
树是n(n≥0)个结点的有限集合T,它或为空树,或为非空树。
对于非空树T,满足:
1)有且仅有一个特定的称为根(root)的结点
2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集 T1,T2,….,Tm,其中每个集合本身又是一棵树,称为根的子树
1)结点:包含一个数据元素及若干指向其子树的分支
2)结点的度:结点拥有的子树数目称为结点的度
3)树的度:树中所有结点的度的最大值
A的度为3
C的度为1
E的度为0
树的度为3
4)叶子结点:度等于零的结点
5)分支结点:度大于零的结点
6)内部结点:除根结点外的分支结点
7)结点的层次:从根到该结点的层数
8)树的深度:树中叶子结点所在的最大层次
9)双亲和孩子:结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲
10)兄弟:同一双亲的孩子之间互称兄弟
11)堂兄弟:双亲位于同一层的结点互为堂兄弟
12)祖先:从根到该结点所经分支的所有结点
13)子孙:以该结点为根的子树中的任一结点
14)有序树:子树之间存在确定的次序关系
①树中结点的各子树从左到右是有次序的,不能互换
②最左边的子树的根称为第一个孩子,最右边的称为最后 一个孩子
15)无序树:子树之间不存在确定的次序关系
16)森林:
是 m(m≥0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此也可以森林和树相互递归的定义来描述树
树定义为二元组
Tree =(root,F)
其中:root是树的根结点,F是m(m≥0) 棵树的森林,F=(T1,T2,……,Tm), 其中Ti=(ri,Fi) 称作根root的第 i 棵子树
当m≠0时,树根和子树森林之间存在
关系:RF={
三、二叉树的定义
二叉树是n(n≥0)个结点的有限集合,它或为空树,或为非空树
对于非空树T,满足:
1)有且仅有一个称为根(root)的结点
2)或由一个根结点和至多两棵称为左子树和右子树的、互不相交的二叉树组成
为何要重点研究二叉树?
1)二叉树的结构最简单,规律性最强;
2)可以证明,所有树都能转为唯一对应的二叉树,不失一般性
1)二叉树或为空;
2)或由根结点和称为左子树、右子树的二叉树构成
5.2 二叉树的性质和存储结构
一、二叉树的性质
证明:归纳法证明
归纳基:i=1 时,只有一个根结点,2i-1=20=1
归纳假设:假设对所有的 j,1≤ j 归纳证明:第 i 层至多有2i-1个结点,二叉树上每个结点至多 有两棵子树,则第 i 层的结点数 = 2i-2*2=2i-1
证明:
基于上一条性质,深度为 k 的二叉树上的结点数至多为 20+21+ ……+2k-1=2k-1
证明:
设二叉树上结点总数 n=n0+n1+n2
又二叉树上分支总数 b=n1+2*n2 而 b=n-1=n0+n1+n2-1
由此: n0=n2+1
特殊的二叉树
可以对满二叉树的结点从根结点起,自上而下、自左至右进行连续编号
(2)完全二叉树:所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应
完全二叉树的特点:
1)叶子结点只可能出现在第k层或第k-1层
2)完全二叉树中度为1的结点有0个或者1个
性质5:若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至n的编号,则任意一个编号为 i 的结点:
1)若i=1,则该结点是二叉树的根,无双亲; 否则,编号为└i/2┘的结点为其双亲结点;
2)若 2i>n,则该结点无左孩子; 否则,编号为2i的结点为其左孩子结点;
3)若 2i+1>n,则该结点无右孩子结点; 否则,编号为2i+1的结点为其右孩子结点
例题:
例1:深度为k的完全二叉树中,编号最小的叶结点的编号是多少? 编号最大的叶结点编号呢?
例2:一棵有 240 个叶结点的完全二叉树,最少有多少个结点? 最多呢?
解:含240个叶结点的完全二叉树有两种形态,n=n0+n2 或 n=n0+n2+1
故:最少有479个结点,最多有480个结点
例3:有2009个结点的二叉树,什么形态叶结点最少?什么形态叶结点最多?分别为多少个?
解:单支树叶结点最少,只有1个;完全二叉树叶结点最多,参考例2:n=2n0-1 或 n= 2n0-1+1,代入n=2009,n0 =1005
一般解法:
1)二叉树深度:k=└log22009┘+1=11
2)第k层叶结点数:2009-1023=986
3)第k-1层叶结点数:210-1-986/2=19
4)叶结点总数:986+19=1005
例4:一棵完全二叉树的第7层有10个叶结点,则该完全二叉树 最多有多少个结点?最少有多少个结点?
二、二叉树的存储结构
1、顺序存储结构
1)基本思想
①用一维数组按满二叉树的编号顺序依次存储各结点,即编号为i的结点存储在下标为i-1的单元中
②一般二叉树的存储与完全二叉树相对应,不存在的结点存 储“0”或空字符
2)顺序存储
#define MAXSIZE 100 // 二叉树的最大结点数
typedef TElemType SqBiTree[MAXSIZE];
// 0号单元存储根结点
SqBiTree bt;
2、链式存储结构
1)基本思想
typedef struct BiTNode { // 结点结构
TElemType data;
struct BiTNode *lchild, *rchild; // 左右孩子指针
} BiTNode, *BiTree;
BiTree T;
//T->data, T->lchild, T->rchild
5.3 遍历二叉树和线索二叉树
一、遍历二叉树
1、遍历二叉树算法描述
顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次
“遍历”是其他操作的基础,是对二叉树进行线性化的过程, 而“访问”的含义可以很广,比如输出、修改结点等
1)先上后下的层序遍历
2)先左后右的遍历:DLR LDR LRD
3)先右后左的遍历
先序遍历二叉树
若二叉树为空树,则空操作; 否则,
1)访问根结点;
2)先序遍历左子树;
3)先序遍历右子树
void Preorder (BiTree T) {
if (T) {
cout<<T->data;
Preorder(T->lchild);
Preorder(T->rchild);
} //if
} //Preorder
中序遍历二叉树
若二叉树为空树,则空操作; 否则,
1)中序遍历左子树;
2)访问根结点;
3)中序遍历右子树
void Inorder (BiTree T) {
if (T) {
Inorder(T->lchild);
cout<<T->data;
Inorder(T->rchild);
} //if
} //Inorder
后序遍历二叉树
若二叉树为空树,则空操作; 否则,
1)后序遍历左子树;
2)访问根结点;
3)后序遍历右子树
void PostOrder (BiTree T) {
if (T) {
PostOrder(T->lchild);
PostOrder(T->rchild);
cout<<T->data;
} //if
} //Preorder
中序遍历的非递归算法
1)初始化空栈S,指针p指向根结点;
2)申请一个结点空间q,存放栈顶弹出的元素;
3)当p非空或者栈S非空时,循环执行以下操作:
①如果p非空,将p进栈,p指向该结点的左孩子;
②如果p为空,弹出栈顶元素并访问,将p指向该结点的右孩子
Status InOrderTraverse(BiTree T) {
InitStack(S);
p=T;
q=new BiTNode;
while(p||!StackEmpty(S))
{
if(p) {
Push(S,p);
p=p->lchild;
}
else {
Pop(S,p);
cout<<q->data;
p=p->rchild;
}
} //while return OK;
}//InorderTraverse
先序遍历+中序遍历或后序遍历+中序遍历可唯一确定二叉树
先序或后序序列:确定根结点
中序序列:区分左右子树
先序创建二叉树
[算法步骤]
1)扫描字符序列,读入字符ch;
2)如果ch是”#”字符,则二叉树为空,即T为NULL;否则执行:
①申请一个结点空间T
②将ch赋值给T->data
③递归创建T的左子树
④递归创建T的右子树
[算法描述]
void CreateBiTree(BiTree &T)
{
cin>>ch;
if (ch=='#')
T=NULL; //空树时
else
{
if (!(T=new BiTNode))
exit(OVERFLOW);
T->data=ch; // 先序创建二叉树
CreateBiTree(T->lchild); //构造左子树
CreateBiTree(T->rchild); // 构造右子树
}//else
} //CreateBiTree
先序遍历求结点个数
[算法步骤]
1)如果是空树,递归结束,否则执行:
2)返回根结点个数1+递归求左子树结点个数 +递归求右子树结点个数
[算法描述]
int Nodes(BiTree T)
{
if (T==NULL)
return 0;
else
return (1+Nodes(T->lchild)+Nodes(T->rchild));
}
先序遍历求叶结点个数
[算法步骤]
1)如果是空树,递归结束返回0;否则:
2)判断当前结点是否叶结点,是则递归结束返回1;
否则:
3)递归求左子树叶结点个数和右子树叶结点个数,返回其和
[算法描述]
int LeafNodes(BiTree T)
{
int num1,num2;
if (!T)
return 0;
else
if (T->lchild==NULL&&T->rchild==NULL)
return 1;
else
{
num1=LeafNodes(T->lchild);
num2=LeafNodes(T->rchild);
return (num1+num2);
}
}//LeafNodes
后序遍历求二叉树深度
[算法步骤]
1)如果是空树,递归结束返回0;否则:
2)递归求左子树深度depl;
3)递归求右子树深度depr;
4)如果depl≥depr,返回二叉树深度depl+1,否则二叉树深度为depr+1
[算法描述]
int Depth(BiTree T)
{
int depl,depr;
if (T)
{
depl=Depth(T->lchild);
depr=Depth(T->rchild);
if (depl>=depr)
return (depl+1);
else
return (depr+1);
}// if
return 0;
}// Depth
先序遍历查找值为x的结点
[算法步骤]
1)如果是空树,递归结束;否则:
2)如果根结点值为x,返回根结点指针;否则:
3)在左子树中递归查找值为x的结点,如果找到返回结点指针,
否则:
4)继续在右子树递归查找值为x的结点并返回
[算法描述]
Bitree Find(BiTree T,ElemType x)
{
BiTree p;
if (T==NULL)
return NULL;
else
if (T->data==x)
return T;
else
{
p=find(T->lchild,x);
if (p!=NULL)
return p;
else
return find(T->rchild,x);
}//else
}// Find
二、线索二叉树
1、线索二叉树的基本概念
1)遍历二叉树是指按一定规则将二叉树的结点排列成一个线性序列,实质上是对非线性结构进行线性化操作
2)以二叉链表作为存储结构时,只能得到某个结点的左右孩子,而不能直接得到结点在任一序列中的前驱或后继
3)结点的前驱或后继信息只能在遍历过程中动态获得,保存前驱或后继信息可减少遍历工作量
4)二叉树的二叉链表存储结构共有n个结点,2n个指针域,其中有n-1条分支,n+1个指针域为空。可利用这些空指针域保存部分前驱或后继信息
LTag=0, lchild指示结点左孩子
LTag=1, lchild指示结点的前驱
RTag=0, rchild指示结点右孩子
RTag=1, rchild指示结点的后继
5)如此定义的二叉树的存储结构称作“线索链表”,加上线索的 二叉树称为“线索二叉树”,对二叉树以某种次序遍历使其成为线索 二叉树的过程叫做“线索化”
线索二叉链表的表示
typedef struct BiThrNode
{
TElemType data;
struct BiThrNode *lchild, *rchild;
int LTag, RTag;
} BiThrNode, *BiThrTree;
2、构造线索二叉树
1)在遍历过程中将空指针修改为当前结点的前驱或后继线索
2)附设指针pre始终指向刚刚访问过的结点,指针p 指向当前访问的结点,由此记录访问结点的先后顺序
以结点p为根的子树中序线索化
void InThreading(BiThrTree p)
{
if (p)
{
InThreading(p->lchild); // 左子树递归线索化
if (!p->lchild)
{
p->LTag=1;
p->lchild=pre;
}
else
p->LTag=0; //pre为全局变量,初始化右孩子指针为空
if (!pre->rchild)
{
pre->RTag=1;
pre->rchild=p;
}
else
p->RTag=0;
pre=p;
InThreading(p->rchild);
}// 右子树递归线索化
} // InThreading
带头结点的二叉树中序线索化
void InOrderThreading(BiThrTree &Thrt, BiThrTree T)
{
if (!(Thrt=new BiThrNode))
exit (OVERFLOW);
Thrt->LTag=0;
Thrt->RTag=1;
Thrt->rchild=Thrt; //回指
if(!T)
Thrt->lchild=Thrt;
else
{
Thrt->lchild=T;
pre=Thrt;
InThreading(T); //中序线索化
pre->rchild=Thrt;
pre->RTag=1;
Thrt->rchild=pre;
}
}
3、遍历线索二叉树
(1)中序线索二叉树的构造
5.4 树和森林
一、树的存储结构
1、双亲表示法
用一组连续空间存储树的结点,并附设一个指示器指示其双亲结点的位置
根结点的位置:r=0
树中结点数目:n=6
双亲表示法的存储表示
数组元素的类型定义:
#define MAXSIZE 100
typedef struct PTNode
{
ElemType data;
int parent;
} PTNode;
双亲表示法的树结构:
typedef struct
{
PTNode nodes[MAXSIZE];
int r, n;
} PTree;
树结点表:用一维数组存储树中结点
孩子结点表:将某结点的所有孩子结点用单向链表串起来
孩子表示法的存储表示
孩子结点的类型定义:
typedef struct CTNode
{
int child;
struct CTNode *nextchild;
} *ChildPtr;
双亲结点的类型定义:
typedef struct
{
ElemType data;
int parent;
ChildPtr firstchild;
} CTBox;
孩子表示法的存储表示
#define MAXSIZE 100
typedef struct
{
CTBox nodes[MAXSIZE];
int r,n; // 根结点位置,结点数
} CTree;
3、孩子兄弟法
又称二叉树表示法,或二叉链表表示法
链表中结点的两个链域分别指向结点的第一个孩子结点和下一个兄弟结点
typedef struct CSNode
{
ElemType data;
struct CSNode *firstchild,*nextsibling;
} CSNode, *CSTree;
特点:
1)便于查找孩子结点
2)便于查找兄弟结点
3)作为媒介实现树向二叉树的转换
区别:
1)二叉树的指针解释为:左孩子、右孩子
2)树对应的二叉树解释为:左孩子、右兄弟
二、森林与二叉树的转换
1、树转换为二叉树
1)在树的兄弟结点之间加一水平连线
2)对每一结点,只保留它与第一个孩子结点的连线,与其它 孩子结点的连线全部抹掉
3)以树根为轴心,顺时针旋转45。
4)转化后,二叉树的右子树必为空!
2、森林转换为二叉树
[基本思想]
1)森林中的每棵树和唯一的二叉树对应
2)将森林中第i+1棵树的根结点作为第i棵树根结点的右兄弟
[转换过程]
1)在每一棵树的根结点及兄弟结点之间加一水平连线
2)对每一结点,只保留它与第一个孩子结点的连线,与其它 孩子结点的连线全部抹掉
3)以第一棵树的根为轴心,顺时针旋转45°
3、二叉树转换为树或森林
1)如果二叉树为空,则转换后的树或森林也为空
2)如果二叉树没有右子树,则转换为树;如果二叉树有右子树,则其右子树转换为下一棵子树,形成子树森林
3)对二叉树指针的解释遵循“左(第一个)孩子,右(下一个)兄弟”的原则
三、树和森林的遍历
1、树的遍历
1)先根遍历 若树不空,则先访问根结点,然后依次先根遍历各棵子树
2)后根遍历 若树不空,则先依次后根遍历各棵子树,然后访问根结点
2、森林的遍历
1)森林中第一棵树的根结点
2)森林中第一棵树的子树森林
3)森林中其它树构成的剩余森林
1)先序遍历
若森林不空,则:
①访问森林中第一棵树的根结点
②先序遍历森林中第一棵子树的子树森林
③先序遍历森林中的剩余森林
(2)中序遍历
若森林不空,则:
①中序遍历森林中第一棵树的子树森林
②访问森林中第一棵树的根结点
③中序遍历森林中的剩余森林
重要结论:
1)森林的先序遍历 =树的先根遍历”和” =二叉树的先序遍历
2)森林的中序遍历 =树的后根遍历”和” =二叉树的中序遍历
5.5 哈夫曼树及其应用
一、哈夫曼树的基本概念
1)结点的路径长度:从根结点到该结点的路径上分支的数目
2)树的路径长度:树中每个结点的路径长度之和
3)结点的带权路径长度:结点的权值和路径长度的乘积。
4)树的带权路径长度:树中所有叶结点的带权路径长度之和
5)哈夫曼树:在所有含 n 个叶子结点、并带相同权值的二叉 树中,必存在一棵其带权路径长度取最小值的二叉树,称为“最优二叉树”或“赫夫曼树”
二、哈夫曼树的构造算法
1、哈夫曼树的构造过程
1)根据给定的n个权值{w1,w2,…,wn},构成n棵二叉树的集合
F={T1,T2,…,Tn},其中每一棵二叉树Ti中只有一个带权为wi的根结 点,其左右子树为空
2)在F中选取两棵权值最小的树作为左、右子树构造一棵新的二 叉树,且新二叉树的根结点的权值为其左右子树上根结点权值之和
3)在F中删除这两棵树,同时将新得到的二叉树加入F中
4)重复2、3,直到F中只含一棵树为止——该树即为赫夫曼树
2、哈夫曼树算法的实现
哈夫曼树的存储表示
Typedef struct
{
int weight;
int parent,lchild,rchild;
}HTNode,*HuffmanTree;
[算法步骤]
1)初始化:首先动态申请2n个单元;循环2n-1次,依次将1至 2n-1中双亲、左孩子、右孩子的下标初始化为0;再循环n次,输入 前n个单元中叶结点的权值
2)创建树:循环n-1次,通过n-1次的选择、删除与合并来创建 哈夫曼树。选择是从当前森林中选取双亲为0且权值最小的两个树 根结点s1和s2;删除是指将结点s1和s2的双亲改为非0;合并是将 s1和s2的权值和作为一个新结点的权值依次存取到数组的第n+1之 后的单元,同时记录该结点的左孩子下标为s1,右孩子的下标为s2
[算法描述]
void CreatHuffmanTree(HuffmanTree &HT,int n)
{
int m,s1,s2,i;
if(n<=1)
return;
m=2*n-1;
HT=new HTNode[m+1];
//0号单元未用,所以需要动态分配 m+1个单元,HT[m]表示根结点
for(i=1;i<=m;++i)
{
HT[i].parent=0;
HT[i].lchild=0;
T[i].rchild=0;
}
cout<<"请输入叶子结点的权值:\n";
for(i=1;i<=n;++i)
cin>>HT[i].weight;
/*――――初始化工作结束,下面开始创建赫夫曼树―――――*/
for(i=n+1;i<=m;++i)
{
//通过n-1次的选择、删除、合并来创建赫夫曼树
Select(HT,i-1,s1,s2);
//在HT[k](1≤k≤i-1)中选择两个其双亲域为0且权值最小的结点, 并返回它们在HT中的序号s1和s2
HT[s1].parent=i;
HT[s2].parent=i;
//得到新结点i,从森林中删除s1,s2,将s1和s2的双亲域由0改为i
HT[i].lchild=s1;
HT[i].rchild=s2 ;//s1,s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight;
}//for
}// CreatHuffmanTree
1、哈夫曼编码的主要思想
1)前缀编码:在一个编码方案中,任一字符的编码都不是另一 字符编码的前缀(最左字符串),则称编码为前缀编码
2)哈夫曼编码:对一棵具有n个叶子的哈夫曼树,若对树中的每 个左分支编码0,右分支编码1,则从根到每个叶结点的路径上,各分支的赋值分别构成一个二进制编码串,称为哈夫曼编码
例如:编码集合{ 00,110,111,10,01 }是前缀编码;如果加入 0、1、11则为非前缀编码!
哈夫曼编码满足两个性质
1)哈夫曼编码是前缀编码
2)哈夫曼编码是最优前缀编码
设要传送的字符为: A B A C C D A
编码规则:A—0 B—110 C—10 D—111
实际传送的编码为:0 110 0 10 10 111 0
哈夫曼编码表的存储表示
typedef char **Huffmancode; //动态分配数组存储哈夫曼编码表
void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n)
{
//从叶子到根逆向求每个字符的赫夫曼编码,存储在编码表HC中
int i,start,c,f; HC=new char*[n+1];
//分配存储n个字符编码的编码表空间
char *cd=new char[n];
//分配临时存放编码的动态数组空间
cd[n-1]='\0';
//编码结束符
for(i=1;i<=n;++i)
//逐个字符求哈夫曼编码
{
start=n-1;
//start开始时指向最后,即编码结束符位置
c=i;
f=HT[i].parent;
//f指向结点c的双亲结点
while(f!=0)
{
--start;
//回溯一次start向前指一个位置
if(HT[f].lchild==c)
cd[start]='0';
//结点c是f的左孩子,则生成代码0
else
cd[start]='1';
//结点c是f的右孩子,则生成代码1
c=f;
f=HT[f].parent;
//继续向上回溯
}
//求出第i个字符的编码
HC[i]=new char[n-start];
// 为第i 个字符编码分配空间
strcpy(HC[i], &cd[start]);
//将求得的编码复制到HC的当前行中
}
delete cd;
//释放临时空间
} // CreatHuffanCode