1.31k likes | 1.69k Views
第 6 章 树和二叉树. 树型结构是一类重要的非线性数据结构。其中以树和二叉树最为常用,直观看来,树是以分支关系定义的层次结构。树结构在客观世界中广泛存在,如人类社会的族谱和各种社会组织机构都可用树来形象表示。树在计算机领域中也得到广泛应用,如在编译程序中,可用树来表示源程序的语法结构。又如在数据库系统中,树形结构也是信息的重要组织形式之一。本章重点讨论二叉树的存储结构及其各种操作,并研究树和森林与二叉树的转换关系,最后介绍几个应用例子。. 6 . 1 树的定义和基本术语 ( 1 学时). 树 (Tree) 是 n(n≥0) 个结点的有限集。在任意一棵非空树中:
E N D
第6章 树和二叉树 树型结构是一类重要的非线性数据结构。其中以树和二叉树最为常用,直观看来,树是以分支关系定义的层次结构。树结构在客观世界中广泛存在,如人类社会的族谱和各种社会组织机构都可用树来形象表示。树在计算机领域中也得到广泛应用,如在编译程序中,可用树来表示源程序的语法结构。又如在数据库系统中,树形结构也是信息的重要组织形式之一。本章重点讨论二叉树的存储结构及其各种操作,并研究树和森林与二叉树的转换关系,最后介绍几个应用例子。
6.1 树的定义和基本术语(1学时) 树(Tree)是n(n≥0)个结点的有限集。在任意一棵非空树中: (1)有且仅有一个特定的称为根(Root)的结点; (2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
A B C D E F G H I J K L M 例如,在图6.1中,(a)是只有一个根结点的树;(b)是有13个结点的树,其中A是根,其余结点分成三个互不相交的子集:T1= {B,E,F,K,L},T2={C,G},T3={D,H,I,J,M};T1、T2和T3都是根A的子树,且本身也是一棵树。例如T1,其根为B,其余结点分为两个互不相交的子集;T11={E,K,L},T12={F}。T11和 T12都是B的子树。而T11中E是根,{K}和{L}是正的两棵互不相交的子树,其本身又是只有一个根结点的树。 (a)只有根结点的树 (b)一般的树 图6.1 A
A C G B E D K H M L I J F 树的结构定义是一个递归的定义,即在树的定义中又用到树的概念,它道出了树的固有特性。树还可有其它的表示形式,如图6.2所示为图6.1(b)中树的各种表示。其中 (a)是以嵌套集合(即是一些集合的集体,对于其中任何两个集合,或者不相交,或者一个包含另一个)的形式表示的; (a)
A B E K L F C G D H M I J (b)是以广义表的形式表示的,根作为由子树森林组成的表的名字写在表的左边; (A(B(E(K , L) , F) , C(G) ,D(H(M) , I , J))) (b) (c)用的是凹入表示法(类似书的编目)。 表示方法的多样化,正说明了树结构在 日常生活中及计算机程序设计中的重要 性。一般说来,分等级的分类方案都可 用层次结构来表示,也就是说,都可导 致一个树结构。 ( c )
在给出抽象数据类型树的定义之前,先给出树结构中的一些基本术语。在给出抽象数据类型树的定义之前,先给出树结构中的一些基本术语。 树的结点包含一个数据元素及若干指向其子树的分支。 结点拥有的子树数称为结点的度(Degree)。例如,在图6.1(b)中,A的度为3,C的度为1,F的度为0。 度为0的结点称为叶子(1eaf)或终端结点。图6.1(b)中的结点K、L、F、G、M、I、J都是树的叶子。 度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内务结点的度的最大值。如图6.1(b)的树的度为3。
结点的层次(level)从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第i层,则其子树的根就在第i+1层。其双亲在同一层的结点互为堂兄弟。例如,结点 G与E、F、H、I、J互为堂兄弟。树中结点的最大层次称为树的深度(Depth)或高度。图 6.1(b)所示的树的深度为4。 如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。 森林(Forest)是m(m≥0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。由此,也可以森林和树相互递归的定义来描述树。
就逻辑结构而言,任何一棵树是一个二元组Tree= (root,F),其中:root是数据元素,称做树的根结点;F是m(m≥0)棵树的森林;F=(T1,T2,…,Tn),其中Ti=(ri,Fi)称做根root的第i棵子树;当m!=0时,在树根和其子树森林之间存在下列关系: RF={<root , ri >| i=1 , 2 , … , m , m >0 } 这个定义将有助于得到森林和树与二叉树之间转换的递归定义。 树的应用广泛,在不同的软件系统中树的基本操作集不尽相同。
抽象数据类型树的定义。 ADT Tree { 数据对象D: D是具有相同特性的数据元素的集合。 数据关系R: 若D为空集,则称为空树;若D仅含一个数据元素,则R为空集,否则R={H}, H是如下二元关系: (1)在D中存在唯一的称为根的数据元素root,它在关系H下无前驱; (2)若D-{root}!=Φ,则存在D-{root)的一个划分D1,D2,…,Dm (m>0),对任意j!=k(1≤j,k≤m)有Di∩Dk=Φ,且对任意的i(1≤i≤m),唯一存在数据元素xi∈Di,有<root,xi>∈H;
(3)对应于D-{root}的划分,H-{<root,x1>,…,<root,xm>}有唯一的一个划分H1,H2,…,Hm(m>0),对任意j!=k(1≤j,k≤m)有Hj∩Hk=Φ,对任意i(1≤i≤m),Hi是Di上的二元关系,(Di ,{Hi})是一棵符合本定义的子树,称为根root的子树。 基本操作: InitTree(&T); 操作结果:构造空树T。 DestroyTree(&T); 初始条件:树T存在。 操作结果:销毁树T。
CreateTree(&T,definition); 初始条件:definition给出树T的定义。 操作结果:按definition构造树T。 ClearTree(&T); 初始条件:树T存在。 操作结果:将树T清为空树。 TreeEmpty(T); 初始条件:树T存在。 操作结果:若T为空树,则返回TRUE,否则FALSE; TreeDepth(T); 初始条件:树T存在。 操作结果:返回T的深度。
Root(T); 初始条件:树T存在。 操作结果:返回T的根。 Value(T,cur_e); 初始条件:树T存在,cur_e是T中某个结点。 操作结果:返回cur_e的值。 Assign(T,cur_e,value); 初始条件:树T存在,cur_e是T中某个结点。 操作结果:结点cur_e赋值为value。 Parent(T,cur_e); 初始条件:树T存在,cur_e是T中某个结点。 操作结果:若cur_e是T的非根结点,则返回它的双亲。 否则函数值为“空”。
LeftChild(T,cur_e); 初始条件:树T存在,cur_e是T中某个结点。 操作结果:若cur_e是T的非叶子结点,则返回它的最左孩子,否则返回“空”。 Rightsibling(T,cur_e); 初始条件:树T存在,cur_e是T中某个结点。 操作结果:若cur_e有右兄弟,则返回它的右兄弟,否则函数值为“空”。 InsertChild(&T,&p,i,c); 初始条件:树T存在,p指向T中某个结点,1≤i≤p指结点的度+1,非空树c与T不相交。 操作结果:插入c为T中p指结点的第i棵子树。
DeleteChild(&T,&p,i); 初始条件:树T存在,p指向T中某个结点,1≤i≤p指结点的度。 操作结果:删除T中p所指结点的第i棵子树。 TraverseTree(T,Visit()); 初始条件:树T存在,Visit是对结点操作的应用函数。 操作结果:按某种次序对T的每个结点调用函数,visit()一次至多次,一旦visit()失败,则操作失败。 }ADT Tree
6.2 二 叉 树(2学时) 在讨论一般树的存储结构及其操作之前,我们首先研究一种称为二叉树的抽象数据。 6.2.1 二叉树的定义 二叉树(BinaryTree)是另一种树型结构,它的特点是每个结点至多只有二棵子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。
ADT BinaryTree { 数据对象D: D是具有相同特性的数据元素的集合。 数据关系R: 若D= Φ,则R=Φ,称BinaryTree为空二叉树; 若D!= Φ,则R={H},H是如下二元关系: (1)在D中存在唯一的称为根的数据元素root,它在关系H 下无前驱; (2)若D-{root}!=Φ,则存在D-{root}={Dl,Dr},且Dl∩Dr=Φ;
(3)若D!=Φ,则Dl中存在唯一的元素xl,<root,xl>∈H,且存在Dl上的关系H1为H的子集;若Dr!=Φ,则Dr中存在唯一的元素xr,<root,xr>∈H,且存在Dr上的关系Hr为H的子集; H={<root,xl>,<root,xr>,Hl,Hr}; (4)(Dl,{Hl})是一棵符合本定义的二叉树,称为根的左子树,(Dr,{Hr})是一棵符合本定义的二叉树,称为根的右子树。
基本操作P: InitBiTree(&T); 操作结果:构造空二叉树T。 DestroyBiTree(&T); 初始条件:二叉树T存在。 操作结果:销毁二叉树T。 CreateBiTree(&T,definition) 初始条件:definition给出二叉树T的定义。 操作结果:按definition构造二叉树T。 ClearBiTree(&T); 初始条件:二叉树T存在。 操作结果:将二叉树T清为空树。
BiTreeEmpty(T); 初始条件:二叉树T存在。 操作结果:若T为空二叉树,则返回TRUE, 否则FALSE。 BiTreeDepth(T); 初始条件:二叉树T存在。 操作结果:返回T的深度。 Root(T); 初始条件:二叉树T存在。 操作结果:返回T的根。 Value(T,e); 初始条件:二叉树T存在,e是T中某个结点。 操作结果:返回e的值。
Assign(T,&e,value); 初始条件:二叉树T存在,e是T中某个结点。 操作结果:结点e赋值为value。 Parent(T,e); 初始条件:二叉树T存在,e是T中某个结点。 操作结果:若e是T的非根结点,则返回它的双亲。 LeftChild(T,e); 初始条件:二叉树T存在,e是T中某个结点。 操作结果:返回e的左孩子。若e无左孩子,则返回“空”。 RightChild(T,e); 初始条件:二叉树T存在,e是T中某个结点。 操作结果:返回e的右孩子。若e无右孩子,则返回“空”。
Leftsibling(T,e); 初始条件:二叉树T存在,e是T中某个结点。 操作结果:返回e的左兄弟。若e是T的左孩子或无左兄弟,则返回“空”。 RightSibling(T,e); 初始条件:二叉树T存在,e是T中某个结点。 操作结果:返回e的右兄弟。若e是T的右孩子或无右兄弟,则返回“空”。
InsertChild(T , p , LR , c); 初始条件:二叉树T存在,p指向T中某个结点,LR为0或1,非空二叉树c与T不相交且右子树为空。 操作结果:根据LR为0或1,插入c为T中p所指结点的左或右子树。p所指结点的原有左或右子树则成为c的右子树。 DleteChild(T,p,LR); 初始条件:二叉树T存在,p指向T中某个结点,LR为0或1。 操作结果:根据Ix为0或1,删除T中p所指结点的左或右子树。
LevelOrderTraverse(T,Visit()); 初始条件:二叉树T存在,Visit是对结点操作的应用函数。 操作结果:层序遍历T,对每个结点调用函数Visit一次且仅一次, 一旦visit()失败,则操作失败。 PreOrderTraverse(T,Visit()); 初始条件:二叉树T存在,Visit是对结点操作的应用函数。 操作结果:先序遍历T,对每个结点调用函数Visit一次且仅一次,一旦visit失败,则操作失败。
InOrderTraverse(T,Visit()); 初始条件:二叉树T存在,Visit是对结点操作的应用函数。 操作结果:中序遍历T,对每个结点调用函数Visit一次且仅一次,一旦visit失败,则操作作失败。 PostOrderTraverse(T,Visit()); 初始条件:二叉树T存在,Visit是对结点操作的应用函数。 操作结果:后序遍历T,对每个结点调用函数Visit一次且仅一次,一旦visit失败,则操作失败。 }ADT BinaryTree
上述数据结构的递归定义表明二叉树或为空,或是由一个根结点加上两棵分别称为左子树和右子树,两棵互不相交的二叉树组成。由于这两棵子树亦是二叉树,则由二叉树的定义它们可以是空树。由此二叉树可以有五种基本形态 (a)空二叉树;(b)仅有根结点的二叉树;(c)右子树为空的二叉树;(d)左、右子树均非空的二叉树;(e)左子树为空的二叉树。
6.2.2 二叉树的性质 二叉树具有下列重要特性。 性质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的二叉树的最大结点数为性质2深度为k的二叉树至多有2k-1个结点,(k≥1)。由性质1可见,深度为k的二叉树的最大结点数为 k k ∑(第i层上的最大结点数) =∑2i-1=2k -1 i=1 i=1
性质3对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。性质3对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。 设n1为二叉树T中度为1的结点数。因为二叉树中所有结点的度均小于或等于2所以其结点总数为 n=n0+n1+n2 (6—1) 再看二叉树中的分支数。除了根结点外,其余结点都有一个分支进入,设B为分支总数,则n=B+1。由于这些分支是由度为1或2的结点射出的,所以又有B=n1+ 2n2。 n=n1+2n2+1 (6—2) 于是得由式(6—1)和(6—2)得 n0=n2+1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 完全二叉树和满二叉树,是两种特殊形态的二叉树。 一棵深度为k且有2k-1个结点的二叉树称为满二叉树。如图6.4(a)所示是深度为4的满二叉树,这种树的特点是每一层上的结点数都是最大结点数。 6.4(a) 可以对满二叉树的结点进行连续编号,约定编号从根结点起,自上而下,自左至右。由此可引出完全二叉树的定义:深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称之为完全二叉树。
1 1 1 2 3 2 3 2 3 4 5 6 7 4 5 4 5 6 8 9 10 11 12 6 7 如图6.4(b)所示为一棵深度为4的完全二叉树。显然,这种树的特点是:(1)叶子结点只可能在层次最大的两层上出现;(2)对任一结点,若其右分支下的子孙的最大层次为L,则其左分支下的子孙的最大层次必为L或L+1。如图6.4中(c)和(d)不是完全二叉树。 对应顺序存储结构 6.4(b) 6.4(c) 6.4(d)
完全二叉树将在很多场合下出现,下面介绍完全二叉树的两个重要特性。完全二叉树将在很多场合下出现,下面介绍完全二叉树的两个重要特性。 性质4具有n个结点的完全二叉树的深度为[ log2n ]+1。 证明:假设深度为k,则根据性质2和完全二叉树的定义有 2k-1-1<n≤2k-1 或 2k-1≤n<2k 于是k-1≤log2n<k 因为k是整数,所以 k = [ log2n ]+1。
i+1 2i 2i+1 2i+2 2i+3 i [i/2] … i i+1 … 2i+3 2i 2i+1 2i+2 性质5如果对一棵有n个结点的完全二叉树(其深度为[log2n]+1)的结点按层序编号(从第1层到第[log2n]+1层,每层从左到右),则对任一结点i(1≤i≤n),有 (1)如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲PARENT(i)是结点[ i/2 ]。 (2) 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子LCHILD(i)是结点2i。 (3)如果2i+1>n,则结点i无右孩子;否则其右孩子RCHILD(i)是结点2i+1。 (a)结点i和i+1在同一层上; (b)结点i和i+1不在同一层上。 图6.5 完全二义树中结点i和i+1的左、右孩子
我们只要先证明(2)和(3),便可以从(2)和(3)导出(1)。我们只要先证明(2)和(3),便可以从(2)和(3)导出(1)。 对于i=1,由完全二叉树的定义,其左孩子是结点2。若2>n,即不存在结点2,此时结点i无左孩子。结点i的右孩子也只能是结点3,若结点3不存在,即3>n,此时结点i无右孩子。 对于i>1可分两种情况讨论: (1)设第j (1≤j≤[log2n] )层的第一个结点的编号为i(由二叉树的定义和性质2可知i=2j-1),则其左孩子必为第j+1层的第一个结点,其编号为2j=2(2j-1)=2i,若2i>n,则无左孩子;其右孩子必为第j+1层的第二个结点,其编号为2i+1,若2i+1>n,则无右孩子;
6.2.3 二叉树的存储结构 一、顺序存储结构 //———二叉树的顺序存储表示———— #define MAX_TREE_SIZE 100, //二叉树的最大结点数 typedef TElemType SqBiTree[MAX_TREE_SIZE]; //0号单元存储根结点 SqBiTree bt;
按照顺序存储结构的定义,在此约定,用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素,即将完全二叉树上编号为i的结点元素存储在如上定义的一维数组中下标为i-1的分量中。(参见图6.4(b) (c) ) (a)完全二叉树; (b)一般二叉树。 图6.6 二叉树的顺序存储结构
PARENT DATA (a)二叉树的结点; LCHILD RCHILD lchild data rchild (b)含有两个指针域的结点结构; lchild data parent rchild (c)含有三个指针域的结点结构 二、链式存储结构 设计不同的结点结构可构成不同形式的链式存储结构。由二叉树的定义得知,二叉树的结点(如图6.7(a)所示)由一个数据元素和分别指向其左、右子树的两个分支构成,则表示二叉树的链表中的结点至少包含三个域:数据域和左、右指针域,如图6.7(b)所示。有时,为了便于找到结点的双亲,则还可在结点结构中增加一个指向其双亲结点的指针域,如图6.7(c)所示。 图6.7 二义树的结点及其存储结构
A A A A B A B B B B C D C D C D C E F C D E F E F G D G G 利用这两种结点结构所得二叉树的存储结构分别称之为二叉链表和三叉链表,如图6.8所示。链表的头指针指向二叉树的根结点。容易证得,在含有n个结点的二叉链表中有n+1个空链域。在6.3节中我们将会看到可以利用这些空链域存储其它有用信息,从而得到另一种链式存储结构—线索链表。 (a)单支树的二叉链表; (b)二叉链表; (c)三叉链表。 图6.8 链表存储结构
6.3 遍历二叉树和线索二叉树(2学时) 6.3.1 遍历二叉树 在二叉树的一些应用中,常常要求在树中查找具有某种特征的结点,或者对树中全部结点逐一进行某种处理。这就提出了一个遍历二叉树(Traversing Binary Tree)的问题,即如何按某条搜索路径巡访树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。“访问”的含义很广,可以是对结点作各种处理,如输出结点的信息等。遍历对线性结构来说,是一个容易解决的问题。而对二叉树则不然,由于二叉树是一种非线性结构,每个结点都可能有两棵子树,因而需要寻找一种规律,以便使二叉树上的结点能排列在一个线性队列上,从而便于遍历。
回顾二叉树的递归定义可知,二叉树是由三个基本单元组成:根结点、左子树和右子树。因此,若能依次遍历这三部分,便是遍历了整个二叉树。假如以L、D、R分别表示遍历左子树、访问根结点和遍历右子树,则可有DLR、LDR、LRD、DRL、RDL、RLD六种遍历二叉树的方案。若限定先左后右,则只有前三种情况,分别称之为先(根)序遍历,中(根)序遍历和后(根)序遍历。基于二叉树的递归定义,可得下述遍历二叉树的递归算法定义。回顾二叉树的递归定义可知,二叉树是由三个基本单元组成:根结点、左子树和右子树。因此,若能依次遍历这三部分,便是遍历了整个二叉树。假如以L、D、R分别表示遍历左子树、访问根结点和遍历右子树,则可有DLR、LDR、LRD、DRL、RDL、RLD六种遍历二叉树的方案。若限定先左后右,则只有前三种情况,分别称之为先(根)序遍历,中(根)序遍历和后(根)序遍历。基于二叉树的递归定义,可得下述遍历二叉树的递归算法定义。 先序遍历二叉树的操作定义为: 若二叉树为空,则空操作;否则 (1)访问根结点; (2)先序遍历左子树; (3)先序遍历右子树。
中序遍历二叉树的操作定义为: 若二叉树为空,则空操作;否则 (1)中序遍历左子树; (2)访问根结点; (3)中序遍历右子树。 后序遍历二叉树的操作定义为: 若二叉树为空,则空操作;否则 (1)后序遍历左子树; (2)后序遍历右子树; (3)访问根结点。
N T Y Ok N Visit ( T) Y Return error PreOrderTraverse(T->lchild,Visit) N Y Return error PreOrderTraverse N (T->rchild,Visit) Y Return error Return Ok 先序遍历二叉树的递归算法 PreOrderTraverse ( T, visit)
Status PreOrderTraverse ( Bitree T, Status (*Visit)(TElemType e)){ //采用二叉链表存储结构, //visit是对数据元素操作的应用函数, //先序遍历二叉树T的递归算法, //对每个数据元素调用函数visit。 //最简单的visit函数是: // Status PrintElement(TElemType e){ // printf(e); 输出元素e的值,实用时,加上格式串 // return OK;} //调用实例:PreOrderTraverse(T,PrintElement);
if(T) { if(Visit(T->data)) if(PreOrderTraverse(T->lchild,Visit)) if(PreOrderTraverse(T->rchild,Visit)) return OK; return ERROR;} else return OK; }//PreOrderTraverse 算法6.1
- + / a * e f b - c d 例如图6.9所示的二叉树表示下述表达式 a+b*(c-d)-e/f 若先序遍历此二叉树,按访问结点的先后次序将结点排列起来,可得到二叉树的先序序列为 -+a*b-cd/ef (6—3) 类似地,中序遍历此二叉树,可得此二叉树的中序序列为 a+b*c-d-e/f (6—4) 后序遍历此二叉树,可得此二叉树的后序 序列为 abcd -*+ef/- (6—5) 从表达式来看,以上三个序列(6—3)、 (6—4)和(6—5)恰好为表达式的前缀表 示(波兰式)、中缀表示和后缀表示 (逆波兰式)。 图6.9表达式的二叉树
Status InOrderTraverse(BiTree T, Status(*Visit)(TElemType e)){ //采用二叉链表存储结构,Visit是对数据元素操作的应用函数。 //中序遍历二叉树T的非递归算法,对每个数据元素调用函数Visit。 InitStack(S); Push(S,T);//根指针进栈 while(!StackEmpty(S)){ while(GetTop(S,p)&&p)Push(S,p->lchild); //向左走到尽头 Pop(S,p); //空指针退栈
if(!StackEmpty(s)){ //访问结点,向右一步 Pop(S,p); if(!Visit(p->data)) return ERROR; Push(S,p->rchild); }//if }//While return OK; }// InOrderTraverse 算法 6.2
Status InOrderTraverse(BiTreeT, Status(*Visit)(TElemType e)){ //采用二叉链表存储结构,Visit是对数据元素操作的应用函数。 //中序遍历二叉树T的非递归算法,对每个数据元素调用函数Visit。 InitStack(S); p=T; while(p || !StackEmpty(S)){ if(p){ Push(S,p); p=p->lchild; }//根指针进栈,遍历左子树