树
一:树(一对多)
-
定义:是N(N>=0)个结点的有限集。n = 0时为空树,有且仅有一个特定的称为根(Root)的结点
-
子树
-
对于定义的强调:
- n > 0时根结点是唯一的,不可能存在多个根结点
- m > 0时,子树的个数没有限制,但是他们一定是互不相交的。
1.结点分类
- 定义:树的结点包含一个数据元素及若干指向其子树的分支。
- 结点拥有的子树数称为结点的度。
- 度为0的结点称为叶结点或者终端结点。度不为0的结点称为分支结点。
- 树的度:是树内各结点的度的最大值。
2.结点的关系
- 结点的子树称为该结点的孩子,该结点称为孩子的双亲
3.结点的层次(根的深度)
4.其他概念
- 如果将树中的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
- 森林:是m棵互不相交的树的集合。
5.线性结构和树结构
线性结构 | 树结构 |
---|---|
第一个数据元素:无前驱 | 根结点:无双亲,唯一 |
最后一个数据元素:无后继 | 叶结点:无孩子,可以多个 |
中间元素:一个前驱一个后继 | 中间结点:一个双亲多个孩子 |
二:存储结构
1.顺序存储结构
- 双亲表示法
- 孩子表示法
- 孩子兄弟表示法
三:二叉树
1.定义及特点:
是另一种树型结构,他的特点是每个结点至多只有两棵子树(即二叉树不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。
2.重要性质
-
二叉树在第i层上至多有2i-1个结点(i >= 1)
-
深度为k的二叉树至多有2k-1个结点(k >=)
-
任何一棵二叉树T,如果其终端结点数(叶结点)树为n0,度为2的结点树为n2,则n0 = n2 + 1。
-
具有n个结点的完全二叉树的深度为log2n + 1
-
如果对一棵有n个结点的完全二叉树(其深度为log2n + 1)的结点按层序编号(从第1层到第log2n + 1层,每层从左到右),则对任一结点i(1<= i <= n),有:
-
如果i = 1,则结点i是二叉树的根,无双亲;如果i > 1,则其双亲partent(i)是结点(i/2)
-
如果2i > n, 则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
-
如果2i + 1 > n,则结点i无左右孩子;否则其有孩子的结点是2i + 1。
-
3.满二叉树
- 定义:一棵深度为k且有2k-1个结点的二叉树称为满二叉树。(即除了叶结点,其余结点的度均为2)。
4.完全二叉树
- 定义:深度为K的,有N个节点的满二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。
5.二叉树的存储结构
1.顺序存储结构
-
表示
#define MAX_TREE_SIZE 100 typedef TElemType SqBiTree[MAX_TREE_SIZE]; SqBiTree bt;
2.链式存储结构
-
表示
typedef struct BiTNode{ TElemType data; struct BiTNode *lchild, *rchild; // 左右孩子指针 }BiTNode, *BiTree;
-
实现
3.递归遍历二叉树(遍历代码链接)
-
先序遍历二叉树的操作定义为:
若二叉树为空,则空操作;否则:
- 访问根节点;
- 先序遍历左子树
- 先序遍历右子树
-
中序遍历二叉树的操作定义为:
若二叉树为空,则空操作;否则:
- 中序遍历左子树;
- 访问根节点
- 中序遍历右子树
-
后序遍历二叉树的操作定义为:
若二叉树为空,则空操作;否则:
- 后序遍历左子树
- 后序遍历右子树
- 访问根节点
1.前序和中序遍历序列可以唯一确定一个二叉树;中序和后序遍历可以唯一确定一个二叉树
2.为什么创建二叉树使用二级指针: 待续
4.线索二叉树
- 定义:对于普通二叉树以某种次序遍历使其成为线索二叉树的过程叫线索化
作如下规定:
- 若结点有左子树,则其lchild域指示其左孩子,否则令lchild域指示其前驱;
- 若结点有右子树,则其rchild域指示其右孩子,否则令rchild域指示其后继;
前驱和后继均指以某种次序遍历所得序列中的前驱和后继,(其中指向节点前驱和后继的指针叫做线索)
- 结点如下:
- 说明:
LTag | RTag | |
---|---|---|
0 | lchild域指示结点的左孩子 | rchild域指示结点的左孩子 |
1 | lchild域指示结点的前驱 | rchild域指示结点的前驱 |
-
以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表
注:所有非终端节点的右链都为指针 -
存储表示
// 枚举类型
typedef enum PointerTag{
Link; // Link == 0:指针
Thread; // Thread == 1:线索
}
typedef struct BIThrNode{
TElemType data;
struct BiThrNode *lchild,*rchild; // 左右孩子指针
PointerTag LTag,RTag;
}
// 中序遍历线索化二叉树
// BiTree pre; 全局变量,始终指向刚刚访问过的结点
void InThreading(BiTree p,BiTree *pre){
if (p)
{
// 递归左子树线索化
InThreading(p->lchild);
// 如果没有左孩子
if (!p->lchild)
{
// 前驱线索
p->LTag = Thread; // Thread = 1;
// 左孩子指针指向前驱
p->lchild = pre;
}
// 前驱没有右孩子
if (!pre->rchild)
{
// 后继线索
pre->RTag = Thread;
// 前驱右孩子指针指向后继(当前结点p)
pre->rchild = p;
}
// 保持pre指向p的前驱
pre = p;
// 递归右子树线索化
InThreading(p->rchild);
}
}
线索化的实质:就是将二叉链表中的空指针改为指向前驱或者后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
四:树、森林与二叉树的转换
1.树转换为二叉树
- 加线:所有兄弟之间加线
- 去线:对于每个节点只保留第一个孩子节点的连线,删除他与其他孩子之间的连线。
- 层次调整:以树的根节点为轴心,顺时针旋转一定的角度。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。
2.森林转换为二叉树
- 把每个树转换为二叉树。
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
- 当所有的二叉树连接起来后就得到了由森林转换来的二叉树。
3.二叉树转换为树
- 加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点……哈,反正就是左孩子的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
- 去线。删除原二叉树中所有结点与其右孩子结点的连线。
- 层次调整。使之结构层次分明。
4.二叉树转换为森林
判断二叉树是否能转为森林标准:二叉树的根节点有没有右孩子,有就能转为森林,没有就是一棵树
5.树和森林的遍历
树的遍历分为两种方式:
- 一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树
- 另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。
如二叉树转树图中右下角的树:
- 先根遍历序列为ABEFCDG
- 后根遍历序列为EFBCGDA
森林的遍历也分为两种方式:
- 前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林。
- 是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。
如二叉树转森林图中右下角的森林:
- 前序遍历序列的结果就是ABCDEFGHJI
- 后序遍历序列的结果就是BCDAFEJHIG
这也就告诉我们,当以二叉链表作树的存储结构时,树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。这其实也就证实,我们找到了对树和森林这种复杂问题的简单解决办法
五:赫夫曼树(哈夫曼树)
1.哈夫曼树的定义和原理
-
定义:给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
-
路径长度:从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。
二叉树a中,根结点到结点D的路径长度就为4,二叉树b中根结点到结点D的路径长度为2
-
树的路径长度:就是从树根到每一个节点的路径长度之和。
-
二叉树a的树路径长度就为1+1+2+2+3+3+4+4=20。二叉树b的树路径长度就为1+2+3+3+2+1+2+2=16。(不理解)
-
树的带权路径长度为树中所有叶子结点的带权路径长度之和。
-
带权路径长度WPL最小的二叉树称做赫夫曼树。也有不少书中也称为最优二叉树
2.构造哈夫曼树
-
先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列,即:A5,E10,B15,D30,C40。
-
取头两个最小权值的结点作为一个新节点N1的两个子结点,注意相对较小的是左孩子,这里就是A为N1的左孩子,E为N1的右孩子,如图6-12-5所示。新结点的权值为两个叶子权值的和5+10=15。
-
将N1替换A与E,插入有序序列中,保持从小到大排列。即:N115,B15,D30,C40。
3.哈夫曼编码
定义:一般地,设需要编码的字符集为{d1,d2,…,dn},各个字符在电文中出现的次数或频率集合为{w1,w2,…,wn},以d1,d2,…,dn作为叶子结点,以w1,w2,…,wn作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是赫夫曼编码。