1. 树的定义
定义
-
树(Tree)是 n(n>=0)个结点的有限集。n=0 时称为空树
-
在任意一颗非空树中:
- (1)有且仅有一个特定的称为根(root)的结点;
- (2)当 n>1 时,其余结点可分为 m(m>0)个互不相交的有限集 T1、T2、······、Tm,其中每一集合本身又是一棵树,并且称为根的子树(SubTree)
-
结点分类
- 树的结点包含一个数据元素及若干指向其子树的分支
- 结点拥有的子树数称为结点的度(Degree)
- 度为 0 的结点称为叶结点(Leaf)或终端结点
- 度不为 0 的结点称为非终端结点或分支结点
- 除根结点之外,分支结点也称为内部结点
- 树的度是数内各结点的度的最大值
结点间关系
- 结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)
- 同一个双亲的孩子之间互称兄弟(Sibling)
- 结点的祖先是从根到该结点所经分支上的所有结点
- 以某结点为根的子树中的任一结点都称为该结点的子孙
树的其他相关概念
- 结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层
- 其双亲在同一层的结点互为堂兄弟
- 树中结点的最大层次称为树的深度(Depth)或高度
- 如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树
- 森林(Forest)是 m(m>=0)棵互不相交的树的集合
- 对树中每个结点而言,其子树的集合即为森林
线性表与树的结构的不同
-
线性结构
- 第一个数据元素:无前驱
- 最后一个数据元素:无后继
- 中间元素:一个前驱一个后继
-
树结构
- 根结点:无双亲,唯一
- 叶结点:无孩子,可以多个
- 中间结点:一个双亲多个孩子
2. 树的抽象数据类型
ADT 树(tree)
Data 树是由一个根节点和若干棵子树构成。树中结点具有相同数据类型及层次关系。
Operation InitTree(*T); 构造空树T DestroyTree(*T); 销毁树T CreateTree(*T, definition); 按definition中给出树的定义来构造树 ClearTree(*T); 若树T存在,则将树T清空为空树 TreeEmpty(T); 若T为空树,返回true,否则返回false TreeDepth(T); 返回T的深度 Root(T); 返回T的根结点 Value(T, cur_e); cur_e是树T中一个结点,返回此结点的值 Assign(T, cur_e, value); 给树T的结点cur_e赋值为value Parent(T, cur_e); 若cur_e是树T的非根结点,则返回它的双亲,否则返回空 LeftChild(T, cur_e); 若cur_e是树T的非叶结点,则返回它的双亲,否则返回空 RightSibling(T, cur_e); 若cur_e有右兄弟,则返回它的右兄弟,否则返回空 InsertChild(*T, *p, i, c); 其中p指向树T的某个结点,i为所指结点p的度加上1,非空树c与T不相交, 操作结果为 插入c为树T中p指结点的第i棵子树 DeleteChild(*T, *p, i); 其中p指向树T的某个结点,i为所指结点p的度, 操作结果为 删除T中p所指结点的第i棵子树
endADT
3. 树的存储结构
存储结构的设计是一个非常灵活的过程。一个存储结构设计得是否合理,取决于基于该存储结构的运算是否合适、是否方便,时间复杂度好不好等
双亲表示法
- 以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点在链表中的位置
- data 是数据域,存储结点的数据信息;parent 是指针域,存储该结点的双亲在数组中的下标
- 根结点没有双亲,所以约定根结点的位置域设置为 -1
- 增加一个结点最左边孩子的域,叫长子域(firstchild),这样可以容易得到结点的孩子,如果没有孩子的结点,这个长子域就设置为 -1
- 右兄弟域来体现兄弟关系,存在右兄弟,记录右兄弟的下标;不存在则赋值为 -1
孩子表示法
-
每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这种方法叫做多重链表表示法
-
树的每个结点的度,也就是它的孩子个数是不同的
-
方案一
- 指针域的个数就等于树的度
- data 是数据域,child1 到 childd 是指针域,用来指向该结点的孩子结点
- 缺点:树中各结点的度相差很大时,浪费空间
-
方案二
- 每个结点指针域的个数等于该结点的度,取一个位置来存储结点中指针域的个数,child1 到 childd 是指针域,用来指向该结点的孩子结点
-
-
把每个结点的孩子结点排列起来,以单链表做存储结构,则 n 个结点有 n 个孩子链表,如果是叶子结点则此单链表为空。然后 n 个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中
-
-
为此。设计两种结点结构
- 孩子链表的孩子结点;child 是数据域,用来存储某个结点在表头数组中的下标;next 是指针域,用来存储指向某结点的下一个孩子结点的指针
- 表头数组的表头结点;data 是数据域,存储某结点的数据信息;firstchild 是头指针域,存储该结点的孩子链表的头指针
-
-
双亲孩子表示法:表示出结点的双亲
孩子兄弟表示法
- 任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的
- 因此我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟
- data 是数据域,firstchild 为指针域,存储该结点的第一个孩子结点的存储地址,rightsib 是指针域,存储该结点的右兄弟结点的存储地址
4. 二叉树的定义
定义
- 二叉树(Binary Tree)是 n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的,分别称为根结点的左子树和右子树的二叉树组成
特点
- 每个结点最多有两棵子树,所以二叉树中不存在度大于 2 的结点,注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树
二叉树具有物种基本形态
- 空二叉树
- 只有一个根结点
- 根结点只有左子树
- 根结点只有右子树
- 根结点既有左子树又有右子树
特殊二叉树
-
斜树
- 所有的结点都只有左子树的二叉树叫左斜树
- 所有的结点都只有右子树的二叉树叫右斜树
- 这两者统称为斜树
- 特点:每一层都只有一个结点,结点的个数与二叉树的深度相同
- 其实线性表结构就可以理解为是树的一种特殊的表现形式
-
满二叉树
-
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树
-
特点
- 叶子只能出现在最下一层,出现在其他层就不可能达成平衡
- 非叶子结点的度一定是 2
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多
-
-
-
完全二叉树
-
对一棵具有 n 个结点的二叉树按层序编号,如果编号为 i (1 <= i <= n)的结点与同样深度的满二叉树中编号为 i 的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树
-
特点
- 叶子结点只能出现在最下两层
- 最下层的叶子一定集中在左部连续位置
- 倒数二层,若有叶子结点,一定都在右部连续位置
- 如果结点度为 1 ,则该结点只有左孩子,即不存在只有右子树的情况
- 同样结点数的二叉树,完全二叉树的深度最小
-
5. 二叉树的性质
性质1:在二叉树的第 i 层上至多有 2i-1 个结点(i >= 1)
性质2:深度为 k 的二叉树至多有 2k-1 个结点(k >= 1)
- 深度为 k 意思就是有 k 层的二叉树
性质3:对任何一棵二叉树 T,如果其终端结点数为 n0,度为 2 的结点数为 n2,则 n0 = n2 + 1
性质4:具有 n 个结点的完全二叉树的深度为「log2n」+ 1(「x」表示不大于 x 的最大整数)
性质5:如果对一棵有 n 个结点的完全二叉树的结点按层编号,对任一结点 i(1 <= i <= n)有:
- 如果 i = 1,则结点 i 是二叉树的根,无双亲;如果 i>1,则其双亲是结点「i/2」
- 如果 2i > n,则结点 i 无左孩子(结点 i 为叶子结点);否则其左孩子是结点 2i
- 如果 2i+1 > n,则结点 i 无右孩子;否则其右孩子是结点 2i + 1
6. 二叉树的存储结构
二叉树顺序存储结构
- 一般只用于完全二叉树
二叉链表
- 二叉树每个结点最多有两个孩子,所以为它设置一个数据域和两个指针域
- data 是数据域,lchild 和 rchild 都是指针域
三叉链表
- 在二叉链表的基础上增加一个指向其双亲的指针域
7. 遍历二叉树
遍历原理
- 从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次
遍历方法
-
前序遍历
- 规则:若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树(根左右)
-
中序遍历
- 规则:若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树(左根右)
-
后序遍历
- 规则:若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点(左右根)
- 层序遍历
- 规则:若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问
遍历性质
- 已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树
- 已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树
- 已知前序和后序遍历,是不能确定确定一棵二叉树的
8. 二叉树的建立
为了能让每个结点确认是否有左右孩子,我们对它进行了扩展
将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,比如“#”
我们称这种处理后的二叉树为原二叉树的扩展二叉树
扩展二叉树就可以做到一个遍历序列确定一棵二叉树
9. 线索二叉树
原理
- 我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树
- 我们对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化
如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择
10. 树、森林与二叉树的转换
树转换为二叉树
- 加线。在所有兄弟结点之间加一条连线
- 去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
- 层次调整。以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子时二叉树的左孩子,兄弟转换过来的孩子是结点的右孩子
森林转换为二叉树
- 把每个树转换为二叉树
- 第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树
二叉树转换为树
- 加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、······,就是左孩子的 n 个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来
- 去线。删除原二叉树中所有结点与其右孩子结点的连线
- 层次调整。使之结构层次分明
二叉树转换为森林
-
判断
- 二叉树的根结点有没有右孩子,有就是森林
-
从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除······,直到所有右孩子连线都删除为止,得到分离的二叉树
-
再将每棵分离后的二叉树转换为树即可
树和森林的遍历
-
树的遍历
- 第一种:先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树
- 第二种:后根遍历,即先依次后根遍历每棵子树,然后再访问根结点
-
森林的遍历
-
前序遍历
- 先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余数构成的森林
-
后序遍历
- 先访问森林的第一棵树,后根遍历的方式遍历每棵子树,然后在访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林
-
11. 赫夫曼树及其应用
定义和原理
-
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度
-
树的路径长度就是从树根到每一结点的路径长度之和
-
带权的结点
- 结点的带权的路径长度为:从该结点到树根之间的路径长度与结点上权的乘积
- 树的带权路径长度为树中所有叶子结点的带权路径长度之和
- 假设有 n 个权值{w1,w2,···,wn},构造一棵有 n 个叶子结点的二叉树,,每个叶子结点带权 wk,每个叶子的路径长度为lk
- 其中带权路径长度 WPL 最小的二叉树称做赫夫曼树,也称最优二叉树
构造算法描述
- (1)根据给定的 n 个权值{w1,w2,···,wn}构成 n 棵二叉树的集合 F={T1,T2,···,Tn},其中每棵二叉树 Ti 中只有一个带权为 wi 根结点,其左右子树均为空
- (2)在 F 中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点阿权值为其左右子树上根结点的权值之和
- (3)在 F 中删除这两棵树,同时将新得到的二叉树加入 F 中
- (4)重复 2 和 3 步骤,直到 F 只含一棵树为止,这棵树便是赫夫曼树
赫夫曼编码
- 一般地,设需要编码的字符集为{d1,d2,···,dn},各个字符在电文中出现的次数或频率集合为{w1,w2,···,wn},以 d1,d2,···,dn 作为叶子结点,以 w1,w2,···,wn 作为相应叶子结点的权值来构造一棵赫夫曼树
- 规定赫夫曼树的左分支代表 0,右分支代表 1,则从根结点到叶子结点所经过的路径分支组成的 0 和 1 的序列便为该结点对应字符的编码,这就是赫夫曼编码