第五章 树与二叉树
从本章开始学习非线性数据结构,树作为一类重要的一对多的数据结构的代表,以分支结构关系定义层次结构,在现实生活中很多关系都可以用树形结构表示,其中二叉树更是最常出现的一种表现方式。
因此,在考试过程中,会涉及选择题(几率大)、填空题、综合应用题和算法题各方面,同时所占分值较大,要对本章内容重点把握。
【考点】①树的基本概念
②二叉树: 掌握二叉树的定义及其主要特征;
二叉树的顺序存储结构和链式存储结构;
熟练掌握二叉树的遍历;
了解线索二叉树的基本概念和构造。
③树、森林:树的存储结构;
森林与二叉树的转换;
树和森林的遍历。
④树与二叉树的应用:二叉排序树;平衡二叉树;
掌握哈夫曼树和哈夫曼编码的实现方法。
【本章大纲】
【目录】
一、树的相关概念
1.1 树的定义
【定义】树(Tree):是n(n≥0)个结点的有限集,它或为空树(n = 0);或为非空树,对于非空树T:
①有且仅有一个称之为根的结点;
②除根结点以外的其余结点可分为m(m>0)个互不相交的有限集T1, T2, …, Tm, 其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
1.2 树的相关术语
【根】指在树中没有前驱的结点称之为根节点(如上图中A结点)。
【叶子】即终端结点,没有后继的结点称之为叶子结点(如上图K、L、F、G、M、I、J结点)。
【森林】指m棵不相交的树的集合(例如删除A后以B、C、D为根节点的三棵树所组合而成的就是森林)。
【有序树】指结点各子树从左至右有序,不能互换的树称之为有序树。
【无序树】指结点各子树可互换位置,称之为无序树。。
【双亲】本结点上层的结点(直接前驱),(如上图B结点是E结点的双亲)。
【孩子】即下层结点的子树的根(直接后继)(如上图B结点的孩子是E、F结点)。
【兄弟】同一双亲下的同层结点(孩子之间互称兄弟)(如上图中E、F两结点为兄弟结点)。
【堂兄弟】即双亲位于同一层的结点(但并非同一双亲)(如上图中E、G两结点为堂兄弟结点)。
【祖先】即从根到该结点所经分支的所有结点(如上图中B结点为K、L结点的祖先结点)。
【子孙】即该结点下层子树中的任一结点(如上图中K、L结点为B结点的子孙结点)。
【结点的度】结点挂接的子树数(如上图中E结点的度为2)。
【结点的层次】从根到该结点的层数(根结点算第一层)(如上图B结点的层次为2)。
【终端结点】即度为0的结点,即叶子。(如上图K、L、F、G、M、I、J结点)。
【分支结点】即度不为0的结点(也称为内部结点) (如上图B、C、D、E、H结点)。
【树的度】所有结点度中的最大值(如上图树的度为3)。
【树的深度(或高度)】指所有结点中最大的层数(如上图树的深度为4)。
二、二叉树
2.1 二叉树的相关概念
【定义】二叉树(Binary Tree)是n(n≥0)个结点所构成的集合,它或为空树(n = 0);或为非空树,对于非空树T:
①有且仅有一个称之为根的结点;
②除根结点以外的其余结点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身又都是二叉树。
【特点】①结点的度小于等于2;
②有序树(子树有序,不能颠倒)。
【二叉树的形态】
【满二叉树】一棵深度为k 且有2k -1个结点的二叉树。(特点:每层都“充满”了结点),即叶子一个也不少的树。满二叉树是完全二叉树的一个特例。
【完全二叉树】完全二叉树:深度为k 的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k 的满二叉树中编号从1至n的结点一一对应。简而言之,完全二叉树虽然前n-1层是满的,但最底层却允许在右边缺少连续若干个结点。
2.2 二叉树的性质
【性质一】二叉树的第i层至多有2i-1个结点(i>=1)。
采用归纳法证明:
①当i=1时,只有一个根结点,2i-1=20 =1,命题成立;
②现假定第i-1层上至多有2i-2个结点,由于二叉树每个结点的度最大为2,故在第i层上最大结点数为第i-1层上最大结点数的二倍,即2×2i-2=2i-1。
【性质二】深度为k的二叉树至多有2k-1个结点(k>=1)。
证明:深度为k的二叉树的最大的结点数为二叉树中每层上的最大结点数之和,由性质1得到每层上的最大结点数:2i-1 ,∑(第i层上的最大结点数)= ∑ 2i-1=2k–1。
【性质三】对任何一棵二叉树,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
证明:设二叉树中度为1的结点数为n1,二叉树中总结点数为n,则有:结点总数 n=n0+n1+n2,分支数为n-1= n1 + 2n2,两式联立,即得n0=n2+1。
【性质四】性质4: 具有n个结点的完全二叉树的深度必为 log2n+1。
【性质五】如果对一棵有n个结点的完全二叉树的结点按层序编号(从第1层到第log2n +1层,每层从左到右),则对任一结点i(1≤i≤n),则有:
①如果i=1,则结点i无双亲,是二叉树的根;如果i>1,则其双亲是结点 i/2。
②如果2*i>n,则结点i为叶子结点;若2*i==n,其左孩子是结点2*i(i为最后一个非叶子结点)。
③如果2*i+1>n,则结点i无右孩子;若2*i+1==n,其右孩子是结点2*i+1(i为最后一个非叶子结点)。
(注意:②、③是判断i是否为叶子结点的临界点判断,如果结点总数(n=2*i)为偶数,说明第i个结点为这棵树唯一度为1的结点编号;若n(n=2*i+1)为奇数,则i是这棵完全二叉树的最后一个度为2的结点,没有度为1的结点。)
2.3 二叉树的存储结构
2.3.1 顺序存储结构
【思路】①对于完全二叉树:用一组地址连续的存储单元依次自上至下、自左至右存储完全二叉树上的结点元素。
②对于一般的二叉树:通过“补虚结点”,将一般的二叉树变成完全二叉树。
【特点】结点间逻辑关系蕴含在其存储位置中,若为单支树浪费空间,适于存满二叉树和完全二叉树
2.3.2 链式存储
由二叉树的定义得知,二叉树的结点由一个数据元素和分别指向其左、右子树的两个分支构成,则表示二叉树的链表中的结点至少包含3个域:数据域和左、右指针域,如图所示。利用这结点结构所得二叉树的存储结构称之为二叉链表。
有时,为了便于找到结点的双亲,还可在结点结构中增加一个指向其双亲结点的指针域,如图所示。利用这种结点结构所得二叉树的存储结构称之三叉链表。
链表的头指针指向二叉树的根结点。容易证得,在含有 n个结点的二叉链中有n+1个空链域。
2.4 二叉树的遍历
二叉树的遍历是常用的二叉树最基础的操作,遍历是指按某条搜索路线遍访每个结点且不重复。而遍历用途就是树结构插入、删除、修改、查找和排序运算的前提,是二叉树一切运算的基础和核心。
然而,不管使用那种遍历方法,都是自左向右开始遍历。
2.4.1 二叉树的先序遍历
【遍历方式】
若二叉树为空,则空操作;否则访问根结点(D),先序遍历左子树 (L),先序遍历右子树 (R)。
【遍历结果】A B D E C
【递归先序遍历算法】
Status PreOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
cout<<T->data; //访问根结点
PreOrderTraverse(T->lchild); //递归遍历左子树
PreOrderTraverse(T->rchild); //递归遍历右子树
}
}
【算法分析】
因每个结点只访问一次,因此,时间复杂度均为O(n);因栈所占用了最大辅助空间,因此空间复杂度也是O(n)。
2.4.2 二叉树的中序遍历
【遍历方式】
若二叉树为空,则空操作;否则:中序遍历左子树 (L),访问根结点 (D),中序遍历右子树 (R)。
【遍历结果】D B E A C
【递归中序遍历算法】
Status InOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
InOrderTraverse(T->lchild); //递归遍历左子树
cout<<T->data; //访问根结点
InOrderTraverse(T->rchild); //递归遍历右子树
}
}
【算法分析】
因每个结点只访问一次,因此,时间复杂度均为O(n);因栈所占用了最大辅助空间,因此空间复杂度也是O(n)。
2.4.3 二叉树的后序遍历
【遍历方式】
若二叉树为空,则空操作;否则后序遍历左子树 (L),后序遍历右子树 (R)访问根结点 (D)。
【遍历结果】D E B C A
【递归后序遍历算法】
Status PostOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
PostOrderTraverse(T->lchild); //递归遍历左子树
PostOrderTraverse(T->rchild); //递归遍历右子树
cout<<T->data; //访问根结点
}
}
【算法分析】
因每个结点只访问一次,因此,时间复杂度均为O(n);因栈所占用了最大辅助空间,因此空间复杂度也是O(n)。
2.4.4 二叉树的层序遍历
【遍历方式】按照从上到下,从左向右的顺序进行。
【遍历结果】A B C D E
【递归后序遍历算法】层序遍历方法与其他遍历方式不同,主要使用辅助队列进行遍历。主要思想为:首先根结点指针入队
①出队一个元素,访问该元素所指结点;
②若该元素所指结点左、右孩子非空,则分别入队;
③重复执行①②,直到队列为空。
Status PostOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
PostOrderTraverse(T->lchild); //递归遍历左子树
PostOrderTraverse(T->rchild); //递归遍历右子树
cout<<T->data; //访问根结点
}
}
2.5 线索二叉树
普通二叉树只能找到结点的左右孩子信息,而该结点的直接前驱和直接后继只能在遍历过程中获得,若将某种遍历序列某个结点的前驱和后继预存起来,则从第一个结点开始就能很快“顺藤摸瓜”而遍历整个树,由此产生了线索二叉树。
2.5.1 相关概念
【线索】指向结点前驱和后继的指针(非孩子指针)
【线索链表】加上线索的二叉链表
【线索二叉树】加上线索的二叉树(图形式样)
【线索化】对二叉树以某种次序遍历使其变为线索二叉树的过程
2.5.2 线索化的主要思想
①若结点有左子树,则lchild指向其左孩子;否则,lchild指向其直接前驱(即线索);
②若结点有右子树,则rchild指向其右孩子;否则, rchild指向其直接后继(即线索) 。
然而,为了避免混淆,增加两个标志域LTag 与RTag 。
利用这种思想,上图中树的先序线索二叉树结果如图所示。
上图中树的中序线索二叉树结果如图所示。
上图中树的后序线索二叉树结果如图所示。
【例题】画出与下列二叉树对应的中序线索二叉树
解: ①该二叉树的中序遍历结果为:H D B E A F C G
②对应线索树应当按此规律连线,即在原二叉树中添加虚线。
③中序遍历第一个元素的前驱与最后一个元素的后驱指向NULL。
三、树与森林
3.1 树的存储结构
关于树的存储结构有很多,下面一一进行介绍。
3.1.1 双亲表示法
【主要思想】以一组连续空间存储树的结点,同时在结点中附设一个指针,存放双亲结点在链表中的位置。
3.1.2 孩子表示法
①多重链表法
法一:在同构情况下,如图所示。
法二:在不同构情况下,如图所示。
②孩子链表法
将每个结点的孩子结点排列起来,看成一个线性表,且以单链表作存储结构,n个结点有n个孩子链表,n个头指针组成线性表。链表后的指针指向该结点的孩子结点所在位置。
当采用带双亲的孩子链表法表示时,则如图所示,在线性表中增加了存储双亲结点所在位置的存储空间。
3.1.3 孩子兄弟表示法
在孩子兄弟表示法中,除信息域data外,还包括两个指针,分别指向该结点的第一个孩子firstChild和下一个兄弟nextSibling。
以图中B结点为例,其左指针指向其二叉树中所对应的第一个孩子,即为E结点;右指针指向它的下一个兄弟(左边开始,最近的兄弟)。而在c结点中,因为它为叶子结点无孩子,所以左指针置空。
3.2 树、森林与二叉树的转换
3.2.1 树至二叉树的转换
【转换方法】该转换,主要利用了树的存储结构中的孩子兄弟表示法,步骤为:
①树中所有相邻兄弟间加一连线;
②对树中的每个结点,只保留其与第一个孩子间的连线,删去它与其他孩子间连线;
③以树根为轴心,将整棵树顺时针旋转45度,使之结构层次分明。
【特点】转换后的二叉树的任意一结点的左孩子与该结点在原树中关系为父子关系,而其右孩子与该结点在原树中为兄弟关系。同时,根没有兄弟,所以一棵树转换后的二叉树一定只有左子树。
3.2.2 森林至二叉树的转换
【转换方法】需要将森林中每棵树转换成相应的二叉树;同时,第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树根结点的右孩子,当所有二叉树连起来后,此时,所得二叉树即是森林转换得到的。
3.2.3 二叉树至森林/树的转换
【转换方法】①若某结点是其双亲的左孩子则把该结点的右孩子,右孩子的右孩子...都与该结点的双亲结点用线连起来;(蓝线+黑线)
②删除原二叉树中所有的双亲结点与右孩子结点间的连线;(白线)
③整理上两步所得的树/森林,使之结构层次分明。
【特点】根没有右孩子,则转换成的是树,否则转换成的是森林。
3.3 树与森林的遍历
3.3.1 树的遍历
【树的先根遍历】若树不空,则先访问根结点,然后依次从左到右先根遍历根的各棵子树。
【树的后根遍历】若树不空,则先依次从左到右后根遍历根的各棵子树,然后访问根结点;
3.3.2 森林的遍历
【先序遍历森林】 若森林不空,则可依下列次序进行遍历:
① 访问森林中第一棵树的根结点;
②先序遍历第一棵树中的子树森林;
③先序遍历除去第一棵树之后剩余的树构成的森林。
【中序遍历森林】若森林不空,则可依下列次序进行遍历:
①中序遍历第一棵树中的子树森林;
② 访问森林中第一棵树的根结点;
③ 中序遍历除去第一棵树之后剩余的树构成的森林。
四、树与二叉树
4.1 二叉排序树(BST)
4.1.1 二叉排序树的定义
【定义】二叉排序树或者是一棵空树;或者是具有如下特性的二叉树:
①若它的左子树不空,则左子树上所有结点的值均小于根结点的值;
②若它的右子树不空,则右子树上所有结点的值均大于根结点的值;
③它的左、右子树也都分别是二叉排序树
【特点】根据二叉排序树的定义,左子树结点值<根结点值<右子树结点值,所以对二叉排序树进行中序遍历,可以得到一个递增的有序序列。如图中二叉排序树的中序遍历序列为1 2345678。
4.1.2 二叉排序树的查找
【算法思想】对于二叉排序树的查找是从根结点开始,沿某个分支逐层向下比较的过程。若二叉排序树非空,先将给定值与根结点的关键字比较:
①若相等,则查找成功;
②若不等,如果小于根结点的关键字,则在根结点的左子树上查找;
③否则在根结点的右子树上查找。
【算法描述】
BSTNode *BST_ Search (BiTree T, ElemType key) {
while (T!=NULL&&key!=T->data) ( //若树空或等于根结点值,则结束循环
if (key<T->data) T=T-> lchild; //小于,则在左子树上查找
else T=T->rchild; //大于,则在右子树上查找
}
return T;
}
4.1.3 二叉排序树的插入
【特点】二叉排序树作为一种动态树表,其特点是树的结构通常不是一次生成的, 而是在查找过程中,当树中不存在关键字值等于给定值的结点时再进行插入的。
【算法思想】插入结点的过程如下:
①若原二叉排序树为空,则直接插入结点;
②否则,若关键字k小于根结点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树。
插入的结点一定是一个新添加的叶结点,且是查找失败时的查找路径上访问的最后一个结点的左孩子或右孩子。
【算法描述】
int BST_ Insert.(BiTree &T,KeyType k){
if (T==NULL){ //原树为空,新插入的记录为根结点
T= (BiTree) malloc (size0f (BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1; //返回1,插入成功
else if(k==T->key) //树中存在相同关键字的结点,插入失败
return 0;
else if(k<T->key) //插入到T的左子树
return BST_Insert(T->1child,k);
else //插入到T的右子树
return BST_Insert (T->rchild, k);
}
4.1.4 二叉排序树的构造
从一棵空树出发, 依次输入元素,将它们插入二叉排序树中的合适位置。设查找的关键字序列为{45, 24, 53, 45, 12, 24},则生成的二叉排序树如图所示。
4.1.5 二叉排序树的删除
【特点】在二叉排序树中删除一个结点时, 不能把以该结点为根的子树上的结点都删除,必须先把被删除结点从存储二叉排序树的链表上摘下,将因删除结点而断开的二叉链表重新链接起来,同时确保二叉排序树的性质不会丢失。
【算法思想】删除操作的实现过程按3种情况来处理:
①若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质。
②若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。
③若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
【例题】①删除结点45(情况②)
②删除结点78(情况②)
③删除结点78(情况③)
4.2 平衡二叉树(AVL)
4.2.1 平衡二叉树的相关概念
【定义】在插入和删除二叉树结点时,要保证任意结点的左、右子树高度差的绝对值不超过1,将这样的二叉树称为平衡二叉树(BalancedBinary Tree),简称平衡树。
【平衡因子】定义结点左子树与右子树的高度差为该结点的平衡因子,则平衡二叉树结点的平衡因子的值只可能是-1、0或1。
【目的】为避免树的高度增长过快,降低二叉排序树的性能。
4.2.2 平衡二叉树的插入
【算法思想】
每当在二叉排序树中插入(或删除)一个结点时,首先检查其插入路径上的结点是否因为此次操作而导致了不平衡。
若导致了不平衡,则先找到插入路径上离插入结点最近的平衡因子的绝对值大于1的结A,再对以A为根的子树,在保持二叉排序树特性的前提下,调整各结点的位置关系,使之重新达到平衡。
【调整规律】
平衡二叉树的插入过程的前半部分与二叉排序树相同,但在新结点插入后,若造成查找路径上的某个结点不再平衡,则需要做出相应的调整。可将调整的规律归纳为下列4种情况:
① LL平衡旋转(右单旋转)。
由于在结点A的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。
将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树。
②RR平衡旋转(左单旋转)。
由于在结点A的右孩子(R)的右子树(R)上插入了新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。
将A的右孩子B向左上旋转代替A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树,如图所示。
③LR平衡旋转(先左后右双旋转)。
由于在A的左孩子(L)的右子树(R)上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。
先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后把该C结点向右上旋转提升到A结点的位置,如图所示。
④RL平衡旋转(先右后左双旋转)。
由于在A的右孩子(R)的左子树(L)上插入新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。
先将A结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置,然后把该C结点向左上旋转提升到A结点的位置,如图所示。
【例题】依次插入的关键字为 5, 4, 2, 8, 6, 9
解:具体过程如下:
4.3 哈夫曼树(Huffman)与哈夫曼编码
4.3.1 哈夫曼树的基本概念
【结点路径长度】两结点的路径上的分支数即为结点的路径长度。
【树的路径长度】树根到每一结点的路径长度之和。
【结点的带权路径长度(Weight Path Length)】该结点到树根之间的路径长度与结点上权的乘积。
【树的带权路径长度(WPL)】树中所有叶子结点的带权路径长度之和(WPL最小的二叉树就是哈夫曼树,或最优二叉树)。
【前缀编码】任一字符的编码都不是另一个字符编码的前缀。
4.3.2 哈夫曼树的构造过程
【基本思想】使权大的结点靠近根。
【操作要点】对权值结点的合并、删除与替换过程中,总是合并当前根结点权值最小的两棵子树或结点。
【构造过程】
①在森林中选取两棵根结点权值最小的树作左右子树,构造一棵新的二叉树,置新二叉树根结点权值为其左右子树根结点权值之和;
②在森林中删除这两棵树,同时将新得到的二叉树加入森林中;
③重复上述两步,直到只含一棵树为止,这棵树即哈夫曼树。
【特点】根据构造过程可知,每当进行左右子树合并时就会产生一个新的结点,因此一棵有n个叶子结点的Huffman树有2n-1个结点
【哈夫曼编码基本思想】概率大的字符用短码,概率小的用长码,构造哈夫曼树。
【译码过程】遇“0”向左,遇“1”向右;一旦到达叶子结点,则译出一个字符,反复由根出发,直到译码完成。(注意:译码过程较自由,可选择左0右1、左1右或其他方式,但规则要提前说明。)
【例题】某系统在通讯时,只出现C,A,S,T,B五种字符,其出现频率依次为2,4,2,3,3,试设计Huffman编码。