数据结构之树

330 阅读15分钟

树结构是重要的非线性的数据结构,是以分支关系定义的层次结构。操作系统中用树来表示文件目录的组织结构,编译系统中用树来表示源程序的语法结构,数据库中树也是信息的重要组织形式之一。

1 树

是n个结点的有限集,或为空树、或为非空树,对于非空树:

  • 有且只有一个可以称为根的结点
  • 除了根结点其他结点可以分为m个有限集,每个有限集又是一棵树,称为根的子树

关于树的一些用语:

  • 结点:树中一个单元,包含数据元素和指向子树的分支
  • 结点的度:结点拥有的子树个数
  • 树的度:树的所有结点当中最大的度
  • 叶子:度为0,也叫终端结点
  • 非终端结点:度不为0的结点,也叫分支结点,除了根节点之外也可以叫内部结点
  • 双亲和孩子:结点的子树为该结点的孩子,该节点也是其子树的双亲
  • 兄弟:拥有同样双亲的结点
  • 祖先:从根节点到该分支的所有结点
  • 子孙:以某个结点为根的所有子树上的任一结点都叫该结点的子孙
  • 层次:从根开始为第一层,之后每个结点的层次为其双亲结点的层次加一
  • 堂兄弟:双亲在同一层的结点互为堂兄弟
  • 树的深度:也叫树的高度,是树的最大层次
  • 有序树和无序树:如果一棵树的子树从左到右认为是有序的,该树为有序树,否则该树为无序树
  • 森林:由互不相交的任意棵树组成

2 二叉树

是由n个结点组成的集合,或为非空树,或为空树,对于非空树:

  • 有且只有一个称之为根的结点
  • 除了根节点,其余结点分为互不相交的子集T1和T2,称之为左子树和右子树,且都为二叉树
  • 二叉树每个结点至多两棵子树,子树有左右之分

二叉树的应用

  • 哈夫曼编码
  • 求解表达式

2.1 二叉树的性质

  • 二叉树的第i层至多2^(i - 1) 个结点
  • 深度为k的二叉树最多有 2^k - 1 个结点
  • 对任意一棵二叉树T,如果其终端节点个数为n0,度为2的结点个数为n2,n0 = n2 + 1
    • 设n1为结点中度为1的结点个数
    • 计算结点总数 n = n0 + n1 + n2
    • 依靠边来计算结点总数,除了根结点,每个结点都存在一条边从上边连接下来,度为1贡献一条边,度为2贡献2条边,度为0下边没有边, n = 1 + n1 + 2n2
    • 故n0 = n2 + 1

2.2 特殊形态的二叉树

2.2.1 满二叉树

深度为k且含有2^k - 1个结点的二叉树,也就是说每一层都达到了课包含结点的最大值,每一层i都有2^i - 1个结点

2.2.2 完全二叉树

允许满二叉树最后一层从右往左连续缺失。深度为k,有n个结点,每个结点的编号和位置都与深度为k的满二叉树一一对应

  • 完全二叉树的叶子只可能出现在最后两层
  • 具有n个结点的完全二叉树的深度k为 log2(n)向下取整 + 1
  • 对一棵有n个结点的完全二叉树按层次从上到下从左到右从1开始编号,存在如下性质
    • i = 1 则i为根,i > 1则该结点的根为 i / 2 向下取整
    • 2i > n,则结点i没有左孩子,否则i的左孩子为2i
    • 2i + 1 > n + 1,则结点i没有右孩子,否则i的右孩子为2i + 1

2.3 二叉树的存储结构

2.3.1 顺序存储结构

用一组地址连续的存储单元来存储数据元素,为了能够在存储结构中反映出结点之间的逻辑关系,必须将二叉树当中的结点按照一定的规律安排在这组单元当中

  • 对于完全二叉树,从根开始按层序存储即可,依次自上而下、从左至右存储结点元素,比如编号为i的结点存放在一维数组的 i - 1 下标位置
  • 对于一般二叉树,将其每个结点按照完全二叉树的结点编号填入对应位置,如果某个位置不存在结点,则一维数组当中该位置不存放结点 可以看出顺序存储结构比较适用于完全二叉树。对于非完全二叉树采用顺序存储结构会导致空间的浪费。比如有k个结点的单支树只需要k个结点空间存储,适用顺序存储结构需要分配给他2^(k - 1)个存储空间

2.3.2 链式存储结构

对于一般的二叉树,适合采用链式存储结构。设计不同的结点结构可以构成不同形式的链式存储结构。
二叉链表:设计每个结点包含一个数据域和两个指针域,指针域分别指向左右子树
三叉链表: 设计每个节点包含一个数据域和三个指针域,指针域两个分别指向左右子树,剩下一个指向该子树的根结点

2.4 遍历二叉树

