**树是n(n>=0)个结点的有限集。**当n=0时,称为空树。在任意一颗非空树中应满足:
1) 有且仅有一个特定的称为根的结点
2) 当n>1时,其余结点可分为m (m>0) 个互不相交的有限集,其中每个集合本身又是一棵树,并且称为根的子树。
树是一种递归的数据结构,树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:
- 树的根结点没有前驱,除根结点外的所有结点有且仅有一个前驱
- 树中所有结点可以有零个或多个后继
基本术语
-
A称为B和C的双亲,B和C是A的孩子,B和C有相同双亲,所以C是B的兄弟。
-
结点的度:树中一个结点的孩子个数称为结点的度,A,B,C的度都为2
-
树的度:树中结点的最大度数称为树的度。上图中树的度为2
-
分支结点:度大于0的结点。度为0的结点称为叶结点。
-
有序树和无序树:树中结点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树。
-
路径和路径长度:树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
【注】由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,同一双亲的两个孩子之间不存在路径
-
森林:森林是m (m>0) 棵互不相交的树的集合。
树的性质
树具有以下最基本的性质:
-
树中的结点数等于所有结点的度数+1
-
度为m的树中第i层上至多有m^(i-1)个结点(i>=1)
-
高度的h的m叉树至多有(m^h-1)/(m-1)个结点
-
具有n个结点的m叉树的最小高度的为
⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ \lceil log_m(n(m-1)+1) \rceil ⌈logm(n(m−1)+1)⌉
度为m的树 | m叉树 |
---|---|
任意结点的度<=m | 任意结点的度<=m |
至少有一个结点的度=m | 允许所有结点的度都<m |
一定是非空树 至少有m+1个结点 | 可以是空树 |
二叉树
二叉树的定义及其主要特性
1.二叉树的定义
二叉树是另一种树形结构,其特点是每个结点至多只有两棵子树,并且二叉树的子树有左右之分,其次序不能颠倒
二叉树是有序树,若将其左右子树颠倒,则成为另外一棵不同的二叉树
2.几个特殊的二叉树
- 满二叉树,一棵高为h,含有2^h-1个结点的二叉树称为满二叉树。满二叉树的叶子结点都集中在二叉树的最下一层,并且除叶结点以外的每个结点度数均为2。可以对满二叉树按层序编号,约定编号从根节点起,自上而下,自左向右。
- 完全二叉树,高度为h、有n个结点的二叉树,当且仅当其每个结点都与高度为h的满二叉树中编号1~n的结点一一对应时,称为完全二叉树。
- 二叉排序树,左子树上的所有结点的关键字都小于根节点的关键字,右子树上的所有结点的关键字均大于根节点的关键字,左子树和右子树又分别是一颗二叉排序树
- 平衡二叉树,树上任一结点的左子树和右子树的深度之差不超过1。
3.二叉树的性质
-
非空二叉树上的叶子结点数等于度为2的结点数加1,即
n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1 -
非空二叉树上第k层上至多有2^(k-1)个结点(k>=1)
-
高度为h的二叉树至多有2^h-1个结点(h>=1)
二叉树的存储结构
1.顺序存储结构
依据二叉树的性质,完全二叉树和满二叉树采用顺序存储比较合适,树中结点的序号可以唯一地反映结点之间的逻辑关系,这样既能最大可能地节省存储空间,又能利用数组元素的下标值确定结点在二叉树中的位置,以及结点之间的关系。
2.链式存储结构
由于顺序存储的空间利用率较低,因此二叉树一般都采用链式存储结构,二叉链表至少包含3个域:数据域data、左指针域 lchild,右指针域rchild。
二叉树的链式存储结构描述如下:
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
二叉树的遍历与线索二叉树
1.先序遍历
先序遍历的操作过程如下:
若二叉树为空,则什么也不做;否则
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
对应的递归算法如下:
void PreOrder(BiTree T){
if(T!=NULL){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
2.中序遍历
中序遍历的操作过程如下:
若二叉树为空,则什么也不做;否则
- 中序遍历左子树
- 访问根节点
- 中序遍历右子树
对应的递归算法如下:
void InOrder(BiTree T){
if(T!=NULL){
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
3.后序遍历
后序遍历的操作过程如下:
若二叉树为空,则什么也不做;否则
- 后序遍历左子树
- 后序遍历右子树
- 访问根节点
对应的递归算法如下:
void PostOrder(BiTree T){
if(T!=NULL){
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
例如,上面这棵二叉树的先序遍历序列为:ABDFECGHI
中序遍历序列为:DBEFAGHCI
后序遍历序列为:DEFBHGICA
4.层序遍历
如图,按照序号的顺序对二叉树的结点进行逐个遍历即为二叉树的层序遍历。
二叉树的层序遍历算法如下:
void LevelOrder(BiTree T){
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q,T); //将根节点入队
while(!IsEmpty(Q)){ //队列不空则循环
DeQueue(Q,p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild!=NULL)
EnQueue(Q,p->lchild); //左子树不空,则左子树根节点入队
if(p->rchild!=NULL)
EnQueue(Q,p->rchild); //右子树不空,则右子树根节点入队
}
}
线索二叉树
1.线索二叉树的概念
遍历二叉树是以一定的规则将二叉树的结点排成衣蛾线性序列,从而得到几种遍历序列,使得该序列中的每一个结点(第一个和最后一个结点除外)都有一个直接前驱和直接后继。
规定:若无左子树,则令其lchil指向其前驱结点,若无右子树,则令其rchild指向其后继结点。
线索二叉树的存储结构描述如下:
typedef struct ThreadNode{
Elemtype data;
struct ThreadNode *lchild,*rchild;
int ltag,rtag;
}ThreadNode,*ThreadTree;
以这种结点结构构成的二叉链表作为二叉树的存储结构,称为存储链表,其中指向结点前驱和后继的指针称为线索,加上线索的二叉树称为线索二叉树。
2.中序线索二叉树的构造
二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索。
中序线索二叉树的建立思想:附设指针pre指向刚刚访问过的结点,指针p指向正在访问的结点,即pre指向p的前驱。在中序遍历的过程中,检查p的左指针是否为空,若为空就将它指向pre,检查pre的右指针是否为空,若为空就将它指向p。
中序遍历对二叉树线索化的递归算法如下:
void InThread(ThreadTree &p,ThreadTree &pre){
if(p!=NULL){
InThread(p->lchild,pre);
if(p->lchild==NULL){
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL){
pre->rchild=p;
pre->rtag=1;
}
pre=p;
InTread(p->rchild,pre);
}
}
树、森林
树的存储结构
1.双亲表示法
这种存储方式采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。
双亲表示法的存储结构描述如下:
#define MAX_TREE_SZIE 100
typedef struct{
ElemType data;
int parent;
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE];
int n;
}PTree;
这种存储结构能够快速得到每个结点的双亲结点,但求结点的孩子时需要遍历整个结构。
2.孩子表示法
孩子表示法是将每个结点的孩子结点都用单链表链接起来形成一个线性结构,此时n个结点就有n个孩子链表(叶子结点的孩子链表为空表)。
3.孩子兄弟表示法
孩子兄弟表示法又称二叉树表示法,即以二叉链表作为树的存储结构。孩子兄弟法使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针。
孩子兄弟表示法的存储结构描述如下:
typedef struct CSNode{
Elemtype data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
这种存储方式比较灵活,是最常用的表示方法,其最大的优点是可以方便地实现树转换为二叉树的操作,易于查找结点的孩子等,但查找当前结点的双亲比较麻烦。
树、森林、二叉树的转换
树转换为二叉树的规则:每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟。即“左孩子右兄弟”
森林转换为二叉树的规则与树转换成二叉树的规则相似。
二叉树转换为森林的规则:若二叉树非空,则二叉树的根及其左子树为第一棵树的二叉树形式,故将其根的右链断开。
树和二叉树的应用
二叉排序树
1.二叉排序树的定义
二叉排序树或者是一棵空树,或者是具有下列特性的二叉树:
- 若左子树非空,则左子树上所有结点的值均小于根节点的值
- 若右子树非空,则右子树上所有结点的值均大于根节点的值
- 左右子树也分别是一棵二叉排序树
根据二叉排序树的定义,左子树的结点值<根节点值<右子树的结点值,故若对二叉排序树进行中序遍历。可以得到一个递增的有序序列。
2.二叉排序树的查找
二叉排序树的查找是从根节点开始,若二叉排序树非空,给定比较值与根节点的关键字相等,则查找成功;若小于根节点的关键字,则在根节点的左子树上查找;否则在根节点的右子树上查找。
二叉排序树的非递归查找算法:
BSTNode *BST_Search(BiTree T,ElemType key){
while(T!=NULL&&key!=T->data){
if(k<T->data) T=T->lchild;
else T=T->rchild;
}
return T;
}
3.二叉树排序树的查找效率分析
二叉排序树的查找效率,主要取决于树的高度。若二叉树的左、右子树的高度之差的绝对值不超过1,则这样的二叉排序树称为平衡二叉树,它的平均查找长度为O(log_2 n)
平衡二叉树
为避免树的高度增长过快,降低二叉排序树的性能,规定在插入和删除二叉树结点时,要保证在任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树。定义左子树和右子树的高度之差为该节点的平衡因子,平衡二叉树结点的平衡因子的值只可能是-1,0, 1。
1.平衡二叉树的插入
二叉排序树保证平衡的基本思想如下:每当在二叉排序树中插入(或删除)一个结点时,首先检查其插入路径上的结点是否因为此次操作而导致了不平衡,若导致了不平衡,则先找到插入路径上离结点最近的平衡因子绝对值大于1 的结点,再对以这个结点为根的子树在保持二叉排序特性的前提下,调整各结点的位置关系,使之重新达到平衡
【注】每次调整的对象都是最小不平衡子树。
2.平衡二叉树的4种调整规律:
1.LL型调整(右单旋转)
在A的左孩子(L)的左子树(L)上插入新结点,使原来平衡二叉树不平衡,此时A的平衡因子由1增至2。下图是LL型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B顺时针旋转一样。
2. RR型调整(左单旋转)
在A的右孩子®的右子树®上插入新结点,使原来平衡二叉树不平衡,此时A的平衡因子由-1变为-2。显然,按照大小关系,下图中结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B逆时针旋转一样。
3. LR型调整(先左后右双旋转)
在A的左孩子(L)的右子树®上插入新结点,使原来平衡二叉树不平衡,此时A的平衡因子由1变为2。图5是LR型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
4. RL型调整(先右后左双旋转)
在A的右孩子®的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
哈夫曼树和哈夫曼编码
1.哈夫曼树的定义
在许多应用场景中,树中结点常常被赋予一个某种意义的数值,称为该结点的权。从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为该树的带权路径长度。记为:
W
P
L
=
∑
i
=
0
n
w
i
l
i
WPL=\sum_{i=0}^n w_il_i
WPL=i=0∑nwili
其中,w为树中第i个叶结点所带的权值,l是该叶结点到根节点的路径长度。
在含有n个带权叶结点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树。
2.哈夫曼树的特点
- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
- 构造过程中共新建了n-1个结点(双分支结点),因此哈夫曼树的结点总数为2n-1。
- 每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点。
3.哈夫曼编码
在数据通信中,若对每个字符用相等长度的二进制位表示,称这种编码方式为固定长度编码。
若允许对不同字符用不等长的二进制位表示,则这种编码方式称为可变长度编码。
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。
由哈夫曼树得到哈夫曼编码的过程是很自然的,首先,将每个出现的字符当做一个独立的结点,其权值作为它出现的频度(或次数),构造出对应的哈夫曼树。我们可以将字符的编码解释为从根到该字符(字符结点都是叶结点)的路径边标记的序列,其中边标记为0表示“转向左孩子”,标记为1表示“转向右孩子”。
这棵哈夫曼树的WPL为
WPL = 2*(14+24+34)+3*13+4*7+5*(5+5)= 261
孩子,因此哈夫曼树中不存在度为1的结点。
3.哈夫曼编码
在数据通信中,若对每个字符用相等长度的二进制位表示,称这种编码方式为固定长度编码。
若允许对不同字符用不等长的二进制位表示,则这种编码方式称为可变长度编码。
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。
由哈夫曼树得到哈夫曼编码的过程是很自然的,首先,将每个出现的字符当做一个独立的结点,其权值作为它出现的频度(或次数),构造出对应的哈夫曼树。我们可以将字符的编码解释为从根到该字符(字符结点都是叶结点)的路径边标记的序列,其中边标记为0表示“转向左孩子”,标记为1表示“转向右孩子”。
[外链图片转存中…(img-L5naYN67-1598190177880)]
这棵哈夫曼树的WPL为
WPL = 2*(14+24+34)+3*13+4*7+5*(5+5)= 261
构造的哈夫曼树不唯一,但是哈夫曼树的带权路径长度一定唯一。