数据结构复习笔记(5)

第六章 树和二叉树

基本定义

树是零个或多个结点的有限集,任意一棵非空树具有以下性质

  • 有且仅有一个特定的结点称为根结点
  • 当结点数大于1时,其余结点可以分为1个或多个互不相交的有限集,称为根结点的子树

树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的子树数目称为该结点的;度为0的结点称为叶子/终端结点,度不为0的结点称为非终端结点/分支结点

结点的子树的根称为该结点的孩子,该结点则为其孩子的双亲,同一个双亲的孩子之间互称兄弟,结点的祖先是从整棵树的根到该结点所经分支上的所有结点,以某结点为根的子树中的任一结点都是该结点的子孙

结点的层次是从根开始计算的,规定根为第一层,根的孩子为第二层。即若某结点为第n层,则其子树的根在第n+1层。双亲在同一层的结点互称为堂兄弟。树中结点的最大层次称为树的深度

若树中结点的各子树从左到右是有序的、不能互换,该树称为有序树,否则称为无序树

森林是0棵或多棵互不相交的树的集合。对树中的每个结点,其各子树的集合就是一个森林

二叉树是每个结点至多只有两棵子树的树,二叉树的子树有左右之分,顺序不能调换

特殊的二叉树

满二叉树

深度为k且有2k1个结点的二叉树为满二叉树。很显然,满二叉树中每一层的结点数都是该层的最大结点树

完全二叉树

深度为k,有n个结点的二叉树,其每个结点都与深度为k的满二叉树中编号(从上到下、从左到右)为1到n的结点一一对应,该二叉树称为完全二叉树。直观地看,就是在满二叉树的基础上,缺失最后一层的最右边若干结点的二叉树

基本性质

  1. 二叉树的第i层上至多有2i1 (i1)个结点

  2. 深度为k的二叉树至多有2k1个结点

  3. 对于任何树,每个结点可以对应一条分支线(除了根结点),因此就有一个计算公式=+1

    inikNN=1+i=1kini

  4. 具有n个结点的完全二叉树的深度为log2n+1

  5. 在满二叉树(完全二叉树)从上到下、从左到右的编号中,某一个结点的编号为i,则其左孩子编号为2i,其右孩子编号为2i+1。记整棵树的结点数为n,若2i>n,则该结点无左孩子;若2i+1>n,则该结点无右孩子

具体实现

顺序存储实现

简单地使用满二叉树的编号规则将结点对应到一维分量中即可,对于空的(不存在)结点,存为一个特殊的符号即可

链式存储实现

一个结点中包括一个数据域和两个指针域,指针域分别指向该结点的左右孩子结点,这种结构称为二叉链表;也可以使用三个指针域,其中一个指向双亲结点,这种称为三叉链表

线索二叉树

在普通的链式存储的二叉树中会有一些结点因为没有左/右孩子导致指针域为空,可以利用这个指针域使原本为空的左指针指向某种遍历形式的前驱、右指针指向后继。为区分是指向前驱/后继还是左/右孩子,额外添加两个域来标记

线索二叉树结点结构图

一般来说,在线索二叉树中也设一个头结点,其左指针指向二叉树根结点,右指针指向指定遍历序列顺序的最后一个结点

在将二叉树线索化时,需要通过在遍历中修改指针域内容来实现。需要一个指针pre来保持指向当前遍历到的结点的前驱,在遍历过程中根据结点是否有左/右孩子结点来修改指针域内容。以中序线索化为例,线索化的递归函数如下

void InThreading(nodePt p) {
if (p) {
InThreading(p->lchild);//递归线索化p的左子树
if (!p->lchild) {//没有左孩子,就使左指针指向前驱结点
p->LTag = Thread;
p->lchild = pre;
}
if (!pre->rchild) {//没有右孩子,就是右指针指向后继结点
pre->RTag = Thread;
pre->rchild = p;
}
InThreading(p->rchild);//递归线索化p的右子树
}
}

由于只能处理pre的后继,最后一个结点需要在递归函数外进行特殊处理。

树的链式存储方法

双亲表示法

每个结点中,设置一个数据域,一个指针域,指针指向该结点的双亲结点

孩子表示法

树的所有结点用链式存储线性表存储,该线性表中的结点设置一个数据域,一个指针域。指针域为一链表,内容为指向该结点的孩子结点的指针

将双亲表示法和孩子表示法结合起来,在孩子表示法的结点中增加一个指针域,指向该结点的双亲结点即可

