slide1 n.
Download
Skip this Video
Loading SlideShow in 5 Seconds..
第五章树和二叉树 第一节 树的概念与表示 第二节 树的基本操作与存储 第三节 二叉树 PowerPoint Presentation
Download Presentation
第五章树和二叉树 第一节 树的概念与表示 第二节 树的基本操作与存储 第三节 二叉树

Loading in 2 Seconds...

play fullscreen
1 / 118

第五章树和二叉树 第一节 树的概念与表示 第二节 树的基本操作与存储 第三节 二叉树 - PowerPoint PPT Presentation


  • 196 Views
  • Uploaded on

第五章树和二叉树 第一节 树的概念与表示 第二节 树的基本操作与存储 第三节 二叉树 第四节 二叉树的遍历 第五节 线索二叉树 第六节 二叉树的应用 第七节 树、森林与二叉树的转换 本 章 小 结 实训一 实训二 思 考 与 习 题. 第五章 树和二叉树. 学习要求: 要求掌握树和二叉树的概念,二叉树的性质,存储结构以及基本运算,并用二叉树解决一些综合应用问题。 主要内容:

loader
I am the owner, or an agent authorized to act on behalf of the owner, of the copyrighted work described.
capcha
Download Presentation

PowerPoint Slideshow about '第五章树和二叉树 第一节 树的概念与表示 第二节 树的基本操作与存储 第三节 二叉树' - emilia


An Image/Link below is provided (as is) to download presentation

Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author.While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server.


- - - - - - - - - - - - - - - - - - - - - - - - - - E N D - - - - - - - - - - - - - - - - - - - - - - - - - -
Presentation Transcript
slide1
第五章树和二叉树

第一节 树的概念与表示

第二节 树的基本操作与存储

第三节 二叉树

第四节 二叉树的遍历

第五节 线索二叉树

第六节 二叉树的应用

第七节 树、森林与二叉树的转换

本 章 小 结

实训一 实训二

思 考 与 习 题

slide2

第五章 树和二叉树

学习要求:

要求掌握树和二叉树的概念,二叉树的性质,存储结构以及基本运算,并用二叉树解决一些综合应用问题。

主要内容:

树型结构是一类非常重要的非线性结构,其中以树和二叉树最为常用。本章介绍了树和二叉树的概念、二叉树的性质、存储结构、基本运算以及哈夫曼树的定义和构造过程。

slide3
在前面几章里讨论的数据结构主要是线性结构,线性结构中数据元素的关系是一一对应的,其特点是逻辑结构简单,易于实现基本操作。然而,线性结构在许多实际应用中不能明确、方便地表示数据元素之间的复杂关系。要描述这些元素之间的复杂关系应采用非线性结构。所谓非线性结构是指,在该结构中至少存在一个数据元素有两个或两个以上的直接前驱(或直接后继)元素。在前面几章里讨论的数据结构主要是线性结构,线性结构中数据元素的关系是一一对应的,其特点是逻辑结构简单,易于实现基本操作。然而,线性结构在许多实际应用中不能明确、方便地表示数据元素之间的复杂关系。要描述这些元素之间的复杂关系应采用非线性结构。所谓非线性结构是指,在该结构中至少存在一个数据元素有两个或两个以上的直接前驱(或直接后继)元素。

本章和下一章将分别介绍两种重要的非线性结构:树和图。树型结构中结点之间有分支关系,又具有层次关系,它非常类似于自然界中的树。树型结构在现实世界中广泛存在,如家族的家谱、各单位的行政组织机构等都可用树来形象地表示。在计算机领域中,操作系统中对磁盘文件的管理就采用了树形目录结构;在数据库中,树型结构也是数据的重要组织形式之一。本章将重点讨论二叉树的存储结构及各种操作,并研究树、森林与二叉树的转换及应用实例。

slide4
第一节 树的概念与表示

一、树的定义及相关术语

(一)树的定义

树(tree)是n(n≥0)个结点的有限集T。当n=0即T为空时,称为空树;当n>0时,T满足如下两个条件:

1.有且仅有一个特定的称为根(root)的结点,它没有直接前驱,但有零个或多个直接后继。

2.其余的n-1个结点可分为m(m≥0)个互不相交的子集T1,T2,…,Tm,其中每个子集本身又是一棵树,并称其为根的子树(subtree)。

slide6
树的定义是递归的,因为在树的定义中又用到树的定义。图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。

slide7
结点的度:一个结点所拥有的子树个数称为该结点的度。图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互为兄弟。

slide8
路径与路径长度:对于任意两个结点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。

slide9
树的高度(或深度):树中结点的最大层数称为树的高度或深度。图5.1中,树的高度为4。树的高度(或深度):树中结点的最大层数称为树的高度或深度。图5.1中,树的高度为4。

有序树和无序树:将树中每个结点的各子树看成是从左到右有次序的(即不能随意变换),则称该树为有序树;否则,该树为无序树。图5.2中的两棵树,作为有序树,它们是不同的,因为结点A的两个孩子在两棵树中的左右次序不同;但作为无序树,它们是相同的。注意:如果不特别指明,我们这一章讨论的树都是有序树。

图5.2两棵不同的有序树

森林:m(m≥0)棵互不相交的树的集合。树和森林的概念很相近。将一棵非空树的根结点删去,树就生成了森林;反之,给森林增加一个统一的根结点,森林就变成一棵树。

slide10
二、树的表示

在不同的场合,树的表示方法也不尽相同,通常树的表示法有四种。

(一)树形表示法

用一个圆圈表示一个结点,圆圈内的符号代表该结点的数据信息,结点之间的关系通过连线表示。虽然每条连线上都不带有箭头,但它仍然是有向的,其方向隐含着从上向下,即连线的上方结点是下方结点的前驱,下方结点是上方结点的后继。它的直观形象是一棵倒置的树(树根在上,树叶在下),如图5.1所示。本书主要采用树形表示法来表示树。

(二)嵌套表示法

每棵树对应一个圆圈,圆圈内包含根结点和子树的圆圈,同一个根结点下的各子树对应的圆圈是不能相交的。用这种方法表示的树中,结点之间的关系是通过圆圈的包含来表示的。如图5.3(a)所示。

slide11

(a)嵌套表示法

(b)凹入表示法

图5.3 树的表示

slide12
(三)凹入表示法

每棵树的根对应着一个线条,子树的根对应着一个较短的线条,且树的根在上,子树的根在下,同一个根下的各子树的根对应的线条长度是一样的。如图5.3(b)所示。

(四)广义表表示法

广义表表示法也称为嵌套括号表示法。每棵树对应一个由根作为名字的表,表名放在表的左边,表是由在一个圆括号里的各子树对应的表组成的,之间用逗号分开。用这种方法表示的树中,结点之间的关系是通过圆括号的嵌套表示的。如图5.3(c)所示。

slide13
第二节 树的基本操作与存储

一、树的基本操作

树的基本操作通常有以下几种:

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中的每个结点,且使每个结点只被访问一次,得到一个由所有结点组成的序列。遍历操作是非线性结构中经常用到的基本操作,许多对树的操作都是借助于该操作实现的。

slide14
二、树的存储结构

树的存储要求既要存储结点的数据元素本身,又要存储结点之间的逻辑关系。树的存储结构很多,下面介绍四种常用的存储结构,即双亲表示法、孩子链表表示法、双亲链表孩子表示法和孩子兄弟链表表示法。

(一)双亲表示法

由树的定义可知,在树中每个结点的双亲是唯一的。利用这一性质,可在存储结点信息的同时,为每个结点附设一个指向其双亲的指针parent,就可唯一地表示任何一棵树。尽管可用动态链表来实现这种表示,然而用一维数组来表示更为方便。

