1.18k likes | 1.4k Views
第五章树和二叉树 第一节 树的概念与表示 第二节 树的基本操作与存储 第三节 二叉树 第四节 二叉树的遍历 第五节 线索二叉树 第六节 二叉树的应用 第七节 树、森林与二叉树的转换 本 章 小 结 实训一 实训二 思 考 与 习 题. 第五章 树和二叉树. 学习要求: 要求掌握树和二叉树的概念,二叉树的性质,存储结构以及基本运算,并用二叉树解决一些综合应用问题。 主要内容:
E N D
第五章树和二叉树 第一节 树的概念与表示 第二节 树的基本操作与存储 第三节 二叉树 第四节 二叉树的遍历 第五节 线索二叉树 第六节 二叉树的应用 第七节 树、森林与二叉树的转换 本 章 小 结 实训一 实训二 思 考 与 习 题
第五章 树和二叉树 学习要求: 要求掌握树和二叉树的概念,二叉树的性质,存储结构以及基本运算,并用二叉树解决一些综合应用问题。 主要内容: 树型结构是一类非常重要的非线性结构,其中以树和二叉树最为常用。本章介绍了树和二叉树的概念、二叉树的性质、存储结构、基本运算以及哈夫曼树的定义和构造过程。
在前面几章里讨论的数据结构主要是线性结构,线性结构中数据元素的关系是一一对应的,其特点是逻辑结构简单,易于实现基本操作。然而,线性结构在许多实际应用中不能明确、方便地表示数据元素之间的复杂关系。要描述这些元素之间的复杂关系应采用非线性结构。所谓非线性结构是指,在该结构中至少存在一个数据元素有两个或两个以上的直接前驱(或直接后继)元素。在前面几章里讨论的数据结构主要是线性结构,线性结构中数据元素的关系是一一对应的,其特点是逻辑结构简单,易于实现基本操作。然而,线性结构在许多实际应用中不能明确、方便地表示数据元素之间的复杂关系。要描述这些元素之间的复杂关系应采用非线性结构。所谓非线性结构是指,在该结构中至少存在一个数据元素有两个或两个以上的直接前驱(或直接后继)元素。 本章和下一章将分别介绍两种重要的非线性结构:树和图。树型结构中结点之间有分支关系,又具有层次关系,它非常类似于自然界中的树。树型结构在现实世界中广泛存在,如家族的家谱、各单位的行政组织机构等都可用树来形象地表示。在计算机领域中,操作系统中对磁盘文件的管理就采用了树形目录结构;在数据库中,树型结构也是数据的重要组织形式之一。本章将重点讨论二叉树的存储结构及各种操作,并研究树、森林与二叉树的转换及应用实例。
第一节 树的概念与表示 一、树的定义及相关术语 (一)树的定义 树(tree)是n(n≥0)个结点的有限集T。当n=0即T为空时,称为空树;当n>0时,T满足如下两个条件: 1.有且仅有一个特定的称为根(root)的结点,它没有直接前驱,但有零个或多个直接后继。 2.其余的n-1个结点可分为m(m≥0)个互不相交的子集T1,T2,…,Tm,其中每个子集本身又是一棵树,并称其为根的子树(subtree)。
树的定义是递归的,因为在树的定义中又用到树的定义。图5.1给出了一般的树形示例,它有13个结点,其中A是根结点,其余结点分为三个互不相交的子集:T1={B,E},T2={C,F,G,J,K,L},T3={D,H,I,M};T1,T2,T3都是根A的子树,且各自本身也是一棵树。树的定义是递归的,因为在树的定义中又用到树的定义。图5.1给出了一般的树形示例,它有13个结点,其中A是根结点,其余结点分为三个互不相交的子集:T1={B,E},T2={C,F,G,J,K,L},T3={D,H,I,M};T1,T2,T3都是根A的子树,且各自本身也是一棵树。 在一棵树中,一个结点被定义为其子树根结点的直接前驱结点,而其子树的根结点则是它的直接后继结点。从逻辑上看,树型结构具有以下特点: 1.树的根结点没有前驱结点,除根结点之外的所有结点有且只有一个直接前驱结点。 2.树中的每个结点都可以有零个或多个后继结点。 3.树型结构是一种具有递归特征的数据结构。 (二)关于树型结构的基本术语 下面以图5.1所示的树为例,介绍树型结构的基本术语。 结点:包含一个数据元素及若干指向其子树的分支信息。图5.1中,结点有:A,B,C,D,E,F,G,H,I,J,K,L,M。
结点的度:一个结点所拥有的子树个数称为该结点的度。图5.1中,结点A的度为3,结点B的度为1,结点G的度为3,结点E的度为0。结点的度:一个结点所拥有的子树个数称为该结点的度。图5.1中,结点A的度为3,结点B的度为1,结点G的度为3,结点E的度为0。 树的度:树的度是指该树中所有结点的度的最大值。图5.1表示的树的度为3。叶子结点(终端结点):度为零的结点,即没有后继的结点称为叶子结点,也称为终端结点。图5.1中,叶子结点有:E,F,H,J,K,L,M。 分支结点(非终端结点):度不为零的结点称为分支结点,也称为非终端结点。除根结点以外的分支结点又称为内部结点。图5.1中,结点A,B,C,D,G,I都是分支结点,其中结点B,C,D,G,I又称为内部结点。 孩子结点与双亲结点:树中某个结点的子树之根称为该结点的孩子(child),相应地,该结点称为孩子的双亲(parent)。图5.1中,结点B是结点A的孩子结点,结点A是结点B的双亲结点。 兄弟结点:同一个双亲的结点之间互为兄弟结点。图5.1中,F和G互为兄弟,J、K和L互为兄弟。
路径与路径长度:对于任意两个结点ki和kj,若树中存在一个结点序列ki,ki1,ki2,…,kin,kj,使得序列中除ki外的任一结点都是其在序列中的前一个结点的后继,则称该结点序列为从ki到kj的一条路径,用路径所经过的结点序列(ki,ki1,ki2,…,kin,kj)表示这条路径。路径的长度等于路径所经过的结点数减1(即路径上分支数目)。可见,路径就是从ki出发“自上而下”到达kj所经过的树中结点序列。图5.1中,结点A到结点K有一条路径ACGK,它的长度为3。显然,从树的根结点到树中其余结点均存在一条唯一的路径。注意:结点K和结点L之间不存在路径。路径与路径长度:对于任意两个结点ki和kj,若树中存在一个结点序列ki,ki1,ki2,…,kin,kj,使得序列中除ki外的任一结点都是其在序列中的前一个结点的后继,则称该结点序列为从ki到kj的一条路径,用路径所经过的结点序列(ki,ki1,ki2,…,kin,kj)表示这条路径。路径的长度等于路径所经过的结点数减1(即路径上分支数目)。可见,路径就是从ki出发“自上而下”到达kj所经过的树中结点序列。图5.1中,结点A到结点K有一条路径ACGK,它的长度为3。显然,从树的根结点到树中其余结点均存在一条唯一的路径。注意:结点K和结点L之间不存在路径。 祖先结点与子孙结点:一个结点的祖先是从根结点到该结点路径上所经过的所有结点,而一个结点的子孙则是以该结点为根的子树中所有的结点。我们约定:一个结点的祖先和子孙不包含该结点本身。图5.1中,G的祖先是A和C,G的子孙是J、K和L。 结点的层次:树具有一种层次结构,从根结点开始定义,根结点为第一层,其余结点的层数等于其双亲结点的层数加1。双亲在同一层的结点互为堂兄弟。图5.1中,结点A的层数为1,结点B、C和D的层数为2,结点E、F、G、H和I的层数为3,结点J、K、L和M的层数为4,结点E、F和H互为堂兄弟。注意:有些书中将根结点的层数定义为0。
树的高度(或深度):树中结点的最大层数称为树的高度或深度。图5.1中,树的高度为4。树的高度(或深度):树中结点的最大层数称为树的高度或深度。图5.1中,树的高度为4。 有序树和无序树:将树中每个结点的各子树看成是从左到右有次序的(即不能随意变换),则称该树为有序树;否则,该树为无序树。图5.2中的两棵树,作为有序树,它们是不同的,因为结点A的两个孩子在两棵树中的左右次序不同;但作为无序树,它们是相同的。注意:如果不特别指明,我们这一章讨论的树都是有序树。 图5.2两棵不同的有序树 森林:m(m≥0)棵互不相交的树的集合。树和森林的概念很相近。将一棵非空树的根结点删去,树就生成了森林;反之,给森林增加一个统一的根结点,森林就变成一棵树。
二、树的表示 在不同的场合,树的表示方法也不尽相同,通常树的表示法有四种。 (一)树形表示法 用一个圆圈表示一个结点,圆圈内的符号代表该结点的数据信息,结点之间的关系通过连线表示。虽然每条连线上都不带有箭头,但它仍然是有向的,其方向隐含着从上向下,即连线的上方结点是下方结点的前驱,下方结点是上方结点的后继。它的直观形象是一棵倒置的树(树根在上,树叶在下),如图5.1所示。本书主要采用树形表示法来表示树。 (二)嵌套表示法 每棵树对应一个圆圈,圆圈内包含根结点和子树的圆圈,同一个根结点下的各子树对应的圆圈是不能相交的。用这种方法表示的树中,结点之间的关系是通过圆圈的包含来表示的。如图5.3(a)所示。
(a)嵌套表示法 (b)凹入表示法 图5.3 树的表示
(三)凹入表示法 每棵树的根对应着一个线条,子树的根对应着一个较短的线条,且树的根在上,子树的根在下,同一个根下的各子树的根对应的线条长度是一样的。如图5.3(b)所示。 (四)广义表表示法 广义表表示法也称为嵌套括号表示法。每棵树对应一个由根作为名字的表,表名放在表的左边,表是由在一个圆括号里的各子树对应的表组成的,之间用逗号分开。用这种方法表示的树中,结点之间的关系是通过圆括号的嵌套表示的。如图5.3(c)所示。
第二节 树的基本操作与存储 一、树的基本操作 树的基本操作通常有以下几种: 1.Initiate(t):初始化一棵空树t。 2.Root(x):求结点x所在树的根结点。 3.Parent(t,x):求树t中结点x的双亲结点。 4.Child(t,x,i):求树t中结点x的第i个孩子结点。 5.RightSibling(t,x):求树t中结点x右边的兄弟结点。 6.Insert(t,x,i,s):把以s为根结点的树插入树t中作为结点x的第i棵子树。 7.Delete(t,x,i):在树t中删除结点x的第i棵子树。 8.Traverse(t):树的遍历操作。即按某种方式访问树t中的每个结点,且使每个结点只被访问一次,得到一个由所有结点组成的序列。遍历操作是非线性结构中经常用到的基本操作,许多对树的操作都是借助于该操作实现的。
二、树的存储结构 树的存储要求既要存储结点的数据元素本身,又要存储结点之间的逻辑关系。树的存储结构很多,下面介绍四种常用的存储结构,即双亲表示法、孩子链表表示法、双亲链表孩子表示法和孩子兄弟链表表示法。 (一)双亲表示法 由树的定义可知,在树中每个结点的双亲是唯一的。利用这一性质,可在存储结点信息的同时,为每个结点附设一个指向其双亲的指针parent,就可唯一地表示任何一棵树。尽管可用动态链表来实现这种表示,然而用一维数组来表示更为方便。 类型说明如下: #define MAXTREESIZE 100 /*向量空间的大小,由用户自己定义*/ typedef char DataType; /*用户自己定义数据类型*/ typedef struct{ DataType data; /*结点数据*/ int parent; /*双亲指针,指示结点的双亲在向量中的位置*/
} PTreeNode; typedef struct { PTreeNode nodes[MAXTREESIZE]; int n; /*结点总数*/ } PTree; PTree T; /*T是双亲链表*/ (a) 树 (b) 双亲表示法 图5.4 树及其双亲表示法
图5.4(a)中的树,其双亲表示法如图5.4(b)所示。结点B、C和D的双亲域是0,表示它们的双亲在一维数组中的位置为0,即结点A是它们的双亲。注意:parent域的值为-1表示该结点无双亲,即该结点是一个根结点。图5.4(a)中的树,其双亲表示法如图5.4(b)所示。结点B、C和D的双亲域是0,表示它们的双亲在一维数组中的位置为0,即结点A是它们的双亲。注意:parent域的值为-1表示该结点无双亲,即该结点是一个根结点。 树的双亲表示法对于实现parent(t,x)操作和root(x)操作很方便,但若求某结点的孩子结点,即实现child(t,x,i)操作时,则需要遍历整个数组。此外,这种存储方式不能反映各兄弟结点之间的关系,所以实现Right Sibling(t,x)操作也比较困难。在实际中,如果需要实现这些操作,可在结点结构中增设存放第一个孩子的域和存放第一个右兄弟的域,这样就能较方便地实现上述操作了。 (二)孩子链表表示法 孩子链表表示法中,主体是一个与结点个数一样大小的一维数组,数组的每一个元素由两个域组成,一个域用来存放结点信息,另一个域用来存放指针,该指针指向由该结点孩子组成的单链表的首位置。单链表中的结点也由两个域组成,一个存放孩子结点在一维数组中的序号,另一个是指针域,指向下一个孩子结点。
类型说明如下: typedef struct cnode{ /*孩子链表结点*/ int child; /*孩子结点在一维数组中对应的序号*/ struct cnode *next; }CNode; typedef struct{ DataType data; /*树中结点数据*/ CNode *firstchild; /*孩子链表的头指针*/ }PTNode; typedef struct{ PTNode nodes[MAXTREESIZE]; int n,root; /*n为结点总数,root指出根在数组中的位置*/ }CTree; CTree T; /*T为孩子链表表示法*/
图5.4(a)中的树,其孩子链表表示如图5.5所示。图5.4(a)中的树,其孩子链表表示如图5.5所示。 与双亲表示法相反,孩子链表表示法便于实现涉及孩子及其子孙的运算,但不便于实现与双亲有关的运算。因此可将这两种表示法结合起来,形成双亲孩子链表表示法。 图5.5 孩子链表表示
(三)双亲孩子链表表示法 双亲孩子链表表示法是将双亲链表表示法和孩子链表表示法相结合的存储结构,其仍将各结点的孩子结点分别组成单链表,同时用一维数组顺序存储树中的各结点,数组元素除了包括结点本身的数据data和该结点的孩子结点链表的头指针firstchild之外,还增设一个域parent,存储其双亲结点在数组中的序号。图5.4(a)中的树,其双亲孩子链表表示如图5.6所示。 图5.6 双亲孩子链表表示
(四)孩子兄弟链表表示法 孩子兄弟链表表示法是一种常用的存储结构。其方法是:每个结点除其信息域外,再增加两个指针域,分别指向该结点的第一个孩子结点和下一个兄弟结点。 图5.4(a)中树的孩子兄弟链表表示如图5.7所示。 图5.7 孩子兄弟链表表示 这种存储结构的最大优点是:它和二叉树的二叉链表表示法完全一样,因此可利用二叉树的算法来实现对树的操作。
第三节 二叉树 二叉树是树型结构的一个重要类型,关于二叉树的存储结构及算法都较为简单,因此,二叉树在数据结构的研究领域中具有很重要的地位。 一、二叉树的基本概念 二叉树(binary tree)是n(n≥0)个结点的有限集T,它或者是空集(n=0),或者同时满足下述两个条件: 1.有且仅有一个称为根的结点。 2.其余结点分为两个互不相交的集合T1和T2,T1和T2称为根的左子树和右子树。 二叉树的定义也是一个递归定义,表明二叉树或为空,或是由一个根结点加上两棵分别称为左子树和右子树的二叉树组成。 二叉树的特点是每个结点至多只有二棵子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能任意颠倒。因此即使一个结点只有一棵非空子树,仍须区别它是该结点的左子树还是右子树,这与树是不同。
二叉树可以有五种基本形态,如图5.8所示。 图5.8 二叉树的五种基本形态 在一棵二叉树中,如果所有分支结点都有左孩子结点和右孩子结点,并且叶子结点都集中在二叉树的最下面一层,这样的二叉树称为满二叉树。图5.9(a)所示就是一棵满二叉树。我们可以对满二叉树的结点进行连续编号,约定编号从树根为1开始,按照层数从小到大、同一层从左到右的次序进行。注意:图5.9中每个结点旁的数字为对应该结点的编号。
若二叉树中最多只有最下面两层的结点度数可以小于2,并且最下面一层的叶子结点都依次排列在该层最左边的位置上,则这样的二叉树称为完全二叉树。图5.9(b)所示为一棵完全二叉树。同样可以对完全二叉树中每个结点进行连续编号,编号的方法与满二叉树相同的。若二叉树中最多只有最下面两层的结点度数可以小于2,并且最下面一层的叶子结点都依次排列在该层最左边的位置上,则这样的二叉树称为完全二叉树。图5.9(b)所示为一棵完全二叉树。同样可以对完全二叉树中每个结点进行连续编号,编号的方法与满二叉树相同的。 (a) 满二叉树
(b) 完全二叉树 图5.9 满二叉树和完全二叉树 显然满二叉树是完全二叉树,但完全二叉树不一定是满二叉树。在满二叉树的最下一层上,从最右边开始删去若干结点后得到的二叉树仍然是一棵完全二叉树。因此,在完全二叉树中,若某个结点没有左孩子,则它一定没有右孩子,即该结点必是叶子结点。
二、二叉树的主要性质 性质1:二叉树第i层上的结点数目最多为2i-1(i≥1)。 证明:利用归纳法容易证得此性质。 当i=1时,只有一个根结点。显然,2i-1=20=1是对的。 现在假定对所有的j(1≤j<i)命题成立,即第j层上至多有2j-1个结点。那么可以证明j=i时命题也成立。 由归纳假设:第i-1层上至多有2i-2个结点。由于二叉树的每个结点的度至多为2,故在第i层上的最大结点数为第i-1层上的最大结点数的2倍,即2×2i-2=2i-1,故命题正确。 性质2:深度为k的二叉树至多有2k-1个结点(k≥1)。 证明:在具有相同深度的二叉树中,仅当每一层都含有最大结点数时其树中结点数最多,因此利用性质1可得,深度为k的二叉树的结点数至多为: 20+21+…+2k-1=2k-1 故命题正确。
性质3:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。性质3:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。 证明:因为二叉树中所有结点的度数均不大于2,所以结点总数n应等于度为0的结点数n0,度为1的结点数n1和度为2的结点数n2之和: n=n0+n1+n2 ① 另一方面,度为1的结点有一个孩子,度为2的结点有两个孩子,故二叉树中孩子结点的总数是n1+ 2n2,但树中只有根结点,它不是任何结点的孩子,故二叉树中的结点总数又可表示为: n=n1+2n2+1 ② 由①②得到n0=n2+1。 性质4:具有n个(n>0)结点的完全二叉树的深度为log2n+1。 证明:根据完全二叉树的定义和性质2可知,当一棵完全二叉树的深度为k、结点个数为n时,有: 2k-1-1<n≤2k-1, 即2k-1≤n<2k 对不等式取对数,有: k-1≤log2n<k 由于k是整数,所以有k=log2n+1。 注:x表示取不大于x的最大整数,如7.5=7;x表示取不小于x的最小整数,如7.5=8。
性质5:对于具有n个结点的完全二叉树,如果按照从上至下和从左到右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意编号为i的结点,有性质5:对于具有n个结点的完全二叉树,如果按照从上至下和从左到右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意编号为i的结点,有 (1)如果i>1,则该结点i的双亲结点的编号为i/2(注:“/”表示整除求商);如果i=1,则该结点i是根结点,无双亲结点。 (2)如果2i≤n,则该结点i的左孩子结点的编号为2i;如果2i>n,则该结点i无左孩子,即该结点必定是叶子结点。因此完全二叉树中编号i>n/2的结点必定是叶子结点。 (3)如果2i+1≤n,则该结点i的右孩子结点的编号为2i+1;否则该结点i无右孩子。 (4)若i为奇数且不为1,则该结点i的左兄弟的编号是i-1;否则,该结点i无左兄弟。 (5)若i为偶数且小于n,则该结点i的右兄弟的编号是i+1;否则,该结点i无右兄弟。 此性质可采用数学归纳法证明。证明略。
三、二叉树的存储 二叉树的存储结构有两种:顺序存储结构和链式存储结构。 (一)顺序存储结构 二叉树的顺序存储结构是指用一组连续的存储单元来存放二叉树的数据元素。首先采用一维数组作存储结构,然后将二叉树中的各结点进行编号,要求该编号与等高的完全二叉树中对应位置上的结点编号相同,并以结点编号作为下标,把各结点的值对应存放到一维数组中去。 显然这种存储方法对于完全二叉树和满二叉树来说是非常方便的,因为树中结点的序号可以唯一反映出结点之间的逻辑关系,这样既能最大限度地节省存储空间,又可利用数据元素的下标值确定结点在二叉树中的位置以及结点之间的关系,如图5.10所示是一棵完全二叉树及其顺序存储结构,bt[0]用来存放结点的个数。
(a) 完全二叉树 (b)顺序存储结构 图5.10 完全二叉树及其顺序存储结构
但是,对于一般的二叉树,如果按照完全二叉树的形式来存储,则会造成存储空间的浪费,因为增加了一些并不存在的结点。图5.11所示是一棵一般二叉树及其顺序存储结构,图(b)中以”¢”表示不存在的结点。但是,对于一般的二叉树,如果按照完全二叉树的形式来存储,则会造成存储空间的浪费,因为增加了一些并不存在的结点。图5.11所示是一棵一般二叉树及其顺序存储结构,图(b)中以”¢”表示不存在的结点。 最坏的情况是单支二叉树(即每个结点只有左孩子或右孩子)。如图5.12所示,一棵深度为3的右单支二叉树,该二叉树实际只有3个结点,如果按照完全二叉树的形式来存储,则空间浪费太大。这是这种存储结构的一大缺点。另外,由于顺序存储结构固有的一些缺陷,会使二叉树的插入、删除等操作不方便,且效率也比较低。因此,对于一般二叉树来说,更适合的存储方法是采用链式存储结构。
(a)一般二叉树 (b) 顺序存储结构 图5.11 一般二叉树及其顺序存储结构
(a) 单支二叉树 • 单支二叉树的顺序存储结构 • 图5.12 单支二叉树及其顺序存储结构
(二)链式存储结构 二叉树的链式存储结构是指采用链表形式来存储一棵二叉树,二叉树中的每一个结点用链表中的一个链结点来存储。二叉树的每个结点最多有两个孩子,用链式存储二叉树时,每个结点除了存储结点本身的数据外,还应设置两个指针域,分别指向该结点的左孩子和右孩子,结点的结构为: 其中,data域存放某结点的数据信息;lchild与rchild域分别存放指向左孩子和右孩子的指针,当左孩子或右孩子不存在时,相应指针域值为空(用符号∧或NULL表示)。 相应的类型说明为: typedef char DataType; typedef struct BiTNode{ DataType data; struct BiTNode *lchild,*rchild; /*左右孩子指针*/ }BiTNode, *BiTree;
在一棵二叉树中,所有类型为BiTNode的结点,再加上一个指向开始结点(根结点)的BiTree型头指针(即根指针)root,就构成了二叉树的链式存储结构,并将其称为二叉链表。图5.13(a)所示的二叉树的二叉链表如图5.13(b)所示。在一棵二叉树中,所有类型为BiTNode的结点,再加上一个指向开始结点(根结点)的BiTree型头指针(即根指针)root,就构成了二叉树的链式存储结构,并将其称为二叉链表。图5.13(a)所示的二叉树的二叉链表如图5.13(b)所示。 (a) 二叉树 (b) 二叉链表表示
显然,一个二叉链表由根指针root唯一确定。若二叉树为空,则root=NULL。若结点的某个孩子不存在,则相应的指针为空。在具有n个结点的二叉树的二叉链表表示中,一共有2n个指针域,其中只有n-1个用来指示结点的左、右孩子,其余的n+1个指针域为空。显然,一个二叉链表由根指针root唯一确定。若二叉树为空,则root=NULL。若结点的某个孩子不存在,则相应的指针为空。在具有n个结点的二叉树的二叉链表表示中,一共有2n个指针域,其中只有n-1个用来指示结点的左、右孩子,其余的n+1个指针域为空。 (c) 三叉链表表示 图5.13 二叉树及其链式存储结构
有时为了方便访问某结点的双亲,还可以给链表结点增加一个指向其双亲的指针parent,结点的结构为:有时为了方便访问某结点的双亲,还可以给链表结点增加一个指向其双亲的指针parent,结点的结构为: 利用这样的结点结构表示的二叉树的链式存储结构被称为三叉链表。图5.13(a)所示的二叉树的三叉链表如图5.13(c)所示。 与前面讨论过的线性链表一样,对于二叉树,无论采用哪种形式的链式存储结构,都要 给出根结点所在链结点的存储地址,否则,有关操作将无法进行。 二叉链表存储结构具有灵活、方便的特点,结点的最大数目只受系统最大可用存储空间的限制。对于一般的二叉树,二叉链表存储结构不仅比顺序存储结构节省空间(用于存储指针的空间开销只是二叉树中结点数的线性函数),而且对二叉树实施有关操作也很方便。因此,二叉链表的使用更加广泛。
四、二叉树的基本操作及实现 (一)二叉树的基本操作 归纳起来,二叉树通常有以下基本操作。 1.Initiate(bt)建立一棵空二叉树。 2.Create(x,lbt,rbt)生成一棵以x为根结点的数据域信息,以二叉树lbt和rbt为左子树和右子树的二叉树。 3.InsertL(bt,x,parent)将数据域信息为x的结点插入到二叉树bt中作为结点parent的左孩子结点。如果结点parent原来有左孩子结点,则将结点parent原来的左孩子结点作为结点x的左孩子结点。 4.InsertR(bt,x,parent)将数据域信息为x的结点插入到二叉树bt中作为结点parent的右孩子结点。如果结点parent原来有右孩子结点,则将结点parent原来的右孩子结点作为结点x的右孩子结点。 5.DeleteL(bt,parent)在二叉树bt中删除parent的左子树。 6.DeleteR(bt,parent)在二叉树bt中删除parent的右子树。
7.Search(bt,x)在二叉树bt中查找数据元素x。7.Search(bt,x)在二叉树bt中查找数据元素x。 8.Traverse(bt)按某种方式遍历二叉树bt的全部结点。 (二)算法实现 下面以二叉链表作为二叉树的存储结构来讨论上述操作的实现算法。 1.Initiate(bt): 初始建立一棵二叉树bt,并使bt指向头结点。在二叉树根结点前建立头结点,就如同在单链表前建立头结点一样,可以方便后面一些操作的实现。 【算法5.1】 int Initiate(BiTree *bt) /*初始化建立一棵带头结点的二叉树*/ { if ((*bt=(BiTNode *)malloc(sizeof(BiTNode)))==NULL) return 0; *bt→lchild=NULL; *bt→rchild=NULL; return 1; }
2.Create(x,lbt,rbt):建立一棵以x为根结点的数据域信息,以二叉树lbt和rbt为左子树和右子树的二叉树。建立成功时返回所建二叉树根结点的指针;建立失败时返回空指针。 【算法5.2】 BiTree Create(elemtype x,BiTree lbt,BiTree rbt) { /*建立一棵以x为根结点、lbt和rbt为左右子树的二叉树*/ BiTree p; if ((p=(BiTNode *)malloc(sizeof(BiTNode)))==NULL) return NULL; p→data=x; p→lchild=lbt; p→rchild=rbt; return p; } 3.InserL(bt,x,parent):将数据域信息为x的结点插入到二叉树bt中作为点parent的左孩子结点。如果结点parent原来有左孩子结点,则将结点parent原来的左孩子结点作为结点x的左孩子结点。
【算法5.3】 BiTree InsertL(BiTree bt,elemtype x, BiTree parent) { /*在二叉树bt中的parent所指结点和其左子树之间插入数据元素x的结点*/ BiTree p; if (parent==NULL) { printf("\n插入出错!") return NULL; } if ((p=(BiTNode *)malloc(sizeof(BiTNode)))==NULL) return NULL; p→data=x; p→lchild=NULL; p→rchild=NULL; if (parent→lchild==NULL) parent→lchild=p; else { p→lchild=parent→lchild; parent→lchild=p; } return bt; }
4.InsertR(bt,x,parent):将数据域信息为x的结点插入到二叉树bt中作为结点parent的右孩子结点。如果结点parent原来有右孩子结点,则将结点parent原来的右孩子结点作为结点x的右孩子结点。4.InsertR(bt,x,parent):将数据域信息为x的结点插入到二叉树bt中作为结点parent的右孩子结点。如果结点parent原来有右孩子结点,则将结点parent原来的右孩子结点作为结点x的右孩子结点。 【算法5.4】 BiTree InsertR(BiTree bt,elemtype x, BiTree parent) { /*在二叉树bt中的parent所指结点和其右子树之间插入数据元素x的结点*/ BiTree p; if (parent==NULL) { printf("\n插入出错!") return NULL; } if ((p=(BiTNode *)malloc(sizeof(BiTNode)))==NULL) return NULL; p→data=x; p→lchild=NULL; p→rchild=NULL; if (parent→rchild==NULL) parent→rchild=p; else
{ p→rchild=parent→rchild; parent→rchild=p; } return bt; } 5.DeleteL(bt,parent):在二叉树bt中删除parent的左子树。当parent或parent的左孩子结点为空时删除失败。删除成功时返回根结点指针;删除失败时返回空指针。 【算法5.5】 BiTree DeleteL(BiTree bt, BiTree parent) { /*在二叉树bt中删除结点parent的左子树*/ BiTree p; if (parent==NULL ‖ parent→lchild==NULL) { printf("\n删除出错!"); return NULL; } p=parent→lchild; parent→lchild=NULL; free(p); return bt;}
6.DeleteR(bt,parent):在二叉树bt中删除parent的右子树。当parent或parent的右孩子结点为空时删除失败。删除成功时返回根结点指针;删除失败时返回空指针。 【算法5.6】 BiTree DeleteL(BiTree bt, BiTree parent) { /*在二叉树bt中删除结点parent的右子树*/ BiTree p; if (parent==NULL ‖ parent→rchild==NULL) { printf("\n删除出错!"); return NULL; } p=parent→rchild; parent→rchild=NULL; free(p); return bt; } Search(bt,x)和Traverse(bt)是关于二叉树遍历操作的实现,将在下一节中重点介绍。
第四节 二叉树的遍历 一、二叉树的遍历方法及递归实现 (一)二叉树的遍历方法 二叉树的遍历是指按照一定次序访问树中所有结点,并且每个结点仅被访问一次的过程。 遍历是二叉树中经常要用到的一种操作。因为在实际应用问题中,常常需要按一定顺序对二叉树中的每个结点逐个进行访问,或查找具有某一特点的结点,然后对这些满足条件的结点进行处理。 由于在二叉树中,左子树和右子树是有严格区别的,因此在遍历一棵非空二叉树时,根据访问根结点(D)、遍历左子树(L)和遍历右子树(R)的先后关系可以组合成6种遍历方法: DLR,LDR,LRD,DRL,RDL,RLD。若规定先遍历左子树(L),后遍历右子树(R),再把访问根结点(D)穿插其中,则对于非空二叉树,可得到以下3种不同的遍历方法:
1先序遍历 先序遍历又称先根遍历,记做DLR。先序遍历二叉树的过程如下: (1)访问根结点。 (2)先序遍历左子树。 (3)先序遍历右子树。 例如,图5.14所示的二叉树的先序遍历序列为ABDEGICFH。 图5.14 二叉树
2中序遍历 中序遍历又称中根遍历,记做LDR。中序遍历二叉树的过程如下: (1)中序遍历左子树; (2)访问根结点; (3)中序遍历右子树。 例如,图5.14所示的二叉树的中序遍历序列为DBGIEACHF。 3后序遍历 后序遍历又称后根遍历,记做LRD。后序遍历二叉树的过程如下: (1)后序遍历左子树; (2)后序遍历右子树; (3)访问根结点。 例如,图5.14所示的二叉树的后序遍历序列为DIGEBHFCA。
二叉树的先序遍历、中序遍历和后序遍历是最常用的3种遍历方式,除此之外,有时也采用层次遍历。所谓二叉树的层次遍历,是指从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。例如,图5.14所示的二叉树的层次遍历序列为ABCDEFGHI。二叉树的先序遍历、中序遍历和后序遍历是最常用的3种遍历方式,除此之外,有时也采用层次遍历。所谓二叉树的层次遍历,是指从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。例如,图5.14所示的二叉树的层次遍历序列为ABCDEFGHI。 由层次遍历的定义可知,在进行层次遍历时,对一层结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问,这样一层一层进行,先遇到的结点先访问,这与队列的操作原则比较吻合。因此,在进行层次遍历时,可设置一个队列结构,遍历从二叉树的根结点开始,首先将根结点指针入队列,然后从队头取出一个元素,每取一个元素,执行下面两个操作: 1访问该元素所指结点; 2若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子指针和右孩子指针顺序入队。 此过程不断进行,当队列为空时,二叉树的层次遍历结束。
【算法5.7】 void LeverOrder(BiTree bt) { /*层次遍历二叉树*/ BiTree queue[MAXNODE]; int front,rear; if (bt==NULL) return; front=-1; rear=0; queue[rear]=bt; while (frontrear) { front++; visit(queue[front]→data); /*访问队首结点的数据域*/ if (queue[front]→lchildNULL) /*将队首结点的左孩子结点入队列*/
{ rear++; queue[rear]=queue[front]→lchild; } if (queue[front]→rchildNULL) /*将队首结点的右孩子结点入队列*/ { rear++; queue[rear]=queue[front]→rchild; } } }
(二)二叉树遍历的递归实现 二叉树的先序、中序和后序遍历过程由以下3种递归算法实现: 1先序遍历的递归算法 【算法5.8】 Void PreOrder(BiTree bt) /*先序遍历的递归算法*/ { if (btNULL) { printf("%c",bt→data); /*访问根结点*/ PreOrder(bt→lchild); PreOrder(bt→rchild) } } 2中序遍历的递归算法 【算法5.9】 void InOrder(BiTree bt) /*中序遍历的递归算法*/ { if (bt NULL) { InOrder(bt→lchild); printf("%c",bt→data); /*访问根结点*/ InOrder(bt→rchild); } }