大话数据结构笔记-树
基础概念
树是n个节点的有限集。
- n=0时称为空树
- 在任意非空树中
- 有且仅有1个根结点(Root)
- n>1时其余节点分别为m个互不相交的有限集
- 每个集合本身就又是一棵树称为根的子树(SubTree)
结点
- 结点拥有的子树称为结点的度(Degree)
- 度为0的结点称为叶结点(Leaf)或者终端结点
树的度是树内各结点的度的最大值
结点的子树的根称为该结点的孩子(child)
- 相应的,该结点称为孩子的双亲(Parent)。。。。。
- 同一个双亲的孩子之间互称兄弟(Sibling)
- 结点的祖先是根到该结点经过的所有结点
- 以某结点为根的子树的任意结点都是他的子孙
其他
- 结点的层次(Level)从根开始定义,根为第一层
- 树中结点的最大层次称为树的速度(Depth)或高度
- 若将树中结点看成从左至右有次序的不能互换的,则为有序树,否则为无序树。
- 森林(Forest)是m棵不相交的树的集合
与线性结构的差别
线性结构
- 第一个数据元素: 无前驱
- 最后一个数据元素: 无后继
- 中间元素: 一个前驱一个后继
树结构
- 根结点: 无双亲,唯一
- 叶结点: 无孩子,可以多个
- 中间结点: 一个双亲多个孩子
存储结构
双亲表示法
在每个结点中,附设一个指示器指示其双亲结点在数组中的位置。
相当于,一个数据结点有两个域:
- 数据域:存储数据信息
- 指针域:存储双亲结点的地址
数据域 | 指针域 |
---|---|
data | parent |
因为根结点没有双亲结点,使用根结点的指针域为’-1’
方便查找双亲结点,孩子结点不方便。
多重链表表示法
每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做 多重链表表示法。
类似于这样:
数据域 | 指针域1 | 指针域2 | 指针域3 | 指针域n |
---|---|---|---|---|
data | child1 | child2 | child3 | childn |
这样就会有个问题:如何统一整棵树各个指针域的个数。
方法一
全树使用树的度。
这样会浪费很多空间。
方法二
每个结点使用自己的度来计算指针域。
这样每个结点需要一个专门的域存储结点的度
这样:
数据域 | 度 | 指针域1 | 指针域2 | 指针域3 | 指针域n |
---|---|---|---|---|---|
data | 度 | child1 | child2 | child3 | childn |
比如说度为2:
数据域 | 度 | 指针域1 | 指针域2 |
---|---|---|---|
data | 2 | child1 | child2 |
这样节省了很多空间,但是因为结构复杂,需要维护的数据变多,在计算上会耗时。
孩子表示法
- 把每个结点的孩子结点排列起来,以单链表作存储结构。
- 则n个结点有n个孩子链表。
- 如果是叶子结点则此单链表为空,
- 然后,n个头指针又组成一个线性表,采用顺序存储结构,存放在一个一维数组中。
孩子链表:
child|next
表头数组:
data|firstchild
双亲孩子表示法
就是在孩子表示法的基础上,在表头数组的表头结点上添加一个parent用来存储双亲结点的地址。
方便查找双亲结点。
表头数组:
data|parent|firstchild
孩子兄弟表示法
任意一棵树,他的结点的第一个孩子如果存在就是唯一的,他的右兄弟如果存在也是唯一的。因此,我们甚至两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
data|firstchild|rightsib
方便查找结点的孩子。
这样,一棵复杂的树就变成了二叉树(左子树是孩子结点,右子树兄弟结点)。
二叉树
二叉树(Binary Tree)是n个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两颗互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
特点
- 每个结点最多有两棵子树。
- 左子树和右子树是有顺序的,不能相互颠倒。
- 即使某结点只有一棵子树,也要区分是左子树还是右子树。
二叉树的基本形态
- 空二叉树
- 只有一个根结点
- 根结点只有左子树
- 根结点只有右子树
- 根结点既有左子树又有右子树
特殊二叉树
斜树
所有结点只有左子树叫左斜树,只有右结点叫右斜树。
满二叉树
所有结点都存在左右子树,所有叶结点都在同一层上。
完全二叉树
就是不满的满二叉树。
二叉树的性质
- 二叉树在第i层上最多有2^(i-1)个结点。
- 深度为k的二叉树至多有2^k-1个结点。
- 对于任何二叉树T,如果终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
- 具有n个结点的完全二叉树的深度为
logv2n+1
- 太复杂,不写了
二叉树的存储结构
顺序存储结构
就是按照完全二叉树的顺序存储。
只适合存储完全二叉树,否则空间利用率低。
二叉链表
二叉树每个结点最多有两个孩子结点,所以如下形式:
左子树 | 数据 | 右子树 |
---|---|---|
lchild | data | rchild |
遍历二叉树
是指从根结点出发,按照某种次序以此访问所有结点,使得每个结点被访问且仅被访问一次。
前序遍历
- 若二叉树为空则空操作返回
- 否则先访问根结点,然后依次前序遍历左子树,再前序遍历右子树。
中序遍历
- 若二叉树为空则空操作返回。
- 否则从左到右先叶子后结点的方式遍历访问左子树
- 然后访问根结点。
- 最后访问右子树。
后序遍历
- 若二叉树为空则空操作返回。
- 否则从左到右先叶子后结点的方式遍历访问左右子树
- 最后访问根结点。
层序访问
- 若二叉树为空则空操作返回。
- 否则从树的第一层开始访问,左到右先,由上至下逐层遍历。
线索二叉树
- 我们把指向前驱和后继的指针称为线索,
- 加上线索后的二叉链表称为线索链表,
- 相应的二叉树称为线索二叉树。
个人理解,线索二叉树就是将所有无子树的结点的rchild指针指向接下来需要遍历的结点(按照某种次序,例如中序遍历)。
对二叉树以某种次序遍历使其变为线索二叉树的过程称作是 线索化。
如果二叉树需要经常遍历或查找结点是需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
树、森林与二叉树的转换
树转换为二叉树
- 加线,所有兄弟结点之间连接一条线。
- 去线,对树中的每个结点,只保留它与第一个孩子结点的连线,删除与其他孩子结点之间的连线。
- 层次调整,一根结点为轴心,第一个孩子是二叉树的左子树。
森林转换为二叉树
- 把每一棵树转换为二叉树
- 第一棵二叉树不动,第二棵树开始,以此把后一棵二叉树的根结点作为前一棵树的根结点的右子树连接起来
树和森林的遍历
树的遍历
- 先根遍历,先访问树的根结点,然后依次访问每棵子树。
- 后根遍历,先访问每棵树树的子结点,再根结点。
森林的遍历
- 前序遍历,先访问第一棵树的根结点,然后依次先根遍历每一棵子树,然后下一课树。。。
- 后序遍历,先后根遍历第一棵树,然后下一课,然后下一棵
赫夫曼树
权:树结点间的边相关的数。
- 从树中一个结点到另一个结点之间的分支构成两个结点之间的路径
- 路径上的分支数目叫做路径的长度。
- 树的路径长度是从根到每个结点路径长度的和。
- 带权路径长度(WPL)最小的二叉树叫做赫夫曼树。
构造赫夫曼树
- 将所有子叶按权值由小到大排序(A5, E10, B15, D30, C40)
- 将最小的两项组成树N,N的权值为两项的和。(Na15, B15, D30, C40)
- 在树N中,小项为左子树。
- 依次到最后
- 生成赫夫曼树
赫夫曼编码
一般的,设需要编码的字符集为
{d1, d2, d3, … dn},
各个字符在电文中出现的次数或频率集合为
{w1, w2, w3, … wn}
以d为叶子结点,w为权值,构造一棵赫夫曼树。
规定赫夫曼树的左分支代表0,右分支代表1,
则从根结点到叶子结点经过的路径分支组成的序列为该结点对应的编码。
这就是赫夫曼编码。