类型说明如下:

#define MAXTREESIZE 100 /*向量空间的大小,由用户自己定义*/

typedef char DataType; /*用户自己定义数据类型*/

typedef struct{

DataType data; /*结点数据*/

int parent; /*双亲指针,指示结点的双亲在向量中的位置*/

slide15

}

PTreeNode;

typedef struct

{

PTreeNode nodes[MAXTREESIZE];

int n; /*结点总数*/

}

PTree;

PTree T; /*T是双亲链表*/

(a) 树

(b) 双亲表示法

图5.4 树及其双亲表示法

slide16
图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)操作也比较困难。在实际中,如果需要实现这些操作,可在结点结构中增设存放第一个孩子的域和存放第一个右兄弟的域,这样就能较方便地实现上述操作了。

(二)孩子链表表示法

孩子链表表示法中,主体是一个与结点个数一样大小的一维数组,数组的每一个元素由两个域组成,一个域用来存放结点信息,另一个域用来存放指针,该指针指向由该结点孩子组成的单链表的首位置。单链表中的结点也由两个域组成,一个存放孩子结点在一维数组中的序号,另一个是指针域,指向下一个孩子结点。

slide17
类型说明如下:

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为孩子链表表示法*/

slide18

图5.4(a)中的树,其孩子链表表示如图5.5所示。图5.4(a)中的树,其孩子链表表示如图5.5所示。

与双亲表示法相反,孩子链表表示法便于实现涉及孩子及其子孙的运算,但不便于实现与双亲有关的运算。因此可将这两种表示法结合起来,形成双亲孩子链表表示法。

图5.5 孩子链表表示

slide19
(三)双亲孩子链表表示法

双亲孩子链表表示法是将双亲链表表示法和孩子链表表示法相结合的存储结构,其仍将各结点的孩子结点分别组成单链表,同时用一维数组顺序存储树中的各结点,数组元素除了包括结点本身的数据data和该结点的孩子结点链表的头指针firstchild之外,还增设一个域parent,存储其双亲结点在数组中的序号。图5.4(a)中的树,其双亲孩子链表表示如图5.6所示。

图5.6 双亲孩子链表表示

slide20

(四)孩子兄弟链表表示法

孩子兄弟链表表示法是一种常用的存储结构。其方法是:每个结点除其信息域外,再增加两个指针域,分别指向该结点的第一个孩子结点和下一个兄弟结点。

图5.4(a)中树的孩子兄弟链表表示如图5.7所示。

图5.7 孩子兄弟链表表示

这种存储结构的最大优点是:它和二叉树的二叉链表表示法完全一样,因此可利用二叉树的算法来实现对树的操作。

slide21
第三节 二叉树

二叉树是树型结构的一个重要类型,关于二叉树的存储结构及算法都较为简单,因此,二叉树在数据结构的研究领域中具有很重要的地位。

一、二叉树的基本概念

二叉树(binary tree)是n(n≥0)个结点的有限集T,它或者是空集(n=0),或者同时满足下述两个条件:

1.有且仅有一个称为根的结点。

2.其余结点分为两个互不相交的集合T1和T2,T1和T2称为根的左子树和右子树。

二叉树的定义也是一个递归定义,表明二叉树或为空,或是由一个根结点加上两棵分别称为左子树和右子树的二叉树组成。

二叉树的特点是每个结点至多只有二棵子树(即二叉树中不存在度大于2的结点),并且二叉树的子树有左右之分,其次序不能任意颠倒。因此即使一个结点只有一棵非空子树,仍须区别它是该结点的左子树还是右子树,这与树是不同。

slide22

二叉树可以有五种基本形态,如图5.8所示。

图5.8 二叉树的五种基本形态

在一棵二叉树中,如果所有分支结点都有左孩子结点和右孩子结点,并且叶子结点都集中在二叉树的最下面一层,这样的二叉树称为满二叉树。图5.9(a)所示就是一棵满二叉树。我们可以对满二叉树的结点进行连续编号,约定编号从树根为1开始,按照层数从小到大、同一层从左到右的次序进行。注意:图5.9中每个结点旁的数字为对应该结点的编号。

slide23
若二叉树中最多只有最下面两层的结点度数可以小于2,并且最下面一层的叶子结点都依次排列在该层最左边的位置上,则这样的二叉树称为完全二叉树。图5.9(b)所示为一棵完全二叉树。同样可以对完全二叉树中每个结点进行连续编号,编号的方法与满二叉树相同的。若二叉树中最多只有最下面两层的结点度数可以小于2,并且最下面一层的叶子结点都依次排列在该层最左边的位置上,则这样的二叉树称为完全二叉树。图5.9(b)所示为一棵完全二叉树。同样可以对完全二叉树中每个结点进行连续编号,编号的方法与满二叉树相同的。

(a) 满二叉树

slide24
(b) 完全二叉树

图5.9 满二叉树和完全二叉树

显然满二叉树是完全二叉树,但完全二叉树不一定是满二叉树。在满二叉树的最下一层上,从最右边开始删去若干结点后得到的二叉树仍然是一棵完全二叉树。因此,在完全二叉树中,若某个结点没有左孩子,则它一定没有右孩子,即该结点必是叶子结点。

slide25
二、二叉树的主要性质

性质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

故命题正确。

slide26
性质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。

slide27
性质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无右兄弟。

此性质可采用数学归纳法证明。证明略。

slide28
三、二叉树的存储

二叉树的存储结构有两种:顺序存储结构和链式存储结构。

(一)顺序存储结构

二叉树的顺序存储结构是指用一组连续的存储单元来存放二叉树的数据元素。首先采用一维数组作存储结构,然后将二叉树中的各结点进行编号,要求该编号与等高的完全二叉树中对应位置上的结点编号相同,并以结点编号作为下标,把各结点的值对应存放到一维数组中去。

显然这种存储方法对于完全二叉树和满二叉树来说是非常方便的,因为树中结点的序号可以唯一反映出结点之间的逻辑关系,这样既能最大限度地节省存储空间,又可利用数据元素的下标值确定结点在二叉树中的位置以及结点之间的关系,如图5.10所示是一棵完全二叉树及其顺序存储结构,bt[0]用来存放结点的个数。

slide29
(a) 完全二叉树

(b)顺序存储结构

图5.10 完全二叉树及其顺序存储结构

slide30
但是,对于一般的二叉树,如果按照完全二叉树的形式来存储,则会造成存储空间的浪费,因为增加了一些并不存在的结点。图5.11所示是一棵一般二叉树及其顺序存储结构,图(b)中以”¢”表示不存在的结点。但是,对于一般的二叉树,如果按照完全二叉树的形式来存储,则会造成存储空间的浪费,因为增加了一些并不存在的结点。图5.11所示是一棵一般二叉树及其顺序存储结构,图(b)中以”¢”表示不存在的结点。

最坏的情况是单支二叉树(即每个结点只有左孩子或右孩子)。如图5.12所示,一棵深度为3的右单支二叉树,该二叉树实际只有3个结点,如果按照完全二叉树的形式来存储,则空间浪费太大。这是这种存储结构的一大缺点。另外,由于顺序存储结构固有的一些缺陷,会使二叉树的插入、删除等操作不方便,且效率也比较低。因此,对于一般二叉树来说,更适合的存储方法是采用链式存储结构。

slide31
(a)一般二叉树

(b) 顺序存储结构

图5.11 一般二叉树及其顺序存储结构

slide32
(a) 单支二叉树
  • 单支二叉树的顺序存储结构
  • 图5.12 单支二叉树及其顺序存储结构