遍历二叉树是指按照某条搜素路径寻访树中的每个结点,每个结点均被访问一次,而且只被访问一次。遍历的结果是将非线性结构的树中的结点排成一个线性序列

2.4.1 遍历方式

遍历二叉树的方式有先序、中序、后序遍历三种:

  • 先序遍历二叉树(根左右)
    • 如果二叉树为空,则空操作,否则进行如下操作
    • 访问根结点
    • 先序遍历左子树
    • 先序遍历右子树
  • 中序遍历二叉树(左根右)
    • 如果而二叉树为空,则空操作,否则进行如下操作
    • 中序遍历左子树
    • 访问根结点
    • 中序遍历右子树
  • 后序遍历二叉树(左右根)
    • 如果而二叉树为空,则空操作,否则进行如下操作
    • 后序遍历左子树
    • 后序遍历右子树
    • 访问根结点

2.4.2 通过遍历序列确定二叉树

由二叉树的先序遍历序列和中序遍历序列,或者二叉树的后序遍历序列,都可以唯一确定一棵二叉树

以先序遍历序列为例

  • 先序遍历序列第一个肯定是根结点
  • 中序遍历结点可以根据根结点将序列划分为三部分:左子树的中序遍历序列、根结点、右子树的中序遍历序列
  • 先序序列中可以根据中序序列得到的左右子树的中序序列,划分自己的左右子树的先序序列
  • 左子树的先序序列的第一个肯定是左子树的根节点(递归判断)

2.4.3 线索二叉树

线索二叉树在结点上除了数据域和两个指针域,还添加了两个标志域,用于标志指针域指向的是否是子结点,如果不是指向子节点,则左指针域会指向遍历序列当中该结点对应的前驱,右指针会指向遍历序列对应该节点的后继。
由这样的结点构成的二叉链表叫做线索链表,其中指向结点的前驱和后继的指针称为线索。加上线索的二叉树称为线索二叉树,对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化

3 树和森林的表示

3.1 树的存储结构

3.1.1 双亲表示法

以一组连续的存储单元存储树的结点,每个结点包括数据域data,和设置了parent域用于指示其双亲结点的位置。

这种存储结构利用了每个结点除了根以外只有唯一的双亲的性质。寻找结点的双亲十分方便,也很容易找到树的根,但是求结点的孩子需要逐个遍历

3.1.2 孩子表示法

采用多重链表的方式,其中存在多种选择

  • 每个结点是固定的,包含一个数据域data和多个指向子结点的指针域,指针域的个数固定为最大的度。会存在一些结点的度小于d,链表中会存在空链域,浪费空间
  • 每个结点不同构造,该结点有多少个度就有多少个指针域。虽然能节省空间,但是操作不方便
  • 使用顺序表 + 链表的形式。对于结点的保存使用顺序表,表内每个元素有自己的数据域和一个指针域,指针域指向一个链表,链表内的元素为该结点的子树的根结点的位置,链表结点只有一个数据域和一个指针域,数据域存储该子树根节点在顺序表当中的位置,指针域存储下一个子树根节点位置信息的链表结点

3.1.3 兄弟表示法

兄弟表示法也叫二叉树表示法,以二叉链表作为树的存储结构,表中的两个指针域分别指向该结点的第一个孩子、该节点的下一个兄弟

3.2 森林与二叉树的转化

  • 森林转化为二叉树
    • 将每棵树转化为二叉树
    • 将第一棵树的根节点作为森林的根结点
    • 还是按照普通树转化为二叉树的规则,把每棵已经转化为为二叉树的树看看成是一个普通的结点
    • 每棵树互为兄弟结点,从第一棵树的根结点一路顺着都放在右子树的位置
  • 二叉树转为森林
    • 根节点及其左子树看作一棵二叉树
    • 根节点的右子树根结点及其左子树看作一棵树
    • 根节点的 右子树的根节点 的右子树的根节点及其左子树看作一棵树
    • ...

3.3 树和森林的遍历

3.3.1 树的遍历

  • 先根遍历:相当于二叉树的先序遍历
  • 后根遍历:相当于二叉树的中序遍历

3.3.2 森林的遍历

  • 先序遍历森林
    • 如果森林不为空,按照下面的规则进行遍历
    • 访问森林中的第一棵树的根结点
    • 先序遍历第一棵树的根节点的子树森林
    • 先序遍历除了第一棵树之后的剩余树的子树森林
  • 中序遍历森林
    • 如果森林不为空,按照下面规则进行遍历
    • 中序遍历森林中第一棵树的根结点的子树森林
    • 访问第一棵树的根结点
    • 中序遍历除了第一棵树之后的剩余的树构成的森林

4 哈夫曼树

哈夫曼树又叫做最优树,是一类带权路径长度最短的树,在现实中有广泛的应用。

