前言
本章内容比较多,需要多做练习
1 树和二叉树的定义
树型结构(非线性结构):
(1)结点之间有分支
(2)具有层次关系
1.1 树的定义
树(Tree)是n(n≥0)个结点的有限集。
若n = 0,称为空树;
若n>0,则它满足如下两个条件:
(1)有且仅有一个特定的称为根(Root)的结点
(2)其余结点可分为m(m≥0)个互不相交的有限集T1,T2,T3,…,Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)
显然,树的定义是一个递归的定义
树的其它表示方式
嵌套集合、凹入表示、广义表
1.2 树的基本术语
结点:数据元素以及指向子树的分支
根结点:非空树中无前驱结点的结点
结点的度:结点拥有的子树数
度 = 0:叶子,终端结点
度 ≠ 0:分支结点,非终端结点,根结点以外的分支结点称为内部结点
树的度:树内各结点的度的最大值
树的深度:树中结点的最大层次
结点的子树的根称为该结点的孩子,该结点称为孩子的双亲
兄弟:同一个双亲的结点
堂兄弟:双亲在同一层的结点
结点的祖先:从根到该结点所经分支上的所有结点
结点的子孙:以某结点为根的子树中的任意结点
有序树:树中结点的各子树从左至右有次序(最左边的为第一个孩子)
无序树:树中结点的各子树无次序
森林:是m(m≥0)棵互不相交的树的集合。
把根结点删除,树就变成了森林,一棵树可以看成是一个特殊的森林,给森林中的各子树加上一个双亲结点,森林就变成了树
树结构和线性结构的比较
线性结构 | 树结构 |
---|---|
第一个数据元素,无前驱 | 根结点(只有一个),无双亲 |
最后一个数据元素,无后继 | 叶子结点(可以有多个),无孩子 |
其他数据元素,一个前驱一个后继 | 其他结点即中间结点,一个双亲,多个孩子 |
一对一 | 一对多 |
1.3 二叉树的定义
为何要重点研究每结点最多只有两个“叉”的树?
(1)二叉树的结构最简单,规律性最强
(2)可以证明,所有树都能转为唯一对应的二叉树,不失一般性
普通树(多叉树)若不转化为二叉树,则运算很难实现
二叉树在树结构的应用中起着非常重要的作用,因为对二叉树的许多操作算法简单,而任何树都可以与二叉树相互转换,这样就解决了树的存储结构及其运算中存在的复杂性
二叉树是n(n≥0)个结点的有限集,它或者是空集(n = 0),或者由一个根结点及两颗互不相交的分别称作这个根的左子树和右子树的二叉树组成
特点:
(1)每个结点最多有俩孩子(二叉树中不存在度大于2的结点)
(2)子树有左右之分,其次序不能颠倒
(3)二叉树可以是空集合,根可以有空的左子树或空的右子树
注!二叉树不是树的特殊情况,它们是两个概念
二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也进行区分,说明它是左子树还是右子树
树当结点只有一个孩子时,就无须区分它是左还是右的次序。因此二者是不同的,这是二叉树与树的最主要的差别
也就是二叉树每个结点位置或者说次序都是固定的,可以是空,但是不可以说它没有位置;而树的结点位置是相对于别的结点来说的,没有别的结点时,它就无所谓左右了,虽然二叉树与树概念不同,但有关树的基本术语对二叉树都适用
2 案例引入
案例1:数据压缩问题
将数据文件转换成由0、1组成的二进制串,称之为编码(哈夫曼树编码)
案例2:利用二叉树求解表达式的值
以二叉树表示表达式的递归定义如下:
(1)若表达式为数或简单变量,则相应二叉树中仅有一个根结点,其数据域存放该表达式信息
(2)若表达式为“第一操作数 运算符 第二操作数”的形式,则相应的二叉树中以左子树表示第一操作数,右子树表示第二操作数,根结点的数据域存放运算符(若为一元运算符,则左子树为空),其中,操作数本身又为表达式
3 树和二叉树的抽象数据类型定义
ADT BinaryTree{
数据对象:
D是具有相同特性的数据元素的集合
数据关系:
若D = Φ,则R = Φ;
若D ≠ Φ,则R = {H};H是如下二元关系:
①root唯一 //关于根的说明
②Dj∩Dk = Φ //关于子树不相交的说明
③… //关于数据元素的说明
④… //关于左子树和右子树的说明
基本操作:
… //至少20个
} ADT Array
4 二叉树的性质和存储结构
4.1 二叉树的性质
性质1:在二叉树的第i层上至多有2^(i-1)个结点(i≥1)
第i层上至少有1个结点
性质2:深度为k的二叉树至多有2^k-1个结点(k≥1)
深度为k的二叉树至少有k个结点
性质3:对任何一颗二叉树T,如果其叶子数为n0,度为2的结点数为n2,则n0 = n2 + 1(利用边数来证明)
性质4:具有n个结点的完全二叉树的深度为└log2n┘+1,注:└x┘称作x的底,表示不大于x的最大整数
性质4表明了完全二叉树结点数n与完全二叉树深度k之间的关系
性质5:如果对一棵有n个结点的完全二叉树(深度为└log2n┘+1)的结点按层序编号(从第1层到第└log2n┘+1层,每层从左到右),则对任一结点i(1≤i≤n),有:
(1)如果i=1,则结点i是二叉树的根,无双亲;如果i大于1,则其双亲是结点└i/2┘
(2)如果2i>n,则结点i为叶子结点,无左孩子;否则,其左孩子是结点2i
(3)如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1
性质5表明了完全二叉树中双亲结点编号与孩子结点编号之间的关系
两种特殊形式的二叉树
满二叉树
一棵深度为k且有2^k-1个结点的二叉树称为满二叉树
特点:
(1)每一层上的结点数都是最大结点数(即每层都满)
(2)叶子节点全部在最底层
对满二叉树结点位置进行编号
(1)编号规则:从根结点开始,自上而下,自左而右
(2)每个结点位置都有元素
满二叉树在同样深度的二叉树中结点个数最多
满二叉树在同样深度的二叉树中叶子结点个数最多
完全二叉树
深度为k的具有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树
在满二叉树中,从最后一个结点开始,连续去掉任意个结点,即是一棵完全二叉树。一定是连续的去掉!!
特点:
(1)叶子只可能分布在层次最大的两层上
(2)对任一结点,如果其右子树的最大层次为i,则其左子树的最大层次必为i或i+1
4.2 二叉树的顺序存储结构
实现:按满二叉树的结点层次编号,依次存放二叉树中的数据元素
二叉树顺序存储表示
#define MAXTSIZE 100
Typedef TElemType SqBiTree[MAXTSIZE]
SqBiTree bt;
二叉树的顺序存储缺点:
最坏情况:深度为k的且只有k个结点的单支树需要长度为2^k-1的一维数组
特点:
结点间关系蕴含在其存储位置中(优点)
浪费空间,适于满二叉树和完全二叉树
4.3 二叉树的链式存储结构—二叉链表
二叉树结点的特点:有一个双亲,两个孩子
结点结构:每个结点有两个指针域,分别指向左孩子和右孩子
二叉链表存储结构
二叉树链式存储表示
Typedef struct BiNode{
TElemType data;
struct BiNode *lchild,*rchild; //左右孩子指针
}BiNode,*BiTree;
在n个结点的二叉链表中,有n+1个空指针域
分析:必有2n个链域。除根结点外,每个结点有且仅有一个双亲,所以只会有n-1个结点的链域存放指针,指向非空子女结点
空指针数目 = 2n - (n - 1) = n + 1
4.4 二叉树的链式存储结构—三叉链表
结点结构:每个结点有三个指针域,分别指向左孩子、双亲和右孩子
二叉树链式存储表示
Typedef struct TriTNode{
TElemType data;
struct BiNode *lchild,*parent,*rchild; //左右孩子指针
}TriTNode,*TriTNode;
5 遍历二叉树和线索二叉树
5.1 遍历二叉树
遍历定义—顺着某一条搜索路径巡访二叉树中的结点,使得每个结点均被访问一次,而且仅被访问一次(又称周游), “访问”的含义很广,可以是对结点作各种处理,如:输出结点的信息、修改结点的数据值等,但要求这种访问不破坏原来的数据结构
遍历目的—得到树中所有结点的一个线性排列
遍历用途—它是树结构插入、删除、修改 、查找和排序运算的前提,是二叉树一切运算的基础和核心
遍历二叉树算法描述
遍历方法:依次遍历二叉树中的三个组成部分,便是遍历了整个二叉树
假设:L 遍历左子树 D 访问根结点 R遍历右子树
则遍历整个二叉树方案共有:DLR、LDR、LRD、DRL、RDL、RLD六种
若规定先左后右,则只有前三种情况:
DLR—先(根)序遍历
LDR—中(根)序遍历
LRD—后(根)序遍历
由二叉树的递归定义可知,遍历左子树和遍历右子树可如同遍历二叉树一样“递归”进行
先序遍历二叉树的操作定义
若二叉树为空,则空操作;否则
(1)访问根结点
(2)先序遍历左子树
(3)先序遍历右子树
中序遍历二叉树的操作定义
若二叉树为空,则空操作;否则
(1)中序遍历左子树
(2)访问根结点
(3)中序遍历右子树
后序遍历二叉树的操作定义
若二叉树为空,则空操作;否则
(1)后序遍历左子树
(2)后序遍历右子树
(3)访问根结点
5.2 根据遍历序列确定二叉树
若二叉树中各结点的值均不相同,则二叉树结点的先序序列、中序序列和后序序列都是唯一的
由二叉树的先序序列和中序序列,或由二叉树的后序序列和中序序列可以确定唯一一棵二叉树
分析:
(1)由先序序列确定根;由中序序列确定左右子树
(2)由先序序列确定根;由后序序列确定左右子树(后序遍历,根结点必在后序序列尾部)
5.3 二叉树遍历算法(略)
主要是用到了递归的思想
5.4 线索二叉树
问题:为什么要研究线索二叉树?
当用二叉链表作为二叉树的存储结构时,可以很方便地找到某个结点的左右孩子;但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结点
提出的问题:如何寻找特定遍历序列中二叉树结点的前驱和后继?
解决的方法:
(1)通过遍历寻找—费时间
(2)再增设前驱、后继指针域—增加了存储负担
(3)利用二叉链表中的空指针域
利用二叉链表中的空指针域:
如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某结点的右孩子为空,则将空的右孩子指针域改为指向其后继。这种改变指向的指针称为“线索”,加上了线索的二叉树称为线索二叉树(Threaded Binary Tree),对二叉树按某种遍历次序使其变为线索二叉树的过程叫线索化
为了区分Ichild和rchild指针到底是指向孩子的指针,还是指向前驱或者后续的指针,对二叉链表中每个结点增设两个标志域ltag和rtag,并约定:
ltag = 0 lchild 指向该结点的左孩子
ltag = 1 lchild 指向该结点的前驱
rtag = 0 rchild 指向该结点的右孩子
rtag = 1 rchild 指向该结点的后继
6 树和森林
树(Tree)是n(n≥0)个结点的有限集。
若n = 0,称为空树;
若n>0,则它满足如下两个条件:
(1)有且仅有一个特定的称为根(Root)的结点
(2)其余结点可分为m(m≥0)个互不相交的有限集T1,T2,T3,…,Tm,其中每一个集合本身又是一棵树,并称为根的子树(SubTree)
森林:是m(m≥0)棵互不相交的树的集合
树和森林可以进行相互转换,树的根结点去掉就是一个森林
6.1 树的存储结构
(1)双亲表示法
实现:定义结构数组,存放树的结点,每个结点含两个域
数据域:存放结点本身信息
双亲域:指示本结点的双亲结点在数组中的位置
根结点的双亲域写-1
还需要两个参数:r = 0(用来标记根结点的位置) n = 10(用来标记元素的个数)
特点:找双亲容易,找孩子难
(2)孩子链表
把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储
则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储
特点:找孩子容易,找双亲难
(3)带双亲的孩子链表
(4)孩子兄弟表示法(二叉树表示法,二叉链表表示法)
实现:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点
6.2 树与二叉树的转换
将树转化为二叉树进行处理,利用二叉树的算法来实现对树的操作,由于树和二叉树都可以用二叉链表作存储结构,则以二叉链表作媒介可以导出树与二叉树之间的一个对应关系
树:左指针放第一个孩子,右指针放下一个兄弟
二叉树:左指针放左孩子,右指针放右孩子
给定一棵树,可以找到唯一的一棵二叉树与之对应
将树转换成二叉树:兄弟相连留长子
(1)加线:在兄弟之间加一连线
(2)抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
(3)旋转:以树的根结点为轴心,将整树顺时针转45°
将二叉树转换成树:左孩右右连双亲,去掉原来右孩线
(1)加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子…沿分支找到的所有右孩子,都与p的双亲用线连起来
(2)抹线:抹掉原二叉树中双亲与右孩子之间的连线
(3)调整:将结点按层次排列,形成树结构
6.3 森林与二叉树的转换
森林转换成二叉树(二叉树与多棵树之间的关系):树变二叉根相连
(1)将各棵树分别转换成二叉树
(2)将每棵树的根结点用线相连(变右孩子)
(3)以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
二叉树转换成森林:去掉全部右孩线,孤立二叉再还原
(1)抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树
(2)还原:将孤立的二叉树还原成树
6.4 树与森林的遍历
树的遍历(三种方式)
先根(次序)遍历
若树不空,则先访问根结点,然后依次先根遍历各棵子树
后根(次序)遍历
若树不空,则先依次后根遍历各棵子树,然后访问根结点
按层次遍历
若树不空,则自上而下自左至右访问树中每个结点
森林的遍历
将森林看作由三部分构成:
(1)森林中第一棵树的根结点
(2)森林中第一棵树的子树森林
(3)森林中其它树构成的森林
先序遍历:
若森林不空,则
(1)访问森林中第一棵树的根结点
(2)先序遍历森林中第一棵树的子树森林
(3)先序遍历森林中(除第一棵树之外)其余树构成的森林
即:依次从左至右对森林中的每一棵树进行先根遍历
中序遍历:
若森林不空,则
(1)中序遍历森林中第一棵树的子树森林
(2)访问森林中第一棵树的根结点
(3)中序遍历森林中(除第一棵树之外)其余树构成的森林
即:依次从左至右对森林中的每一棵树进行后根遍历
7 哈夫曼树及其应用
7.1 哈夫曼树的基本概念
判断树:用于描述分类过程的二叉树
路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
结点的路径长度:两结点间路径上的分支数
树的路径长度:从树根到每一个结点的路径长度之和。记作:TL
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
权(weight):将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和
记作:WPL(Weighted Path Length)
哈夫曼树:最优树 带权路径长度(WPL)最短的树
注:“带权路径长度最短”是在“度相同”的树中比较而得到的结果,因此有最优二叉树、最优三叉树之称等等
最优二叉树 带权路径长度(WPL)最短的二叉树
(1)满二叉树不一定是哈夫曼树
(2)哈夫曼树中权越大的叶子离根越近
(3)具有相同带权结点的哈夫曼树不唯一
7.2 哈夫曼树的构造算法
根据7.1(2)哈夫曼树中权越大的叶子离根越近,提出贪心算法:构造哈夫曼树时首先选择权值小的叶子结点
哈夫曼算法(构造哈夫曼树的方法)
(1)根据n个给定的权值{w1,w2,…,wn}构成n棵二叉树的森林F={T1,T2,…,Tn},其中Ti只有一个带权为wi的根结点
(2)在F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和
(3)在F中删除这两棵树,同时将新得到的二叉树加入森林中
(4)重复(2)和(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树
哈夫曼算法口诀:1、构造森林全是根;2、选用两小造新树;3、删除两小添新人;4、重复2、3剩单根
(1)哈夫曼树的结点的度数为0或2,没有度为1的结点(度为0有n个,度为2有n-1个)
(2)包含n个叶子结点的哈夫曼树中共有2n-1个结点
(包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点)
总结:
(1)在哈夫曼算法中,初始时有n棵二叉树,要经过n-1次合并最终形成哈夫曼树
(2)经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点
可见:哈夫曼树中共有n+n-1 = 2n-1个结点,且其所有的分支结点的度均不为1
7.3 哈夫曼编码
前缀编码:要设计长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀
什么样的前缀码能使得电文总长最短?----哈夫曼编码
(1)统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)
(2)利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树,则概率越大的结点,路径越短
(3)在哈夫曼树的每个分支上标上0或1:
结点的左分支标0,右分支标1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码
两个问题
(1)为什么哈夫曼编码能够保证是前缀编码?
因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀
(2)为什么哈夫曼编码能够保证字符编码总长最短?
因为哈夫曼树的带权路径长度最短,故字符编码的总长最短
性质
(1)哈夫曼编码是前缀码
(2)哈夫曼编码是最优前缀码