slide33
(二)链式存储结构

二叉树的链式存储结构是指采用链表形式来存储一棵二叉树,二叉树中的每一个结点用链表中的一个链结点来存储。二叉树的每个结点最多有两个孩子,用链式存储二叉树时,每个结点除了存储结点本身的数据外,还应设置两个指针域,分别指向该结点的左孩子和右孩子,结点的结构为:

其中,data域存放某结点的数据信息;lchild与rchild域分别存放指向左孩子和右孩子的指针,当左孩子或右孩子不存在时,相应指针域值为空(用符号∧或NULL表示)。

相应的类型说明为:

typedef char DataType;

typedef struct BiTNode{

DataType data;

struct BiTNode *lchild,*rchild; /*左右孩子指针*/

}BiTNode, *BiTree;

slide34
在一棵二叉树中,所有类型为BiTNode的结点,再加上一个指向开始结点(根结点)的BiTree型头指针(即根指针)root,就构成了二叉树的链式存储结构,并将其称为二叉链表。图5.13(a)所示的二叉树的二叉链表如图5.13(b)所示。在一棵二叉树中,所有类型为BiTNode的结点,再加上一个指向开始结点(根结点)的BiTree型头指针(即根指针)root,就构成了二叉树的链式存储结构,并将其称为二叉链表。图5.13(a)所示的二叉树的二叉链表如图5.13(b)所示。

(a) 二叉树

(b) 二叉链表表示

slide35
显然,一个二叉链表由根指针root唯一确定。若二叉树为空,则root=NULL。若结点的某个孩子不存在,则相应的指针为空。在具有n个结点的二叉树的二叉链表表示中,一共有2n个指针域,其中只有n-1个用来指示结点的左、右孩子,其余的n+1个指针域为空。显然,一个二叉链表由根指针root唯一确定。若二叉树为空,则root=NULL。若结点的某个孩子不存在,则相应的指针为空。在具有n个结点的二叉树的二叉链表表示中,一共有2n个指针域,其中只有n-1个用来指示结点的左、右孩子,其余的n+1个指针域为空。

(c) 三叉链表表示

图5.13 二叉树及其链式存储结构

slide36
有时为了方便访问某结点的双亲,还可以给链表结点增加一个指向其双亲的指针parent,结点的结构为:有时为了方便访问某结点的双亲,还可以给链表结点增加一个指向其双亲的指针parent,结点的结构为:

利用这样的结点结构表示的二叉树的链式存储结构被称为三叉链表。图5.13(a)所示的二叉树的三叉链表如图5.13(c)所示。

与前面讨论过的线性链表一样,对于二叉树,无论采用哪种形式的链式存储结构,都要

给出根结点所在链结点的存储地址,否则,有关操作将无法进行。

二叉链表存储结构具有灵活、方便的特点,结点的最大数目只受系统最大可用存储空间的限制。对于一般的二叉树,二叉链表存储结构不仅比顺序存储结构节省空间(用于存储指针的空间开销只是二叉树中结点数的线性函数),而且对二叉树实施有关操作也很方便。因此,二叉链表的使用更加广泛。

slide37
四、二叉树的基本操作及实现

(一)二叉树的基本操作

归纳起来,二叉树通常有以下基本操作。

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的右子树。

slide38
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;

}

slide39
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的左孩子结点。

slide40
【算法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;

}

slide41
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

slide42
{ 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;}

slide43
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)是关于二叉树遍历操作的实现,将在下一节中重点介绍。

slide44
第四节 二叉树的遍历

一、二叉树的遍历方法及递归实现

(一)二叉树的遍历方法

二叉树的遍历是指按照一定次序访问树中所有结点,并且每个结点仅被访问一次的过程。

遍历是二叉树中经常要用到的一种操作。因为在实际应用问题中,常常需要按一定顺序对二叉树中的每个结点逐个进行访问,或查找具有某一特点的结点,然后对这些满足条件的结点进行处理。

由于在二叉树中,左子树和右子树是有严格区别的,因此在遍历一棵非空二叉树时,根据访问根结点(D)、遍历左子树(L)和遍历右子树(R)的先后关系可以组合成6种遍历方法:

DLR,LDR,LRD,DRL,RDL,RLD。若规定先遍历左子树(L),后遍历右子树(R),再把访问根结点(D)穿插其中,则对于非空二叉树,可得到以下3种不同的遍历方法:

slide45
1先序遍历

先序遍历又称先根遍历,记做DLR。先序遍历二叉树的过程如下:

(1)访问根结点。

(2)先序遍历左子树。

(3)先序遍历右子树。

例如,图5.14所示的二叉树的先序遍历序列为ABDEGICFH。

图5.14 二叉树

slide46
2中序遍历

中序遍历又称中根遍历,记做LDR。中序遍历二叉树的过程如下:

(1)中序遍历左子树;

(2)访问根结点;

(3)中序遍历右子树。

例如,图5.14所示的二叉树的中序遍历序列为DBGIEACHF。

3后序遍历

后序遍历又称后根遍历,记做LRD。后序遍历二叉树的过程如下:

(1)后序遍历左子树;

(2)后序遍历右子树;

(3)访问根结点。

例如,图5.14所示的二叉树的后序遍历序列为DIGEBHFCA。

slide47
二叉树的先序遍历、中序遍历和后序遍历是最常用的3种遍历方式,除此之外,有时也采用层次遍历。所谓二叉树的层次遍历,是指从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。例如,图5.14所示的二叉树的层次遍历序列为ABCDEFGHI。二叉树的先序遍历、中序遍历和后序遍历是最常用的3种遍历方式,除此之外,有时也采用层次遍历。所谓二叉树的层次遍历,是指从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。例如,图5.14所示的二叉树的层次遍历序列为ABCDEFGHI。

由层次遍历的定义可知,在进行层次遍历时,对一层结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问,这样一层一层进行,先遇到的结点先访问,这与队列的操作原则比较吻合。因此,在进行层次遍历时,可设置一个队列结构,遍历从二叉树的根结点开始,首先将根结点指针入队列,然后从队头取出一个元素,每取一个元素,执行下面两个操作:

1访问该元素所指结点;

2若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子指针和右孩子指针顺序入队。

此过程不断进行,当队列为空时,二叉树的层次遍历结束。

slide48
【算法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)

/*将队首结点的左孩子结点入队列*/

slide49
{ rear++;

queue[rear]=queue[front]→lchild;

}

if (queue[front]→rchildNULL) /*将队首结点的右孩子结点入队列*/

{

rear++;

queue[rear]=queue[front]→rchild;

}

}

}

slide50
(二)二叉树遍历的递归实现

二叉树的先序、中序和后序遍历过程由以下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); }

}

slide51
3后序遍历的递归算法

【算法5.10】

void PostOrder(BiTree bt) /*后序遍历的递归算法*/

{ if (btNULL)

{ PostOrder(bt→lchild);

PostOrder(bt→rchild);

printf("%c",bt→data); /*访问根结点*/ }

}

二、二叉树遍历的非递归实现

下面以二叉链表作为二叉树的存储结构,来讨论二叉树遍历的非递归实现算法。

(一)先序遍历非递归算法

slide52
【算法5.11】

void NRPreOrder(BiTree bt)

{ /*非递归先序遍历二叉树*/

BiTree stack[MAXNODE],p;

int top;

if (bt==NULL)

return ;

top=0;

p=bt;

while ((p==NULL && top==0))

{ while (pNULL)

{

Visit(p→data); /*访问结点的数据域*/

if (top<MAXNODE-1) / *将当前指针p压栈*/

{ stack[top]=p;

top++; }

else

{ printf("栈溢出!");

return; }

slide53
p=p→lchild; /*指针指向p的左孩子结点*/ }

if (top<=0)

return; /*栈空时返回*/

Else

{ top--;

p=stack[top]; /*从栈中弹出栈顶元素*/

p=p→rchild; /*指针指向p的右孩子结点*/ }

}

}