4.1 基本概念

  • 路径:从树中的一个结点到另一个结点之间的分支构成这两个结点之间的路径
  • 路径长度:路径上的分支数目叫做路径长度
  • 树的路径长度:从树到每一个结点的路径长度之和
  • :赋予某个实体的一个量,是对实体的某个或者某些属性的数值化描述,数据结构中,实体有结点和边两大类,对应有点权和边权两大类。结点权和边权各自代表什么意义,视情况而定
  • 结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积
  • 树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常计作WPL
  • 哈夫曼树:设有n个权值,可以构造一个含n个叶子的二叉树,每个叶子结点代表一个权值,其中带权路径长度最小的树就是最优二叉树,或者说哈夫曼树

4.2 哈夫曼树构造方法

所有的哈夫曼树,都是权值越大的越靠上

  • 给定n个权值{wi,wj,..,wn},构造n个只含有根结点的二叉树,这n个二叉树构成一个森林
  • 将二叉树按照根结点的权值从小到大排序
  • 从中取出权值最小的两棵二叉树构成一棵二叉树,新生成的一个根结点,这两棵二叉树作为左子树和右子树,根的权值为两棵子树根的权值之和,之后这棵二叉树重新加入森林,森林里的树依旧按根节点的权值从小到大排列
  • 重复上面两个步骤直到只剩下一棵树,这棵树就是哈夫曼树

构造哈夫曼树时优先选择权小的,这样权大的结点会更靠近根结点,权大的结点的带权路径和也就更小,树的带权路径和(WPL)也会跟着变小,构造过程是一种贪心选择的过程。

构造出来的哈夫曼树也不是唯一的,通过代码逻辑得到的哈夫曼树是确定的,但也有可能存在与其一样的带权路径长度的哈夫曼树,这种情况出现在原来的二叉树或者中间生成的二叉树中存在权值相同的情况

通过哈夫曼树的构造过程可以得知,哈夫曼树的每一个结点要么没有子结点,要么同时拥有左右两个结点,已知叶子结点有m个,即n0 = n,那么n2 = m - 1 ,一共有2m - 1个结点,这是根据二叉树的性质得来的

  • n = n0 + n1 + n2
  • n = 2n2 + n1 + 1
  • n0 = n2 + 1
  • n1 = 0, 故n = n0 + n2 = n0 + n0 - 1 = 2n0 - 1

4.3 哈夫曼树的存储方式

  • 可以用顺序表来存储,每个结点中包括权重,左子树根节点位置、右子树根结点位置、双亲结点位置,一共2n - 1个结点

  • 也可以用二叉链表的形式存储

4.4 哈夫曼编码

在数据通信、数据压缩当中,需要把数据文件转化为二进制字符0、1组成的二进制串,这个称为编码

4.4.1 等长编码

如果一个字符串只通过abcd四个字母组成,那么考虑等长编码,a是00,b是01,c是10,d是11,但这样并不是最优的编码方案

  • 编码方便
  • 每个字母出现的次数都不同,但每个字母确都享有相同的编码长度
  • 当字符数量一多,编码长度就更长了

4.4.2 前缀编码

考虑将频率低的字母用长的编码替代,频率高的字母用短的编码代替,构造一种不等长的编码,但这样会出现一个问题,那就是同一个编码后的串可以有多种不同的翻译方案,比如一个长的串,它可以翻译为一个高频字符,也可以翻译为几个低频字符的组合。

为了能够使用不等长的编码,必须满足,任意一个编码不能是另一个编码的前缀

  • 前缀编码: abcd分别为 1 、 01 、001 、000,这就满足了任意一个编码不是另一个编码的前缀
  • 非前缀编码:abcd分别为 1、10、011、0101,其中1就是10的前缀,不符合前缀编码,用这个编码会造成歧义

4.4.3 哈夫曼编码

我们发现哈夫曼树的带权路径和与编码的问题相挂钩

  • 哈夫曼树的带权路径和是最小的,而编码追求的就是最短编码长度
  • 哈夫曼的叶子结点都有一个权值,编码的字母也有一个出现的频数
  • 通过编码字母的权值构造哈夫曼树,这棵树的带权路径和就是编码的最短长度,具体的编码可以通过哈夫曼树得到,哈夫曼树连接左子树的边设为0,哈夫曼树连接右子树的边设为1,那么通过根到达叶子结点会有一个由01组成的编码,这个编码就是叶子结点的权重的字母对应的编码

哈夫曼编码的特点

  • 哈夫曼编码是前缀编码。从根到叶子结点的路径只有一条,叶子结点也不存在子结点,形成的编码不会是其他字母的编码的前缀
  • 哈夫曼编码是最优前缀码,通过哈夫曼编码能使文件压缩后得到的文件长度最短