孩子兄弟表示法

使用二叉链表结构,即一个数据域、两个指针域,两个指针域分别指向该结点的第一个孩子结点和自身的下一个兄弟结点。这种存储结构非常易于找结点孩子的操作。

二叉树相关算法

遍历二叉树

遍历二叉树有三种形式,可以很容易地用递归实现,下为各遍历形式的访问顺序

  • 先序遍历:根、左子树、右子树
  • 中序遍历:左子树、根、右子树
  • 后序遍历:左子树、右子树、根

将一般树、森林转换为二叉树

任意树转换为二叉树

根据“左孩子、右兄弟”的规则进行转换即可,任意树转换为二叉树后,根结点一定没有右子树

森林转换为二叉树

将森林中各棵互不相交的树视为兄弟,再进行转换即可。

二叉树转换示意图

显然,将森林转换成二叉树,对这棵二叉树的先序、中序遍历对应了森林的先序、中序遍历

回溯法

回溯法是一种递归的设计方法,回溯法对问题的求解过程其实是对问题的“状态树”的先序遍历过程。

回溯法可以用于求幂集(状态树是一棵满二叉树)、x皇后问题(剪枝的x叉树)等

树的计数

二叉树相似的递归定义:若两棵二叉树都为空树或都不为空树,且它们的左右子树分别相似,则这两棵二叉树相似

二叉树等价指两棵二叉树不仅相似,而且所有对应结点上的数据元素都相同

记具有n个结点、互不相似的二叉树的数目为bn。一棵具有n(n>1)个结点的二叉树可以看成由一个根结点、一棵有i个结点的左子树、一棵有n-i-1个结点的右子树组成的,左子树有bi种、右子树有bni1n1种,则共有bibni1种。因此可得递推公式

{b0=1bn=i=0n1bibni1, n1

根据数学计算,含有n个结点的不相似的二叉树共有1n+1C2nn

任意一棵树可以转换为唯一的一棵没有右子树的二叉树,若树有n个结点,将有n-1个结点的二叉树的所有形态作为该树转换成的唯一二叉树的左子树,就可以对应n个结点的树的一种形态。因此,具有n个结点的不同形态的(有序)树的数目tn=bn1

二叉树的应用

表达式的表示

可以用二叉树表示一个表达式,对该二叉树的先序、中序、后序遍历分别对应该表达式的前缀表示(波兰式)、中缀表示、后缀表示(逆波兰式)

哈夫曼树

基本定义

从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径,这条路径上的分支数目称为路径长度

树的路径长度指从树根到每一个结点的路径长度的和。对于不带权的路径长度,最短的就是完全二叉树;对于带权路径长度WPL(Weighted Path Length)最小的,称为最优二叉树/哈夫曼树

在哈夫曼树中,一般将带权的结点作为叶子来计算WPL。哈夫曼树中没有度为1的结点,是一棵严格的/正则的二叉树。有n个叶子的哈夫曼树共有2n-1个结点

哈夫曼树构造算法

哈夫曼算法

将所有结点都视为二叉树(独立的结点视为只有根的二叉树)。在其中找到根权值最小的两棵二叉树,把这两棵树作为左右子树构成一棵新的二叉树,根的权值为刚刚选出的两棵二叉树的根的权值之和。删除选出的两棵二叉树,并把刚刚构造好的新二叉树加入原来的二叉树集合中。不断重复“挑选—组合”的过程,直到二叉树集合中剩余一棵,这棵二叉树就是哈夫曼树

哈夫曼树的应用
  1. 最佳判定算法

    利用哈夫曼树得到最佳判定算法,对于有大量分支判断的可以缩短判定过程

  2. 哈夫曼编码

    编码,就是将要传输的字符与二进制字符转换。哈夫曼编码使用长短不一的前缀编码,即任何一个字符的编码都不是另一个字符编码的前缀。将要编码的字符作为二叉树的叶子,一般将左分支记为0、右分支记为1,就可以得到前缀编码

    要得到哈夫曼编码,就是把要编码的字符作为结点,其权值为该字符在电文中出现的频率

    在求哈夫曼树时,可以使用一维数组存储结点,每个结点中除了权重还包括指示双亲、孩子的指针域。由于使用一维数组存储,使用下标作为“指针”即可

posted @   GeorgeHu6  阅读(190)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署
点击右上角即可分享
微信分享提示