slide54
(二)中序遍历非递归算法

【算法5.12】

void NRInOrder(BiTree bt)

{ /*非递归中序遍历二叉树*/

BiTree stack[MAXNODE],p;

int top;

if (bt==NULL)

return ;

top=0;

p=bt;

while ((p==NULL && top==0))

{ while (pNULL)

{ if (top<MAXNODE-1) /*将当前指针p压栈*/

{ stack[top]=p;

top++; }

else

{ printf("栈溢出!");

return; }

p=p→lchild; /*指针指向p的左孩子结点*/ }

slide55
if (top<=0)

return; /*栈空时返回*/

else

{ top--;

p=stack[top]; /*从栈中弹出栈顶元素*/

Visit(p→data); /*访问结点的数据域*/

p=p→rchild; /*指针指向p的右孩子结点*/ }

}

}

(三)后序遍历非递归算法

由前面的讨论可知,后序遍历与先序遍历和中序遍历不同,在后序遍历过程中,结点在第一次出栈后,还需再次入栈。也就是说,结点要入两次栈,出两次栈,而访问结点是在第二次出栈时访问。因此,为了区别同一个结点指针的两次出栈,设置一标志flag。当flag=1时,表示第一次出栈,结点不能访问;当flag=2时,表示第二次出栈,结点可以访问。

slide56
当结点指针进、出栈时,其标志flag也同时进、出栈。因此,可将栈中元素的数据类型定义为指针和标志flag合并的结构体类型。定义如下:当结点指针进、出栈时,其标志flag也同时进、出栈。因此,可将栈中元素的数据类型定义为指针和标志flag合并的结构体类型。定义如下:

typeset struct{

BiTree link;

int flag;

} stacktype;

【算法5.13】

void NRPostOrder(BiTree bt)

{ /*非递归后序遍历二叉树*/

stacktype stack[MAXNODE];

BiTree p;

int top,sign;

slide57
if (bt==NULL)

return;

top= -1; /*栈顶位置初始化*/

p=bt;

while ((p==NULL && top=-1))

{ if (pNULL) /*结点第一次进栈*/

{ top++;

stack[top].link=p;

stack[top].falg=1;

p=p→lchild; }

else

{ p=stack[top].link;

sign=stack[top].flag;

top--;

slide58
if (sign==1) /*结点第二次进栈*/

{ top++;

stack[top].link=p;

stack[top].flag=2;

p=p→rchild;}

else

{ Visit(p→data); /*访问该结点数据域值*/

p=NULL; }

}

}

}

其中,一维数组stack[MAXNODE]用于实现栈的结构,指针变量p指向当前要处理的结点,整型变量top为栈顶指针,用来表示当前栈顶的位置,整型变量sign为结点p的标志量。

slide59
三、由遍历序列恢复二叉树

同一棵二叉树具有唯一先序序列、中序序列和后序序列,但不同的二叉树可能具有相同的先序序列、中序序列或后序序列,如图5.15所示的5棵二叉树,它们的先序序列都是ABC。因此,仅由一个先序序列(或中序序列,或后序序列),无法唯一确定这棵二叉树。但是,如果同时知道一棵二叉树的先序序列和中序序列,或同时知道中序序列和后序序列,就能唯一确定这棵二叉树。

图5.15 先序序列为ABC的5棵二叉树

slide60
定理1:任何n(n≥0)个不同结点的二叉树,都可由它的先序序列和中序序列唯一地确定。定理1:任何n(n≥0)个不同结点的二叉树,都可由它的先序序列和中序序列唯一地确定。

证明:根据定义,先序遍历先访问根结点,第一个元素必定是根结点,而根结点又将中序遍历序列分为左、右子树两部分,其中左子树集合部分又可从先序遍历序列中找到其根结点。而这个子树的根结点又将其子树的中序遍历序列分为左、右两部分,如此递归下去,当取尽先序序列中的结点时,便可以唯一地得到一棵二叉树。

例如,已知一棵二叉树的先序序列为ABDGCEF,中序序列为DGBAECF。在先序序列中最左边的结点A为根结点,对应的中序序列中A的左边为DGB,而DGB在整个先序序列中的最左结点为B,中序序列中A的右边为ECF,而ECF在整个先序序列中的最左结点为C,则A的左子树中序序列为DGB,左子树的根结点为B,A的右子树中序序列为ECF,右子树的根结点为C。再对左、右子树进行相同的分解,其构造二叉树的过程如图5.16所示,对应的二叉树如图5.17所示。

slide61

图5.16 由先序序列和中序序列构造二叉树的过程

slide62
定理2:任何n(n≥0)个不同结点的二叉树,都可由它的中序序列和后序序列唯一地确定。定理2:任何n(n≥0)个不同结点的二叉树,都可由它的中序序列和后序序列唯一地确定。

图5.17 二叉树

slide63
证明:根据定义,后序遍历序列的最后一个结点必为二叉树的根结点,而根结点又将中序遍历序列分为左、右子树两部分,其中左子树集合部分又可以从后序遍历序列中找到其根结点。而这个子树的根结点又将其子树的中序遍历序列分为左、右子树两部分,如此递归下去,当取尽后序序列中的结点时,便可以唯一地得到一棵二叉树。证明:根据定义,后序遍历序列的最后一个结点必为二叉树的根结点,而根结点又将中序遍历序列分为左、右子树两部分,其中左子树集合部分又可以从后序遍历序列中找到其根结点。而这个子树的根结点又将其子树的中序遍历序列分为左、右子树两部分,如此递归下去,当取尽后序序列中的结点时,便可以唯一地得到一棵二叉树。

例如,已知一棵二叉树的中序序列为DGBAECF,后序序列为GDBEFCA。在后序序列中最后结点A为根结点,对应的中序序列中A的左边为DGB,而DGB在整个后序序列中的最右结点为B,中序序列中A的右边为ECF,而ECF在整个后序序列中的最右结点为C,则A的左子树中序序列为DGB,左子树的根结点为B,A的右子树中序序列为ECF,右子树的根结点为C。再对左右子树进行相同的分解。其构造二叉树的过程如图5.18所示,对应的二叉树如图5.17所示。

slide64

图5.18 由中序序列和后序序列构造二叉树的过程

slide65
第五节 线索二叉树

由上节可知,遍历二叉树时,按一定的规则访问结点可得到一个线性序列。在这些线性序列中,每个结点(除第一个和最后一个外)仅有一个直接前驱和一个直接后继,因此,遍历二叉树实质上是对一个非线性结构的线性化操作。有时为了操作方便,需要知道树中结点按某种次序遍历时的前驱和后继结点,但它们只能在遍历的动态过程中得到。那么能否在二叉链表中保存这种信息呢?

一、线索二叉树的定义及结构

当用二叉链表作为二叉树的存储结构时,因为每个结点中只有指向其左、右孩子结点的指针域,所以从任一结点出发只能直接找到该结点的左、右孩子,而一般情况下无法直接找到该结点在某种遍历序列中的直接前驱结点和直接后继结点。为此,若在每个结点中增加两个指针域来存储遍历时得到的直接前驱和直接后继信息,将大大降低存储空间的利用率。但是在n个结点的二叉链表中含有n+1个空指针域,因此可以利用这些空指针域存放指向结点在某种遍历次序下的直接前驱和直接后继结点的指针,这种附加的指针称为“线索”,加上了线索之后的二叉链表称为线索链表,相应的二叉树称为线索二叉树。

slide66
由于遍历方式不同,产生的遍历线性序列也不同,因此应作如下规定:当某结点的左指针为空时,令该指针指向按某种方式遍历二叉树时得到的该结点的前驱结点;当某结点的右指针为空时,令该指针指向按某种方式遍历二叉树时得到的该结点的后继结点。但如何区分左指针指向的结点到底是左孩子结点还是前驱结点,右指针指向的结点到底是右孩子结点还是后继结点呢?为此,在结点的存储结构上增加两个标志位来区分这两种情况:由于遍历方式不同,产生的遍历线性序列也不同,因此应作如下规定:当某结点的左指针为空时,令该指针指向按某种方式遍历二叉树时得到的该结点的前驱结点;当某结点的右指针为空时,令该指针指向按某种方式遍历二叉树时得到的该结点的后继结点。但如何区分左指针指向的结点到底是左孩子结点还是前驱结点,右指针指向的结点到底是右孩子结点还是后继结点呢?为此,在结点的存储结构上增加两个标志位来区分这两种情况:
slide67
线索链表的结点结构为:

在线索二叉树中,结点的结构可以定义如下:

typedef char datatype;

typedef struct BiThrNode

{ datatype data;

struct BiThrNode *lchild,*rchild;

unsigned ltag,rtag;

}

BiThrNodeType,*BiThrTree;

图5.19(a)所示的二叉树的中序线索二叉树见图5.19(b),其中序线索链表见图5.19(c)。图中的实线表示指针,虚线表示线索。结点C的左线索为空,表示结点C是中序序列的开始结点,它没有前驱;结点E的右线索为空,表示E是中序序列的终端结点,它没有后继。因此,在线索二叉树中,一个结点是叶子结点的充要条件为:它的左、右标志均是1。

slide68

二、线索二叉树的基本操作实现

建立了线索链表之后,我们来讨论线索二叉树上的运算。下面介绍线索二叉树上几种常用的运算。

(a) 二叉树

slide70

(c) 中序线索链表

图5.19 二叉树及其中序线索二叉树和中序线索链表

slide71
(一)建立一棵中序线索二叉树

建立线索二叉树实质上就是遍历一棵二叉树,在遍历的过程中,访问结点的操作就是检查当前结点的左、右指针域是否为空。若为空,将它们改为指向前驱结点或后继结点的线索。

【算法5.14】 建立一棵中序线索二叉树。

BiThrTree pre;

int InOrderThr(BiThrTree &head, BiThrTree T)

{ /*中序遍历二叉树T,并将其中序线索化,&head指向头结点,pre为全局变量*/

if ((&head=( BiThrNodeType*)malloc(sizeof(BiThrNodeType))))

return 0;

head→ltag=0;

head→rtag=1; /*建立头结点*/

head→rchild=*head;

if (T)

head→lchild=*head;

slide72
else

{ head→lchild=T;

pre=head;

InThreading(T); /*中序遍历进行中序线索化*/

pre→rchild=head;

pre→ltag=1; /*最后一个结点线索化*/

head→rchild=pre;} }

void InTreading(BiThrTree p)

{ /*中序遍历进行中序线索化*/

if (p)

{ InTreading(p→lchild); / *左子树线索化*/

if (p→lchild) /*前驱线索*/

{ p→ltag=1;

p→lchild=pre; }

if (pre→rchild) /*后继线索*/

{ pre→rtag=1;

pre→rchild=p; }

pre=p;

InTreading(p→rchild); /*右子树线索化*/

}

}

slide73
(二)在中序线索二叉树上查找任意结点的中序前驱结点(二)在中序线索二叉树上查找任意结点的中序前驱结点

对于中序线索二叉树上的任一结点,寻找其中序的前驱结点,有以下两种情况:

1如果该结点的左标志为1,那么其左指针域所指向的结点便是它的前驱结点。

2如果该结点的左标志为0,表明该结点有左孩子,根据中序遍历的定义,它的前驱结点是以该结点的左孩子为根结点的子树的最右结点,即沿着其左子树的右指针链向下查找,当某结点的右标志为1时,它就是所要找的前驱结点。

【算法5.15】

BiThrTree InPreNode(BiThrTree p)

{ BiThrTree pre;

pre=p→lchild;

if (p→ltag1)

while (pre→rtag==0)

pre=pre→rchild;

return pre;

}

slide74
(三)在中序线索二叉树上查找任意结点的中序后继结点(三)在中序线索二叉树上查找任意结点的中序后继结点

对于中序线索二叉树上的任一结点,寻找其中序的后继结点,有以下两种情况:

1如果该结点的左标志为1,那么其右指针域所指向的结点便是它的后继结点。

2如果该结点的右标志为0,表明该结点有右孩子,根据中序遍历的定义,它的后继结点是以该结点的右孩子为根结点的子树的最左结点,即沿着其右子树的左指针链向下查找,当某结点的右标志为1时,它就是所要找的后继结点。

【算法5.16】

BiThrTree InPostNode(BiThrTree p)

{ BiThrTree post;

post=p→rchild;

if (p→rtag1)

while (post→ltag==0)

post=post→lchild;

return post;

}

slide75
(四)在中序线索二叉树中查找值为x的结点

在中序线索二叉树中查找值为x的结点就是在线索二叉树上进行遍历,将访问结点的操作具体写成用当前结点的值与x比较的语句。

【算法5.17】

BiThrTree Search(BiThrTree head,elemtype x)

{ BiThrTree p;

p=head→lchild;

while (p→ltag==0 && phead)

p=p→lchild;/*查找左下角结点*/

while (phead && p→datax)

p=InPostNode(p); /*查找结点p的中序后继结点的值域为x*/

if (p==head)

{ printf("没有找到该数!\n");

return 0; }

else

return p;

}

若对一棵二叉树要经常遍历,或查找结点在指定次序下的前驱和后继,则应采用线索链表作为存储结构为宜。

slide76
第六节 二叉树的应用

一、二叉树遍历的应用

二叉树的遍历操作是二叉树的基本操作,我们可以利用二叉树的遍历操作来查找数据元素及建立二叉树(二叉链表)等操作。

(一)查找数据元素

Search(bt,x)在以bt为根结点指针的二叉树中查找数据元素x。查找成功时返回该结点的指针;查找失败时返回空指针。

【算法5.18】 查找数据元素。

BiTree Search(BiTree bt,datatype x)

{ /*在bt为根结点指针的二叉树中查找数据元素x*/

BiTree p;

if (bt)

slide77
{

if (bt→data==x) /*查找成功返回该结点的指针*/

return bt;

if (bt→lchildNULL)

{ /*在左子树中查找数据元素x*/

p=Search(bt→lchild,x);

if (p)

return p;

}

else if (bt→rchildNULL)

{ /*在右子树中查找数据元素x*/

p=Search(bt→rchild,x);

if (p)

return p;

}

}

return NULL; /*查找失败返回空指针*/

}

slide78
(二)建立二叉树

例如,输入一个二叉树的先序序列,构造这棵二叉树。为了保证唯一地构造出所希望的二叉树,在键入这棵树的先序序列时,需要在所有空二叉树的位置上填补一个特殊的字符,比如,空格。在算法中,需要对每个输入的字符进行判断,如果对应的字符是空格,则在相应的位置上构造一棵空二叉树;否则,创建一个新结点。整个算法结构以先序遍历递归算法为基础,二叉树中结点之间的指针连接是通过指针参数在递归调用返回时完成的。

【算法5.19】 建立二叉树。

BiTree Pre_Create_BT( )

{ BiTree bt;

getchar(ch);

if (ch==' ') return NULL; /*构造空树*/

else { bt=(BiTree)malloc(sizeof(BiTNode)); /*构造新结点*/

bt→data=ch;

bt→lchild =Pre_Create_BT( ); /*构造左子树*/

bt→rchild =Pre_Create_BT( ); /*构造右子树*/

return bt; }

}

slide79
二、最优二叉树(哈夫曼树)

一、最优二叉树概述

最优二叉树又称哈夫曼树,是一种带权路径长度最短的树,有着广泛的应用。

在许多应用中,常常将树中结点赋予一个有某种意义的实数,称为该结点的权。结点的带权路径长度是指该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度定义为树中所有叶子结点的带权路径长度之和,通常记为:

其中:n表示叶子结点的数目,wi和li分别表示叶子结点ki的权值和根到结点ki之间的路径长度。

在n个带权叶子结点构成的所有二叉树中,带权路径长度WPL最小的二叉树称为哈夫曼树(或最优二叉树)。因为构造这种树的算法最早由哈夫曼于1952年提出,所以称为哈夫曼树(Huffman)。

slide80
例如:给定4个叶子结点a,b,c和d,设其权值分别为6,4,1和3。我们构造如图5.20所示的三棵二叉树(还有许多棵),它们的带权路径长度分别为:例如:给定4个叶子结点a,b,c和d,设其权值分别为6,4,1和3。我们构造如图5.20所示的三棵二叉树(还有许多棵),它们的带权路径长度分别为:

(a) WPL=6×2+4×2+1×2+3×2=28

(b) WPL=6×3+4×3+1×1+3×2=37

(c) WPL=6×1+4×2+1×3+3×3=26

其中(c)树的WPL最小,可以验证,它就是哈夫曼树。

(a)

slide81

(c)

(b)

图5.20 具有不同WPL的二叉树(结点旁的数字为权值)

slide82
(二)哈夫曼树的构造算法

如果叶子上的权值均相同,完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树。对于给定的叶子数目及其权值,如何来构造一棵最优二叉树呢?哈夫曼首先给出了构造最优二叉树的方法,我们称其为哈夫曼算法,其基本思想为:

1根据给定的n个权值(w1,w2,…,wn),对应结点构成n棵二叉树的森林T={T1,T2,…,Tn},其中每棵二叉树Ti(1≤i≤n)中都只有一个带权值为wi的根结点,其左、右子树均为空。

2在森林T中选取两棵根结点权值最小的二叉树(规定把根结点的权值较小的那棵二叉树作为左子树,把根结点的权值较大的那棵二叉树作为右子树)分别作为左、右子树,构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。

3在森林T中,用新得到的二叉树代替这两棵树。

4重复2和3,直到T只含一棵树为止。这棵树便是哈夫曼树。

slide83
例如,假定仍采用上例中给定的权值w=(1,3,4,6)来构造一棵哈夫曼树。按照上述算法,图5.21就给出了一棵哈夫曼树的构造过程,其中图5.21(d)就是最后生成的哈夫曼树,它的带权路径长度为26。例如,假定仍采用上例中给定的权值w=(1,3,4,6)来构造一棵哈夫曼树。按照上述算法,图5.21就给出了一棵哈夫曼树的构造过程,其中图5.21(d)就是最后生成的哈夫曼树,它的带权路径长度为26。

(a)初始森林

(b)一次合并后的森林

slide84

(c)二次合并后的森林

(d)三次合并后的森林

图5.21 构造哈夫曼树的过程

slide85
由此可知,具有n个叶子结点的哈夫曼树中共有2n-1个结点,其中n个叶子结点是初始森林中的n个孤立结点,哈夫曼树中没有度数为1的分支结点,这类树常称为严格的二叉树。由此可知,具有n个叶子结点的哈夫曼树中共有2n-1个结点,其中n个叶子结点是初始森林中的n个孤立结点,哈夫曼树中没有度数为1的分支结点,这类树常称为严格的二叉树。

(三)哈夫曼编码

哈夫曼树的应用十分广泛。在不同的应用中,赋予叶子结点的权值可以有不同的解释:当哈夫曼树应用到信息编码中时,权值可以看成是某个符号出现的频率;当应用到判定过程中时,权值可以看成是某一类数据出现的频率;当应用到排序问题时,可以看成是已排好次序而待合并的序列的长度。

应用哈夫曼树较多的是通讯及数据传送中的二进制编码,即哈夫曼编码。目前,用电报传送的字符常常被转换成二进制代码传送。如何使传送的电文编码总长度最短?利用哈夫曼编码便能达到这一目的。

slide86
具体构造方法如下:设需要编码的字符集合为{d1, d2,…, dn},各个字符在电文中出现的次数集合为{w1, w2,…,wn},以d1,d2,…,dn作为叶子结点,以w1, w2, …,wn作为各叶子结点的权值构造一棵哈夫曼树,规 定哈夫曼树中的左分支为0,右分支为1,则从根结点到每个叶子结点所经过的分支对应的0和1组成的序列便为该结点对应字符的编码,这样的编码称为哈夫曼编码。哈夫曼编码的实质是:使用频率越高的字符采用越短的编码,从而达到总的电文编码长度变短的目的。

例:假定用于通信的电文仅由a,b,c,d,e,f,g和h等8个字母组成,字母在电文中出现的频率分别为0.07,0.19,0.02,0.06,0.32,0.03,0.21,0.10。为这些字母设计哈夫曼编码。

slide87
解:构造哈夫曼树的过程如下:

1选择频率最低的c和f构成一棵二叉树,其根结点的频率为0.05,记为结点n1。

2选择频率低的n1和d构造一棵二叉树,其根结点的频率为0.11,记为结点n2。

3选择频率低的a和h构造一棵二叉树,其根结点的频率为0.17,记为结点n3。

4选择频率低的n2和n3构造一棵二叉树,其根结点的频率为0.28,记为结点n4。 5选择频率低的b和g构造一棵二叉树,其根结点的频率为0.4,记为结点n5。

6选择频率低的n4和e构造一棵二叉树,其根结点的频率为0.6,记为结点n6。

7选择频率低的n5和n6构造一棵二叉树,其根结点的频率为1.0,记为结点n7。

最后的哈夫曼树如图5.22所示(树中叶子结点用圆或椭圆表示,分支结点用矩形表示,其中的数字表示结点的频率),给所有的左分支加上0,给所有的右分支加上1,从而得到各字母的哈夫曼编码如下:

slide88
a:1010 b:00 c:10000 d:1001

e:11 f:10001 g:01 h:1011

图5.22 哈夫曼树

slide89
从实例可以看到,电文中出现次数越多的字符编码越短,这样就保证了整个电文的编码长度达到最短。另外,在对不等长编码(哈夫曼编码属于不等长编码)的字符进行识别时,必须保证任意一个字符的编码不能是另一字符编码的前缀,哈夫曼编码正好具有这一特点,因为在哈夫曼树中,代表每个字符的结点都是叶子结点,它不可能在从根结点到另一字符的路径上,当然它的编码也不可能是另外一个字符编码的前缀。从实例可以看到,电文中出现次数越多的字符编码越短,这样就保证了整个电文的编码长度达到最短。另外,在对不等长编码(哈夫曼编码属于不等长编码)的字符进行识别时,必须保证任意一个字符的编码不能是另一字符编码的前缀,哈夫曼编码正好具有这一特点,因为在哈夫曼树中,代表每个字符的结点都是叶子结点,它不可能在从根结点到另一字符的路径上,当然它的编码也不可能是另外一个字符编码的前缀。
slide90
第七节 树、森林与二叉树的转换

从树的孩子兄弟表示法可看到,树可以用二叉链表作为存储结构。实际上,当树用二叉链表表示时,树的结构已经转换成一棵二叉树。也就是说,以二叉链表作为媒介可以导出树与二叉树之间的一个对应关系,即给定任何一棵树都可以找到唯一的一棵二叉树与之对应。

从以上分析中我们得出这样的结论:当一棵树用二叉链表表示时,可以用唯一对应的一棵二叉树来表示,研究树的问题转化成了研究二叉树的问题。这既体现了研究二叉树的重要性,同时也为二叉树与树和森林的相互转换提供了依据。

一、树转换为二叉树

树中每个结点可能有多个孩子,但二叉树中每个结点最多只能有两个孩子。要把树转换为二叉树,就必须找到一种结点与结点之间至多有两个量说明的关系。这种关系是树中每个结点最多只有一个最左边的孩子(长子)和一个右邻的兄弟,这就是我们要找的关系。按照这种关系很自然地就能将树转换成相应的二叉树,其方法是:

slide91
1加线。在各兄弟结点之间用虚线相连。

2抹线。对每个分支结点,除了保留与其长子的连线外,去掉该结点与其他孩子的连线。

3旋转。把虚线改为实线,从水平方向向下顺时针旋转45°,成右斜下方向。原树中实线成左斜下方向。这样就形成一棵二叉树。

使用上述变换法,图5.23(a)所示的树就可转换为图5.23(b)所示的形式,它已是一棵二叉树。若按顺时针方向将它旋转约45°,就更清楚地变为图5.23(c)所示的二叉树。由于树根没有兄弟,故树转化为二叉树后,二叉树的根结点的右子树必为空。

(a)树

slide92

(b)在兄弟结点间加一虚线,去掉除最左孩子外的其他孩子连线(b)在兄弟结点间加一虚线,去掉除最左孩子外的其他孩子连线

slide93

(c)顺时针旋转45°后的二叉树

图5.23 树转换为二叉树

slide94
二、森林转换为二叉树

将一个森林转换为二叉树的方法是:先将森林中的每棵树转换为二叉树(不旋转45°),因为转换后的这些二叉树的根结点的右子树均为空,故可将各二叉树的根结点视为兄弟从左至右连在一起,旋转45°就形成了一棵二叉树。

将图5.24(a)所示的森林转换为二叉树的过程如图5.24所示。

(a)森林

slide96

(c)二叉树

图5.24 森林转换为二叉树

slide97
三、二叉树转换为森林

与将树和森林转换为二叉树相同,也有一种自然的方式把二叉树转换为树和森林,其方法是:若结点x是其双亲y的左孩子,则把x的右孩子,右孩子的右孩子,……都与y用连线连起来,最后去掉所有双亲到右孩子的连线。图5.25(b)就是用这种方法将图5.25(a)的二叉树处理后得到的森林。

(a) 二叉树

slide98

(b) 森林

图5.25 二叉树转换成森林

slide99
四、树和森林的遍历

与二叉树的遍历一样,对树和森林中结点的遍历也是一种重要的操作。由于森林中的树可以有两棵以上,因此不便讨论它们的中序遍历。下面给出的是常用的、实现起来也很方便的森林(包括树)的三种遍历方法。

(一)先序遍历

若森林非空,则

1访问森林中第一棵树的根结点;

2先序遍历第一棵树根结点的各子树森林;

3先序遍历森林中除第一棵树外剩余的树构成的森林。

(二)后序遍历

若森林非空,则

1后序遍历第一棵树的根结点的各子树森林;

2访问森林中第一棵树的根结点;

3后序遍历森林中除第一棵树外剩余的树构成的森林。

slide100
(三)按层次遍历

若森林非空,则

1对第一棵树从根结点起按层从左到右依次访问各结点;

2按层访问森林中除第一棵树外剩余的树构成的森林。

对图5.26所示的树来说,先序遍历序列结果是:RADEBCFGKP;后序遍历序列结果是:DEABGKPFCR;按层次遍历序列结果是:RABCDEFGKP。

图5.26 树

slide101
对图5.27所示的森林来说,先序遍历序列结果是:ABCDEFGHIJ;后序遍历序列结果是:BCDAFEHJIG,按层次遍历序列结果是:ABCDEFGHIJ。对图5.27所示的森林来说,先序遍历序列结果是:ABCDEFGHIJ;后序遍历序列结果是:BCDAFEHJIG,按层次遍历序列结果是:ABCDEFGHIJ。

图5.27 森林

slide102
本章小结

本章介绍的树型结构是一种非常重要的非线性结构,其中以树和二叉树最为常用。树型结构是结点之间有分支,并具有层次关系的结构,它非常类似于自然界中的树。树型结构在客观世界中是大量存在的,它可以用来描述客观世界中广泛存在的、呈现层次结构的关系,例如,家族族谱以及各种社会组织机构的领导与被领导关系都可以用树型结构来形象地表示。树在计算机领域中也有着广泛的应用,例如,操作系统中对文件的管理以及网络系统中的域名管理都采用了树型结构;在编译程序中,用树来表示源程序的语法结构;在数据库系统中,可以用树来组织信息。

要求掌握树和二叉树的概念,二叉树的性质,存储结构以及基本运算,并可以用二叉树解决一些综合应用问题。

slide103
实 训 一

一、实训目的

1.掌握二叉树的存储结构。

2.掌握二叉树的遍历方法及递归实现算法。

3.掌握二叉树的基本运算,尤其是二叉树的建立算法。

二、实训内容

1.问题描述:

将二叉树按照二叉链表方式进行存储,编写算法,先建立一棵二叉树,再进行中序遍历,最

后将二叉树左右子树进行交换,并输出交换后的二叉树的中序遍历序列。

2.算法分析:

设二叉树的根指针为t,且以二叉链表表示。利用递归算法实现所有结点的左右子树交换。

slide104
3.算法提示:

#define NULL 0

typedef struct node

{ char data;

struct node *lchild,*rchild;

} bitree;

bitree *creat(); /*建立二叉树*/

t→lchild=creat(); /*构造左子树*/

t→rchild=creat(); /*构造右子树*/

inorder(bitree *t) /*中序遍历二叉树*/

exchange(bitree *t) /*对二叉树t中所有结点的左右子树进行交换*/

4.运行结果:

以图5.28所示的二叉树为例,在运行该程序时,输入数据:

ABC□□D□E□□F□□

其中,符号“□”表示一个空格字符。

则输出数据为:

C B D E A F /*建立一棵二叉树的中序遍历序列*/

F A E D B C /*对二叉树所有结点的左右子树交换后的中序遍历序列*/

slide105

图5.29所示的是对二叉树中所有结点的左右子树进行图5.29所示的是对二叉树中所有结点的左右子树进行

交换后形成的二叉树。

图5.29 左右子树交换后的二叉树

图5.28 二叉树

slide106
实 训 二

一、实训目的

1.掌握最优二叉树(哈夫曼树)的概念,哈夫曼树的构造方法。

2.了解哈夫曼编码的算法实现。

二、实训内容

1.问题描述:

构造哈夫曼树及求出哈夫曼编码。

2.算法分析:

以链式存储结构作为二叉树的存储方式,采用指针来描述哈夫曼树。

3.算法提示:

#include <string.h>

#include <alloc.h>

#define MAXNUM 1000

slide107
typedef struct

{ /*哈夫曼树结点类型定义*/

int weight;/*权值采用整数便于操作*/

int parent;/*双亲结点*/

int lchild,rchild;

}Htnode;

void select(Htnode Ht[],int m,int *L,int *R)

/*在前m个结点中选择两个具有最小权值的结点,并由L和R返回其在数组Ht中的位置*/

void createHuftree(Htnode Ht[],int w[],int n)

/*构造哈夫曼树,Ht为存储哈夫曼树的数组,n为权值个数,w为权值数组(长度为n)*/

void coding(Htnode Ht[],int n,char *p[])

/*由叶子至根求各叶子结点的编码,并保存在指针数组p中*/

/*Ht为已构造好的哈夫曼树,n为树中叶子结点的个数*/

/*p为字符指针数组,p[i]指向第i个叶子结点的编码*/

Htnode *Ht;/*指向哈夫曼树存储空间*/

int i,n,*w;/*n为待编码字符个数*/

char **code;/*保存字符编码的二级指针*/

Ht=(Htnode *)malloc((2*n-1)*sizeof(Htnode));/*哈夫曼树存储空间*/

slide108
4.运行结果:

假设字符的个数为8,权值分别为0.07,0.19,0.02,0.06,0.32,0.03,0.21和0.10。按

如下方式输入数据:

请输入字符个数:8

请输入权值:

7

19

2

6

32

3

21

10

则可能的输出结果为:

slide109
哈夫曼编码为:

7:=1010

19:=00

2:=10000

6:=1001

32:=11

3:=10001

21:=01

10:=1011

slide110
思考与习题

一、填空题

1.一棵深度为k的满二叉树的结点总数为___,一棵深度为k的完全二叉树的结点总数的最小值为___,最大值为___。

2.对于一个具有n个结点的二叉树,当它存储在二叉链表中时,其指针的总数为____个,其中____个用于链接孩子结点,____个空闲。

3.对于一个具有n个结点的二叉树,当它为一棵___二叉树时,具有最小高度,即为____,当它为一棵单支树时具有高度,即为____。

4.具有n个结点的完全二叉树,若按层次从上到下、从左到右对其编号(根结点为1号),则编号最大的分支结点序号为___,编号最小的分支结点序号为___,编号最大的叶子结点序号为____,编号最小的叶子结点序号为____。

slide111
5.具有n个结点的完全二叉树的深度为_____。

6.由三个结点构成的二叉树,共有____种不同的形态。

7.已知二叉树的先序遍历的序列为ABDCEFG,中序遍历序列为DBCAFEG,其后序遍历序列为_____。

8.哈夫曼树又叫____,路径上权值较小的结点与根结点的距离较_____(填“近”或“远”)。

二、选择题

1.树型结构最适合用来描述____。

A有序的数据元素 B无序的数据元素

C数据元素之间具有层次关系的数据

D数据元素之间没有关系的数据

2.“二叉树为空”意味着二叉树____。

A由一些未赋值的空结点组成 B根结点无子树

C不存在 D没有结点

slide112
3.树转换成二叉树后,以下结论正确的是。

A树的先根遍历序列与其对应的二叉树的先序遍历序列相同

B树的先根遍历序列与其对应的二叉树的中序遍历序列相同

C树的后根遍历序列与其对应的二叉树的后序遍历序列相同

D以上都不对

4.在一棵非空二叉树的中序遍历序列中,根结点的右边。

A只有右子树上的所有结点 B只有右子树上的部分结点

C只有左子树上的所有结点 D 只有左子树上的部分结点

5.任何一棵二叉树的叶结点在先序、中序和后序遍历序列中的相对次序 。

A不发生变化 B发生变化 C不能确定 D以上都不对

6.一棵非空二叉树的先序遍历序列和后序序列相反,则该二叉树一定满足。

A二叉树中任意一结点均无左孩子

B二叉树中任意一结点均无右孩子

C二叉树只有一个叶子结点 D是任意一棵二叉树

7.判断线索二叉树某结点P有右孩子的条件是。

APNULL BP→rchildNULL

CP→rtag==0 DP→rtag==1

slide113
8.在下面所示的4棵二叉树中,不是完全二叉树。

B

A

D

C

slide114
9.若二叉树有n个结点,则其深度为。

An-1 B n Clog2n+1 D无法确定

10.设深度为k的二叉树上只有度为0或度为2的结点,则这类二叉树上所含结点总数至少为。

Ak+1 B2k C2k-1 D2k+1

三、简答题

1.假定在树中,结点x是结点y的双亲时,用(x,y)来表示树边。已知一棵树边的集合为{(i,m),(i,n),(e,i),(b,e),(b,d),(a,b),(g,j),(g,k),(c,g),(c,f),(h,l),(c,h),(a,c)},用树形表示法画出此树,并回答下列问题:

(1) 哪个是根结点?哪些是叶结点?

(2) 哪个是g的双亲?哪些是g的祖先?哪些是g的孩子?

(3) 哪些是e的子孙?

(4) 哪些是e的兄弟?哪些是f的兄弟?

(5) 结点b和n的层次各是多少?

(6) 树的深度是多少?以结点e为根的子树的深度是多少?

slide115
2.一棵度为2的有序树与一棵二叉树有何区别?

3.试分别画出具有3个结点的树和3个结点的二叉树的所有不同形态。

4.高度为h的完全二叉树至少有多少个结点?至多有多少个结点?

5.分别写出图5.30所示的各二叉树的先序遍历、中序遍历和后序遍历序列。

(a)

(b)

图5.30 二叉树

slide116
6.假设二叉树包含的结点数据为1,3,7,12。

(1)画出两棵高度最大的二叉树。

(2)画出两棵完全二叉树,要求每个双亲结点的值大于其孩子结点的值。

7.若二叉树中各结点的值均不相同,则由二叉树的先序序列和中序序列,或由其后序序列和中序序列均能唯一地确定一棵二叉树,但由先序序列和后序序列却不一定能唯一地确定一棵二叉树。

(1)已知一棵二叉树的先序序列为ABDGHCEFI,中序序列为GDHBAECIF,请画出此二叉树。

(2)已知一棵二叉树的中序序列为BDCEAFHG,后序序列为DECBHGFA,请画出此二叉树。

(3)已知两棵二叉树的先序序列和后序序列均为AB和BA,请画出这两棵不同的二叉树。

8.画出图5.31所示的森林所对应的二叉树。

slide117

图5.31 森林

9.画出图5.32中的各二叉树所对应的森林。

(a)

图5.32 二棵二叉树

(b)

slide118
10.设给定权集w={2,3,7,8,9},构造关于w的一棵哈夫曼树,并求其加权路径长度WPL。10.设给定权集w={2,3,7,8,9},构造关于w的一棵哈夫曼树,并求其加权路径长度WPL。

11假设用于通信的电文由字符集{a,b,c,d,e,f,g,h}中的字母构成,这8个字母在电文中出现的频率分别为{0.02,0.09,0.04,0.08,0.31,0.03,0.25,0.11}。

(1)为这8个字母设计哈夫曼树。

(2)写出哈夫曼编码。

四、算法题

1.以二叉链表为存储结构,分别写出求二叉树结点总数及叶子总数的算法。

2.以二叉链表为存储结构,写出求二叉树高度的算法。

3.以二叉链表为存储结构,写出一个算法交换各结点的左右子树。

4.以二叉链表为存储结构,分别写出在二叉树中查找值为x的结点及求x结点在树中层数的算法。

5.一棵n个结点的完全二叉树以一维数组作为存储结构,试写一非递归算法实现对该树的先序遍历。

6.假设二叉树以二叉链表方式来存储,设计一算法,判断一棵二叉树是否为完全二叉树。