树
树
一、树的概念
树(tree)是树型结构的简称。它是一种重要的非线性数据结构。树——或者是一棵空树,即不含结点的树,或者是一棵非空树,即至少含有一个结点的树。在一棵非空树中,它有且仅有一称作根(root)的结点,其余的结点可分为m棵(m≥0)互不相交的子树(即称作根的子树),每棵子树(subtree)又同样是一棵树。显然,树的定义是递归的,树是一种递归的数据结构。树的递归定义,将为以后实现树的各种运算提供方便。
图(a)就是一棵树T,它由根结点A和两棵子树T1和T2(分别对应图(b)和图(c))所组成;T1又由它的根结点B和三棵子树T11、T12和T13(分别对应图(d)、(e)、(f))所组成;T11和T13只含有根结点,不含有子树(或者说子树为空树),即不可再分;T12又由它的根结点E和两棵只含有根结点的子树所组成,每棵子树的根结点分别为H和I;T2由它的根结点C和一棵子树所组成,该子树也只含有一个根结点G,不可再分。
在一棵树中,每个结点被定义为它的子结点的前驱,而它的每个子结点又是它的后继。由此,可以用二元组定义一棵树:
Tree=(K,R)
K=(ki|1≤i≤n,n≥0,n为树中的结点数,ki∈elemptype)
R={r}
当n>0(即树为非空树)时,关系r应满足下列条件:
(1)有且仅有一个结点没有前驱,该结点被称为树的根;
(2)除树根结点外,其余每个结点有且仅有一个前驱结点;
(3)包括树根结点在内的每个结点,可以有任意多个(含0个)后继结点。
对于上图(a)所示的树T,若采用二元组表示,则结点的集合K和K上的二元关系r分别为:
K={A,B,C,D,E,F,G,H,I}
R={<A,B>,<A,C>,<B,D>,<B,E>,<B,F>,<C,G>,<E,H>,<E,I>}
其中A结点无前驱结点,被称为树的根结点;其余每个结点有且仅有一个前驱结点;在所有结点中,B结点有三个后继结点,A结点和E结点分为有两个后继结点,C结点有一个后继结点,其余结点均没有后继结点。
在日常生活中,树结构广泛存在。
例1、可把一个家族看作为一棵树,树中的结点为家族成员的姓名及相关信息,树中的关系为父子关系,即父亲是儿子的前驱,儿子是父亲的后继。图2就是一棵家族树,王新贵有两个儿子王万和和王万田,王万和又有三个儿子王家利、王家中和王家国。
例2、可把一本书的结构看作为一棵树,树中的结点为书、章、节的名称及相关信息,树中的关系为包含关系。图3中一本书的结构,根结点为书的名称数学,它包含三章,每章名称分别为加法、减法和乘法,加法一章又包含两节,分别为一位加法和多位加法,减法和乘法也分别包含若干节。
例3、可把一个国家或一个地区的各级行政区划分看作为一棵树,树中的结点为行政区的名称及相关信息,树中的关系为上下级关系。如一个城市包含有若干个区,每个区又包含有若干个街道,每个街道又包含有若干个居委会等。
例4、可把一个算术表达式表示成一棵树,运算符作为根结点,它的前后两个运算对象分别作为根的左、右两棵子树。如把算术表达式a*b+(c-d/e)*f表示成树,则如图3所示。
二、树的表示
树的表示方法有多种。图1至图4中的树形表示法是其中的一种,也是最常用的一种。在这种表示法中,结点之间的关系是通过连线表示的,虽然每条连线上都不带有箭头(即方向),但它并不是无向的,而是有向的,其方向隐含为从上向下,即连线的上方结点是下方结点的前驱,下方结点是上方结点的后继。树的另一种表示法是二元组表示法。除这两种之外,通常还有三种:一是集合图表示,每棵树对应一个圆形,圆内包含根结点和子树,图1的树T对应的集合图表示如图5(a)所示;二是凹入表表示,每棵树的根对应着一个条形,子树的根对应着一个较短的条形,且树根在上,子树的根在下,树T的凹入表表示如图5(b)所示;三是广义表表示,每棵树的根作为由子树构成的表的名字而放在表的左边,树T的广义表表示如图5(c)所示。
三、树的基本术语
1、结点的度和树的度
每个结点具有的子树数或者说后继结点数被定义为该结点的度(degree)。所有结点的度的最大值被定义为该树的度。如图1的树T中,B结点的度为3,A、E结点的度都为2,C结点的度为1,其余结点的度均为0。因结点的最大度为3,所以树T的度为3。
2、分支结点和叶子结点
度大于0的结点称作分支结点或非终端结点,度等于0的结点称作叶子结点或终端结点。在分支结点中,又把度为1的结点叫做单分支结点,度为2的结点叫做双分支结点,其余依此类推。如在图1的树T中,A、B、C、E都是分支结点,D、H、I、F、G都是叶子结点;在分支结点中,C为单分支结点,A、E分别为双分支结点,B为三分支结点。
3、孩子结点、双亲结点和兄弟结点
每个结点的子树的根,或者说每个结点的后继,被习惯地称作该结点的孩子(child)或儿子,相应地,该结点被称作孩子结点的双亲(parent)或父亲。具有同一双亲的孩子互称兄弟(brothers)。每个结点的所有子树中的结点被称作该结点的子孙。每个结点的祖先则被定义为从树根结点到达该结点的路径上经过的所有结点。如在图1的树T中,B结点的孩子为D、E、F结点,双亲为A结点,D、E、F互为兄弟,B结点的子孙为D、E、H、I、F结点,I结点的祖先为A、B、E结点,对于树T中的其他结点亦可进行同样的分析。
由孩子结点和双亲结点的定义可知:在一棵树中,根结点没有双亲结点,叶子结点没有后继结点。如在图1的树T中,A结点没有双亲结点,D、H、I、F、G结点没有孩子结点。
4、结点的层数和树的深度
树既是一种递归结构,也是一种层次结构,树中的每个结点都处在一定的层数上。结点的层数(level)从树根开始定义,根结点为第一层,它的孩子结点为第二层,依次类推。树中结点的最大层数称为树的深度(depth)或高度(height)。如在图1的树T中,A结点处于第一层,B、C结点处于第二层,D、E、F、G结点处于第三层,H、I结点处于第四层。H、I结点所处的第四层为树T中结点的最大层数,所以树T的深度为4。
5、有序树和无序树
若树中各结点的子树是按照一定的次序从左向右安排的,则称之为有序树,否则称之为无序树。如对于图6中的两棵树,若看作为无序树,则是相同的;若看作为有序树,则不同,因为根结点A的两棵子树的次序不同。又如,对于一棵反映父子关系的家族树,兄弟结点之间是按照排行大小有序的,所以它是一棵有序树。再如,对于一个机关或单位的机构设置树,若各层机构是按照一定的次序排列的,则为一棵有序树,否则为一棵无序树。因为任何无序树都可以当作任一次序的有序树来处理,所以以后若不特别指明,均认为树是有序的。
6、森林
森林是m (m≥0)棵互不相交的树的集合。例如,对于树中每一个分支结点来说,其子树的集合就是森林。在图1的树T中,由A结点的子树所构成的森林为{T1,T2},由B结点的子树所构成的森林为{T11,T12,T13},等等。
7、树的存储结构
树的存储结构有多种,其中使用较多的是链式存储结构。由于树中结点可以有多个元素,所以可以用多重链表来描述。所谓多重链表是指每个结点由数据域和n(n为树的度)个指针域共n+1个域组成,其表示方法如下:
const size=树的度;
type
PNode=^TNode; {结点类型}
TNode=record
data:elementType; {数据域}
next:array[1..size] of PNode;{指向各儿子的指针域}
end;
var
root:PNode;
显然,取树的度作为每个结点的链域数(即指向儿子结点的指针数)虽使各种算法简化,但会造成存储空间的大量浪费。因为可能有很多结点存在空链域。容易验证,一棵具有n个结点且度为k的树中必存在n*(k-1)+1个空链域,因此需要寻找一种恰当的树形式即要使每个结点的结构相同、又要尽可能减少存储空间的浪费,方便运算。解决这个空间问题,我们引入二叉树这种特殊的树。
二叉树
一、 二叉树的定义
1、二叉树的非线性数据结构
二叉树(binary() tree)是指树的度不超过2的有序树。它是一种最简单、而且最重要的树。二叉树的递归定义为:二叉树或者是一棵空树,或者是一棵由一个根结点和两棵互不相交的分别称作根的左子树和右子树所组成的非空树,左子树和右子树又同样都是二叉树。
图7(a)就是一棵二叉树BT,它由根结点A和左子树BT1(对应图7(b))、右子树BT2(对应图7(c))所组成,BT1又由根结点B和左子树BT11(只含有根结点D)、右子树BT12(为空树)所组成;对于BT2树也可进行类似的分析。
在二叉树中,每个结点的左子树的根结点被称之为左孩子(left child),右子树的根结点被称之为右孩子(right child)。在图7的BT二叉树中,A结点的左孩子为B结点,右孩子为C结点;B结点的左孩子为D结点,右孩子为空,或者说没有右孩子;C结点的左孩子为E结点,右孩子为F结点;F结点没有左孩子,右孩子为G结点。
2、二叉树的特殊形态
满二叉树和完全二叉树是二叉树的两个特殊形态,其定义如下:
满二叉树:如果一棵二叉树的任何结点,或者是树叶,或者恰有两棵非空子树,则此二叉树称作满二叉树(图8(a))。可以验证具有n个叶子结点的满二叉树共有2n-1个结点。
完全二叉树:如果一棵二叉树最多只有最下面两层结点度数可以小于2,并且最下面一层的结点都集中在该层最左边的若干位置上,则称此二叉树为完全二叉树(如图8(b)和图9)
二、二叉树的性质
二叉树具有下列重要性质:
性质1:二叉树上第i层上至多有2i-1个结点(i≥1)。
下面分为i=1和i>1两种情况讨论:
当i=1时,2i-1=20=1,因为二叉树的第一层上只有一个结点(即根结点),故命题成立。
当i>1时,假定第i-1层上的结点数至多有2(i-1)-1=2i-2个,根据二叉树的定义,每个结点至多有两个孩子,所以第i层上的结点数至多为第i-1层上结点数的2倍,即2×2i-2=2i-1,命题成立。
性质2:深度为h的二叉树至多有2h-1个结点。
显然,当深度为h的二叉树上每一层都达到最多的结点数时,它们的和才能最大,即整个二叉树才具有最多结点数:
=20+21+22+L+2h-1=2h-1 等比数列通项公式an=a1*q(n-1) q=a(n+1)/an {n为自然数}求和公式sn=a1(1-qn)/(1-q) {前提是q不等于1} Sn=n*a1(q=1)
故命题成立。
在一棵二叉树,如果它的第i层的结点数达到了2i-1个,则称这棵树的第i层是满的。当二叉树中的每层都是满的时,就称此二叉树为满二叉树。由性质2的证明可知,深度为h的满二叉树的结点数为2h-1个,而且,深度为h的二叉树中只有满二叉树的结点数能达到2h-1。
性质3 在任何二叉树中,叶子结点数总比度为2的结点多1。
证明 设n0为二叉树的叶结点数,n1为二叉树中度为1的结点数;n2为二叉树中度为2的结点数,因此有:
n=n0+n1+n2。 ①
由于二叉树中除根结点外,其余每个结点都有且仅有一个分支进入,设B为进入分支的总数,因此有:n=B+1。 ②
又由于所有的进入分支是由度为1和度为2的结点射出的,因此又有:
B=n1+2*n2。 ③
将③代入②得:
n=n1+2*n2+1。 ④
比较①和④,得:
n0=n2+1。
即性质3也成立。
完全二叉树有下述重要性质(其中表示“不大于x的最大整数” x表示“不小于x的最小整数”):
性质4具有n(n>0)个结点的完全二叉树的深度为log2n+1=log2(n+1)。
证明:因为深度为h的二叉树的结点个数最多为2h-1个,又深度为h的完全二叉树的结点个数最少为2h-1个,设n个结点的完全二叉树深度为h,则:
2h-1≤n<2h
取对数,得:h-1≤log2n<h
因为h是整数,则不难得到:h=log2n+1
因为log2n+1=log2(n+1),所以h=log2(n+1)亦成立。
完全二叉树的另一个重要性质是对其结点的“按层编号”将得到很好的结果。所谓“按层编号”是指:将一棵树中的所有n个结点按从第一层到最大层、每层从左到右的顺序依次标记为1,2,…,n,则可以得到一个足以反映整个二叉树结构的线性序列。图8(b)中各结点旁边的正整数为该结点的编号。
完全二叉树中除最下面一层外,各层都被结点充满了,每一层结点个数恰是上一层结点的个数的2倍,因此,从一个结点的编号就可推知其双亲,左、右兄弟结点的编号。
性质5 如果将一棵有n个结点的完全二叉树按层编号,则对任一编号为i(0≤i≤n)的结点x有:
① 若i=1,则结点x是根,若i>1,则结点x的双亲的编号为i/2;
② 若2i>n,则结点x无左孩子(且无右孩子);否则x的左儿子L的编号为2i;
③ 若2i+1>n,则结点x无右儿子;否则,x的右孩子R的编号为2i+1。
④在n个结点的完全二叉树中,若n为奇数,则所有的分支结点都有左孩子和右孩子,若n为偶数,则编号为n/2的结点只有左孩子,没有右孩子,其他分支结点既有左孩子又有右孩子。
⑤在n个结点的完全二叉树中,对于标号为i的结点,若i≤n/2,则结点i为分支结点,否则为叶子结点。
完全二叉树一结点之间的父子关系可由它们编号之间的关系来表达。这一性质是二叉树顺序存储结构的基础。
在一棵二叉树中,若除最后一层外,其余层都是满的,则称此树为理想平衡树。显然,完全二叉树是特殊的理想平衡树。下图(a)是一棵理想平衡树,而图(b)不是,因为它的倒数第二层没有满。
三、树转换成二叉树
树转换成二叉树的方法是:将树中每个结点的第一个孩子结点转换为二叉树中对应结点的左孩子,将第二个孩子结点转换为第一个孩子结点的右孩子,将第三个孩子结点转换成第二个孩子结点的右孩子,依此类推。这样,二叉树中的左孩子,实际就是树中的第一个孩子,而二叉树中的右孩子,则对应树中的右兄弟;显然这个转换是可逆的。例如,图10(a)是一棵三叉树,转换成二叉树后如图10(b)所示。
四、森林转换成二叉树
森林转换成二叉树,首先要将森林转换成树。
森林转换成树的方法是:增加一个虚拟结点V,将森林中所有的树的根链接到虚拟结点上作为它的孩子结点。如图11(a)中的森林转换成树如图11(b)所示。
将森林转换成树后,再将树转换成对应的二叉树即可。
五、二叉树的存储结构
二叉树具有线性存储和链接存储两种存储结构。
1.线性存储
顺序存储一棵二叉树时,首先要将二叉树按照完全二叉树中对应的位置进行标号,然后,以每个结点的标号为下标,将对应的值存储到一个一维数组中。由完全二叉树标号的性质可知,根结点的标号为1,如果标号为i的结点有左孩子结点,那么它的左孩子结点标号为2i;如果标号为i的结点有右孩子,那么它的右孩子结点标号为2i+1。这样,就可以按层次从上到下的顺序给每一层标号。如图12就是一个标号的例子,结点里是结点的值,边上的值是结点的编号。
若将图12的两棵二叉树分别存放在数组TreeA和TreeB中,则两个数组如下:
数组下标 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
TreeA |
a |
b |
c |
d |
e |
f |
g |
|
h |
i |
j |
k |
TreeB |
1 |
3 |
6 |
7 |
6 |
15 |
|
|
9 |
12 |
12 |
18 |
注意:在数组中可能有一些空的元素,表示这个结点是空的。
在线性存储的二叉树中,各结点之间的关系可以通过下标表示出来,如下标为i的结点的父结点是下标为i/2的结点,左孩子是下标为2i的结点,而右孩子是下标为2i+1的结点。
当一棵树是完全二叉树时,用顺序存储是一种非常好的选择,它实现简单、不易出错,而且能充分利用空间。但是,对于一般的二叉树,特别是单分支结点比较多的,如果用顺序存储,必然造成空间的极大浪费。
2.链接存储
二叉树有另一种存储方式,那就是链接存储。当一棵二叉树用链接存储时,每个结点都要设置三个域:值域(data)、左指针(left)域和右指针(right)域。其中,值域用于存储结点的值,左指域用于存储一个指向它的左孩子的指针,右指针域用于存储一个指向它的右孩子的指针。通常一个结点表示如下:
如果在二叉树中希望能及时找到一个结点的父结点,那么通常在存储结构中加入一个parent指向它的父结点,即:
如图13中,(a)是一棵二叉树,(b)是用第一种链接存储,(c)是用第二种链接存储。
和单链表一样,二叉树的链接表既可以由静态结点链接而成,也可以由动态结点链接而成。它们的结点定义分别如下:
二叉树的静态结点定义:
Type
TNode=record
data:datatype
left,ritht:integer;
end;
使用静态结点必须使用一个数组来存储。在结点的定义中,left和right为左、右孩子结点在数组中所在的单元的下标,所以用整型。其数组类型的定义如下:
type
TTree=array[1..Size] of TNode;
二叉树的动态结点定义:
Type
PNode=^TNode;
TNode=record
data:elementType;
left,right:PNode;
end;
由于是链接存储,在一个二叉树中,结点的存放顺序可以是任意的,以静态链接为例,图13的树可以存储为:
下标 |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
… |
Size |
data |
d |
b |
i |
e |
a |
f |
c |
g |
… |
|
left |
0 |
1 |
0 |
3 |
2 |
0 |
6 |
0 |
… |
|
right |
0 |
4 |
0 |
0 |
8 |
0 |
8 |
0 |
… |
习题
一、单选题
1.树中所有的结点的度等于所有结点数加( )。
A.0 B. 1 C. -1 D. 2
2、在一棵二叉树的二叉链表中,空指针域数等于非空指针域数加( )。
A.2 B.1 C.0 D.-1
3、在一棵具有n个结点的二叉树中,所有结点的空子树个数等于( )。
A.n B. n-1 C.n+1 D.2n
4、在一棵具有n个结点的二叉树的第i层上,最多具有( )个结点。
A.2i B.2i+1 C.2i-1 D.2n
5、在一棵具有35个结点的完全二叉树中,该树的深度为( )。
A.6 B.7 C.5 D.8
6、在一棵具有n个结点的完全二叉树中,树枝结点的最大编号为( ).
A. (n+1)/2 B. (n+1)/2 C. n/2 D. n/2
7.在一棵完全二叉树中,若编号为i的结点存在左孩子,则左孩子结点的编号为( )。
A.2i B.2i-1 C.2i+1 D.2i+2
8、在一棵完全二叉树中,对于编号为i(i>1)的结点,其双亲结点的编号为( )
A. (i-1)/2 B. (i-1)/2 C. i/2 D. i/2
二、填空题
1、对于一棵具有n个结点的树,该树中所有结点的度数之和为( ).
2、在一棵深度为5的满二叉树中的结点数为( )个。
3、在一棵二叉树中,假定双分支结点数为5个,单分支结点数为6个,则叶子结点数为( )个。
4、对于一棵含有40个结点的理想平衡树,它的高度为( )。
5、若对一棵二叉树从0开始进行结点编号,并按此编号把它顺序存储到一维数组a中,即编号为0的结点存储到a[0]中,其余类推,则a[i]元素的左孩子元素为( ),右孩子元素为( ),双亲元素(i>0)为( )。
6、对于一棵具有n个结点的二叉树,对应二叉链表中指针总数为( )个,其中( )个用于指向孩子结点,( )个指针空闲着。
7、在一棵高度为5的理想平衡树中,最少含有( )个结点,最多含有( )个结点。
参考答案:
一、选择题
1-8CACCADAD
二、填空题
1、n-1 2、31 3、6 4、6 5、a[i*2+1],a[i*2],a[i div 2] 6、2n,n-1,n+1 7、16,31
六、由广义表生成二叉树
二叉树的生成根据输入的不同而不同,这里,我们介绍如何将一个广义表表示的二叉树表示成一棵用动态链接表存储的二叉树。
首先,有必要对广义表的表示规范一下,有助于我们后面的处理,每棵树的根结点放在最前面;若它有子树,则在它后面加一对圆括号,子树按从左子树到右子树的顺序写入,并在左右子树之间放一个逗号(,)隔开;若某个结点只有左子树而没有右子树,则省略逗号和右子树;若某个结点只有右子树而没有左子树,则左子树省略,但不能省略逗号;在整个树的后面加入一个@作为结束符。
算法的基本思想是:依次输入广义表中的每个字符,若遇到字母(假定以字母为结点的值),则表明是结点的值,就为它建立一个新的结点,并把它作为根(第一个字母时)或作为孩子结点连接到它的父结点上;若遇到的是左括号,则表明子树表开始,应首先将它前面字母的结点的指针(即此子树的根)进栈,以便作为以后结点的父结点指针引用,并记下下一个要插入的结点应为其父结点的左孩子(k=1);若遇到右括号,则表明子树表结束,应退栈;若遇到逗号,则表明以左子树处理完毕,应处理其父结点的右孩子(k=2)。这样处理每一个字符,直到处理到@说明输入结束。
其算法的伪代码为:
procedure buildtree;
begin
输入一个字符ch
while (ch 不为@) do
begin
case ch of
字母:新建结点,值域的值为ch,左右子树指针为空;
如果这是第一个结点,那么它是根结点,否则若k=1,则将当前结点作为栈顶结点的左子树,否则(k=2),则将当前结点作为栈顶结点的右子树。
‘(’:将当前的结点插入栈顶;设下一次处理的为左子树(k:=1)
‘)’:栈顶元素出栈。
‘,’:设下一次处理的为右子树(k:=2)。
end;
输入下一个字符ch
end;
end;
二叉树的运算
二叉树的运算主要包括二叉树的遍历、二叉树的输出、求二叉树的深度等。
为了讨论算法的方便,以下我们的操作都建立在动态链接表上,Node是一个具有PNode类型的参数,并且最开始是指向一棵二叉树的根结点。
一、二叉树的遍历
二叉树的遍历是指按照一定的顺序访问二叉树的每一个结点,并且每个结点只访问一次。它是二叉树中一个非常重要的内容。
由二叉树的递归定义,一棵非空二叉树由根结点、左子树和右子树组成。若用D、L和R分别表示根和左、右子树,则二叉树的遍历一共有六种:DLR、LDR、LRD、DRL、RDL、RLD,其中前三种都是先遍历左子树、后遍历右子树,而后三种相反。由于前三种出现得比较普遍,而后三种很少出现,且前三种和后三种具有对称性,这里只讨论前三种遍历。
在遍历DLR中,因根先于左、右子树遍历,故称为前序遍历(preorder)或先根遍历;在遍历LDR中,因根在左、右子树遍历之间,故称为中序遍历(inorder)或中根遍历;在遍历LRD中,因根后于左、右子树遍历,故称为后序遍历(postorder)或后根遍历。显然,遍历左右子树仍然是遍历二叉树的问题,所以很容易给出这三种遍历的递归算法。
1、前序遍历算法
规则如下:
若二叉树为空,则退出;否则
(i) 访问处理根结点;
(ii) 前序遍历左子树;
(iii) 前序遍历右子树。
例如对于图14所示的二叉树,前序序列(A,B,D,E,H,C,F,G)
Procedure preorder(Node:PNode);
begin
if Node<>nil then
process(Node); {表示对Node进行的处理,这个处理是什么要根据具体情况而定}
preorder(Node^.left); {递归遍历左子树}
preorder(Node^.right); {递归遍历右子树}
end;
2、中序遍历。
规则如下:
若二叉树为空,则退出;否则
(i) 中序遍历左子树;
(ii) 访问处理根结点;
(iii) 中序遍历右子树。
例如对于图14所示的二叉树,中序序列(D,B,H,E,A,F,C,G)
procedure inorder(Node:PNode);
begin
if Node<>nil then
inorder(Node^.left);
process(Node);
inorder(Node^.right);
end;
3、后序遍历算法
若二叉树为空,则退出;否则
(i) 后序遍历左子树;
(ii) 后序遍历右子树
(iii) 访问处理根结点。
例如对于图14所示的二叉树,后序序列(D,H,E,B,F,G,C,A)。
Procedure postorder(Node:PNode);
begin
if Node<>nil then
postorder(Node^.left);
postorder(Node^.right);
process(Node);
end;
二、输出二叉树
输出二叉树就是将二叉树以某种表示形式打印出来。下面,我们介绍如何将二叉树以广义表的形式输出。
以广义表的形式输出时,首先应输出树的根结点;然后加一个左括号;如果左子树非空则输出左子树;如果右子树非空则输出一个逗号加上右子树;最后再输出一个右括号。当然,如果左、右子树都是空的,就没有必要输出括号和括号内的。
注:print有打印的意思,写的时候容易阅读,但在PASCAL中的print没有意思,除非你自己的程序定义它,
procedure print(p:sorttree);
begin
if p<>nil then
write(p^.data);
if (p^.left<>nil)or (p^.right<>nil) then
begin
write('(');
if p^.left<>nil then print(p^.left);
if p^.right<>nil then
begin
write(',');
print(p^.right);
write(')');
end
else write(')');
end;
end;
三、求二叉树的深度
若一棵二叉树为空,则它的深度为0,否则它的深度定义为:它左子树和右子树深度的最大值+1。显然,二叉树的深度是递归定义的,因此,在求二叉树的深度时也可以递归地求:
function depth(Node:PNode):integer;
begin
if Node=nil then return 0;
depth1:=depth(Node^.left);
depth2:=depth(Node^.right);
if depth1>depth2 then
return depth1+1
else
return depth2+1;
end;
习题
1、假定一棵二叉树广义表表示为a(b(c),d(e,f)),分别写出它进行先序、中序、后序、按层遍历的结果。
先序:
中序:
后序:
按层:
2、已知一棵二叉树的先根和中根序列,求该二叉树的后根序列。
先根序列:A,B,C,D,E,F,G,H,I,J
中根序列:C,B,A,E,F,D,I,H,J,G
后根序列:
3、已知一棵二叉树的中根和后根序列,求该二叉树的高度和双支、单支及叶子结点数。
中根序列:c,b,d,e,a,g,I,h,j,f
后根序列:c,e,d,b,i,j,h,g,f,a
高度:
双支:
单支:
叶子:
参考答案:
1、先序:abcdef 中序:cbaedf 后序:cbefda 按层:abdcef
2、后根序列:C,B,F,E,I,J,H,G,D,A
3、5, 3, 3, 4
树的存储结构和运算
这里所指的树是指度不小于2的树,在有的书上称为多叉树或多元树。下面我们均以三叉树为例对其进行讨论。
一、树的存储结构
对树的存储通常采用如下三种表示:
1、标准形式
在这种表示中,树中的每个结点除了包含有存储数据元素的值域外,还包含有三个指针域,用来分别指向三个孩子结点,或者说,用来分别链接三棵子树。若结点采用动态产生,则结点类型和指向结点的指针类型可定义为:
Type
PNode=^TNode;
TNode=record
data:elementType;
children:array[1..3] of PNode;
end;
其中children[1]、children[2]、children[3]分别存储三个孩子结点的指针域。
2、广义标准形式
广义标准形式是在标准形式的每个结点中增加一个指向其双亲结点的指针域。
3、二叉树形式
这种表示指首先将树转换为对应的二叉树形式,然后再采用二叉链表存储这棵二叉树。
二、树的运算
树的运算包括建立树的存储印象、遍历等。
1、建立树的存储印象
假定给出树的广义表要求建立树的存储结构采用标准形式。如图15所示,其广义表表示为:
A(B(E,F(H,I,J),C(G),D)
在树的生成算法中,需要设置两个栈:一个用来存储指向根结点的指针,以便将孩子结点链向双亲结点;另一个用来存储待链接的孩子结点的序号,以便能正确地链接到双亲结点的指针域。假定这两个栈分别用S和P表示,则树的生成算法可以写成如下的形式:
procedure buildtree;
begin
read(ch);
while (ch<>@) do
begin
case ch of
‘A’..’Z’:新建结点,值域的值为ch,左右子树指针为空;如果这是第一个结点,那么它是根结点,否则将当前结点作为S栈顶结点的子树,其序号由P栈顶元素决定。
‘(’:将当前的结点插入S栈顶;将1插入P栈顶,表示接下来要链到其父结点的第一个指针域。
‘)’:S、P栈顶元素出栈。
‘,’:将P栈顶元素加1,表示下一次处理的是其父结点的下一个指针域。
end;
read(ch);
end;
end;
其实这里的操作和二叉树的非常类似,只是这里的结点数多一点,不再局限于左子树和右子树。
2、遍历树
树的遍历分为深度优先遍历(有的书上也叫先根遍历)和广度优先遍历(也叫按层遍历),还有后根遍历,但第三种应用得不多。
深度优先遍历是指首先访问其根结点,然后从左到右递归地访问每一棵子树。如对上图(三叉树)中的树进行深度优先遍历的结果为:
A,B,E,F,H,I,J,C,G,D
仿照二叉树的先序遍历,可以给出三叉树的深度优先遍历的算法框架:
procedure preorder(Node:PNode);
begin
if Node<>nil then
write(Node^.data);
for i:=1 to 3 do
preorder(Node^.children[i]);
end;
广度优先遍历是指首先访问第一层的结点(即根结点),然后从左到右访问第二层的结点,然后第三层、第四层,直到所有的结点都访问完毕。如对上图(三叉树)中的树进行广度优先遍历的结果为:
A,B,C,D,E,F,G,H,I,J
在对树进行广度优先遍历时,需要设置一个队列,开始时队列为空。如果树为空树,则算法结束,否则将根结点插入队列中。以后的每一步,都取出队首元素,访问它,并从左到右将其子树插入队列中。这样所遍历的结果就是按广度优先遍历访问的结果。
广度优先遍历的算法框架可写为:
procedure layerorder(Node:PNode);
begin
若Node=nil 则return;
SetNull(Q);
insert(Node,Q);
repeat
P:=Q.pop;
write(P^.data);
for i:=1 to 3 do
if p^.children[i]<>nil then
insert(P^.children[i],Q);
until Null(Q)=true;
end;
二叉搜索树
一、二叉搜索树的定义
二叉搜索树(Binary search tree)又称二叉排序树或二叉查找树,它或者是一棵空树,或者是一棵具有如下特性的非空二叉树:
(1)若它的左子树非空,则左子树中所有的结点的值都不大于根结点的值;
(2)若它的右子树非空,则右子树中所有的结点的值都不小于根结点的值;
(3)它的左、右子树分别是一棵二叉搜索树。
由于二叉搜索树的值是按左子树、根、右子树有序的,所以对一棵二叉搜索树进行中序遍历访问所有结点所得的序列是有序的。
排序二叉树的存储结构如下:
Type
sorttree=^node; {排序二叉树结点的指针类型}
node=record {排序二叉树的结点类型}
key:keytype;
info:datatype;
llink,rlink:sorttree;
end;
二、二叉搜索树的查找
由二叉搜索树的定义,在二叉搜索树中查找一个结点值为K的结点的过程为:若二叉树为空,则查找失败,返回空指针;若当前根结点的值等于K,则查找成功,返回当前的根结点;若K小于当前结点的值,则说明要找的结点可能在当前结点的左子树中,进入左子树查找;若大于当前结点的值,则说明要找的结点只可能在当前结点的右子树中,进入右子树查找。显然这个过程和前面的过程一样,也是递归调用的。其实现过程如下:
Type
PNode=^TNode;
TNode=record
data:elementtype;
left,right:PNode;
end;
function searchkey(Node:PNode;K:datatype):PNode;
begin
if Node=nil then return nil;
if K=Node^.data then return Node;
if K<Node^.data then return searchkey(Node^.left,K);
if K>Node^.data then return searchkey(Node^.right,K);
end;
从算法中可以看出,这个算法递归时一旦返回,就再也不会再出现递归的调用,这种递归叫做末尾递归。末尾递归可以写成非递归的形式,这样可以节省栈所用的空间和时间。如上面的算法可以写成:
function searchkey(Node:PNode,K:datatype):PNode;
begin
while (Node<>nil) do
if K=Node^.data then return Node;
if K<Node^.data then Node:=Node^.left;
if K>Node^.data then Node:=Node^.right;
return nil;
end;
在对二叉树搜索树进行查找的过程中,K与结点比较的次数最小为一次,即树的根结点就是要查找的结点,最多为树的深度次,所以平均的查找次数要小于等于树的深度。理论上已经证明,平均查找次数为1+4log2n次。若二叉树是理想平衡二叉树或接近理想平衡树,则查找次数大概为log2n次。因此,对二叉搜索树的查找的复杂度在最好情况和平均情况下都是O(log2n)级别的。但是,要是二叉搜索树退化成一条链或近似链的情况,即单支结点非常多,则深度会非常大,查找时的复杂度为O(n)。由上面的分析可知,利用二叉搜索树虽然最坏情况下和单链表的复杂度相同,但一般情况下比单链表好得多。至于空间复杂度,显然只有结点存在时要分配内存,因此空间复杂度为O(n)的。
三、二叉搜索树的插入和生成
procedure insert(var t:sorttree;s:sorttree); {把s插入以t为根的排序二叉树}
var p,f:sorttree;flag:bollean;
begin
p:=t;
while p<>nil do {查找应插入的位置f}
begin
f:=p;
case flag of
s^.key=p^.key:return;
s^.key<p^.key:p:=p^.llink;
s^.key>p^.key:p:=p^rlink;
end;
end;
case flag of
t=nil:t:=s;
s^.key<f^.key:f^.llink:=s;
s^.key>f^.key:f^.rlink:=s;
end;
end;
二叉搜索树的插入是指在一棵二叉搜索树中插入一个关键字,使它仍满足二叉搜索树的要求。
二叉搜索树的生成通常是从一棵空树开始,将关键字一个一个地加入树中,从而产生一个具有很多结点的二叉搜索树。
在插入结点时,若当前的树为空,则建立一个结点,将这个结点作为根,并使它的值为插入的关键字;若关键字小于当前根结点的值,则应该将关键字插入左子树中,否则应插入右子树中。
上面的算法是递归的,可以写为:
同查找一样,这里的递归也是末尾递归,可以转化为非递归来做。和递归不同的是,非递归时必须要记下父结点的指针是多少,而不能单纯地对其父结点的某一个子结点的指针操作。其出现过程为:
procedure insert(var Node:PNode;key:datatype);
begin
new(TmpNode);
TmpNode^.data:=key;
TmpNode^.left:=nil;
TmpNode^.right:=nil;
if Node=nil then Node:= TmpNode;
p:=Node;{当前处理的结点指针}
q:=nil;{父结点记录指针,树的根结点无父结点}
while (p<>nil)
q:=p;{接下来要进入子结点,改变父结点指针}
if key<p^.data then p:=p^.left else p:=p^.right;{改变当前结点指针}
if key<q^.data then q^.left:=TmpNode else q^.right:=TmpNode;
end;
对二叉搜索树的一次插入的复杂度主要集中在查找上,所以最好情况和平均情况下都是O(log2n)的,而最坏情况下是O(n)的。
设要建立二叉搜索树的序列为:(63,71,6,52,85,53,82,38,11),则二叉搜索树的插入过程如图16所示:
例题1:读入一批数,遇负数时结束,将其中的正数组成排序的二叉树。
讨论:二叉树的结点类型应是包括三个域的记录类型:一个数据域,两个指针域。分别称为左指针和右指针,用它们指向左子树和右子树。
一级算法:
1 将根指针赋值为NIL,读第一个数
2 while读入数≥0 do
begin
3 加入树
4 读下一个数
end
其中第3步需求精。
二级求精
将第3步定义成一个递归过程。设该过程有个指针参数p,开始它指向树的根结点,在递归调用时将指向子树的根结点。
第3步 加入树
3—1 if p=nil
3—2 then 加入一结点,并由p指向它
else if 读入数<p所指向的结点值
3—3 then 加入左子树
3—4 else 加入右子树
程序:建立排序二叉树
program createtree(input,output);
{建立排序二叉树}
type
point=^node; {排序二叉树结点的指针类型}
node=record {排序二叉树的结点类型}
data:real;
left,right:point
end;
var
x:real;
root:point;
procedure add(x:real;var p:point);
{加入树过程}
begin
if p=nil
then begin {加入一结点}
new(p);
with p^ do
begin
data:=x;
left:=nil;
right:=nil
end
end
else with p^ do
if x<data
then add(x,left) {加入左子树}
else add(x,right) {加入右子树}
end; {add}
begin
root:=nil;
read(x);
write(x:6:1)
while x>=0 do
begin
add(x,root);
read(x);
write(x:6:1)
end;
writeln
end.
例2、排序二叉树:每一个参加排列的数据对应二叉树的一个结点,且任一结点如果有左(右)子树,则左(右)子树各结点的数据必须小(大)于该结点的数据。中序遍历排序二叉树即得排序结果。程序如下:
program pxtree;
const
a:array[1..8] of integer=(10,18,3,8,12,2,7,3);
type point=^nod;
nod=record
w:integer;
right,left:point ;
end;
var root,first:point;k:boolean;i:integer;
procedure hyt(d:integer;var p:point);
begin
if p=nil then
begin
new(p);
with p^ do begin w:=d;right:=nil;left:=nil end;
if k then begin root:=p; k:=false end;
end
else with p^ do if d>=w then hyt(d,right) else hyt(d,left);
end;
procedure hyt1(p:point);
begin
with p^ do
begin
if left<>nil then hyt1(left);
write(w:4);
if right<>nil then hyt1(right);
end
end;
begin
first:=nil;k:=true;
for i:=1 to 8 do hyt(a[i],first);
hyt1(root);writeln;
end.
若输入的数据依次为:75,28,34,91,83,14,65,99 建立的排序二叉树如上图所示。
结合上图二叉树输入数据,来看看该程序的执行情况
第一次root 为空,x为75,调用过程add,变量参数p为空,执行new(p),实际上相当于执行new(root),将75赋给p(即root)所结指结点的数据域,左、右指针域 赋值为空,这就是树的根结点,返回主程序。
读入第二个数28,再调用过程add。由于第一次调用已给root赋值,它不为空,因而p也不为空。与p所指结点的数据域比较,实际上是与根结点的数据域比较。由于28<75,执行语句add(x,left),它处于with 语句中,相当于执行add(x,root^.left),这是对过程add的递归调用。现在的变量参数p就是root^.left,即根据结点的左指针,由于在第一次调用时,它已被赋值为空,执行语句new(p),相当于执行语句new(root^.left),即让根结点的左指针指向一个新的结点,并给它的数据域赋值为28,左、右指针域赋值为空,返回主程序。
读入第三个数34,再调用过程add。root不为空,p不为空,与root所指结点的数据域75比较,小于它,执行语句add(x,root^.left),递归调用过程add。由于在读入第二个数时,已将它赋值,所以root^.left不为空,对应此时的参数p不为空。与p所指结点的数据域比较,实际上是与root^.left所指结点的数据域值28比较,大于它,执行语句add(x,p^.right),实际上是执行语句add(x,root^.left^.right),再次递归调用过程add。由于在读入第二个数时,已经将root^.left^.right赋值为空,所以在该层调用中,p为空,new(p)即new(root^.left^.right),即让包括数28的结点的右指针指向一个新的结点,并给它的数据域赋值为34,左、右指针赋值为空,返回主程序。
四、二叉搜索树的删除
二叉搜索树的删除比插入要复杂些。我们分几种情况讨论:
若结点为叶结点:直接将结点删除。即,若要删除的是根结点,则将树置空;若删除的是结点X的左子树,则设X的左子树为空树;若删除的是结点X的右子树,则设X的右子树为空树。如图17(a)是一棵二叉搜索树,(b)是它删除关键字11后的二叉树。
若结点为单分支结点:如果一个结点只有左子树或右子树,然后,直接把它删除,用它的左子树或右子树代替它原来的位置即可。如图17(b)是一棵二叉搜索树,(c)是它删除关键字6后的二叉树。
若结点为双分支结点:删除双分支结点的思想是,将它的中序遍历前趋复制到它的位置覆盖它,然后删除原来中序前趋结点。因为结点既有左孩子又有右孩子,所以从它的左孩子开始,每次都往右孩子走,直到走不了了,最后走到的结点就是它的中序前趋(为什么?请读者自己思考)。显然,它的中序前趋的右孩子为空树,因此删除时只要用上面的叶结点或单分支结点的删除方法就可以了。图17(c)是一棵二叉搜索树,(d)是它删除关键字63后的子树。
综上所述,删除二叉搜索树中的一个关键字,可以如上实现:
function delete(var Node;key):boolean;{若成功,返回true}
begin
在Node中查找key,使找到的结点为p,其父结点为q(此步骤可由查找的过程稍微变动一下得到,这里不再写出).
if p=nil then return false;
if (p^.left=nil) and (p^.right=nil) then {实际上,这个过程可以放在左、右子树中有一个非空的里面一同处理}
if p=Node then Node:=nil;return true;
if p=q^.left then q^.left:=nil
else q^.right:=nil;return true;
if (p^.left=nil) or (p^.right=nil) then
tmp:=p^.left(左子树非空) 或tmp:=p^.right(右子树非空)
if p=Node then Node:=tmp;
if p=q^.left then q^.left:=tmp else q^.right:=tmp;return true;
if (p^.left<>nil) and (p^.right<>nil)
tmpq:=p;tmpp:=p^.left;
while (tmpp^.right<>nil)
tmpq:=tmpp;tmpp:=tmpp^.right;
p^.data:=tmpp^.data;
if tmpp=p^.left then p^.left:=tmpp^.left else tmpq^.right:=tmpp^.left;
end;
和对二叉搜索树的一次插入操作一样,二叉搜索树的一次删除操作的复杂度主要集中在查找上,所以最好情况和平均情况下都是O(log2n)的,而最坏情况下是O(n)的。
另附:
排序二叉树删除结点算法:
要删除排序二叉树的某一结点p(f为p的双亲),可以分成以下几种情况:
1、如果p为叶子结点,只要把链接p的指针置为nil。若p为f的左子树,则令f^.llink:=nil,若为右子树,令f^.rlink:=nil。
2、如果p的度为1,非根结点。则根据排序二叉树特点进行下列操作:
case flag of
(p=f^.llink) and (p^.llink<>nil):f^.llink:=p^.llink;
(p=f^.llink) and (p^.rlink<>nil):f^.llink:=p^.rlink;
(p=f^.rlink) and (p^.llink<>nil):f^.rlink:=p^.llink;
(p=f^.rlink) and (p^.rlink<>nil):f^.rlink:=p^.rlink;
end;
3、如果p的度为2且非根结点。由于f的左子树关键字值小于右子树关键字值,可以进行以下操作:
If p=f^.llink
then begin f^.llink:=p^.rlink;f^.llink^.link:=p^.llink;end
else begin f^.rlink:=p^.rlink;f^.rlink^.llink:=p^.llink;end;
dispose(p);
4、如果p为根结点。那么删除操作可以这样进行:
s1:=p^.llink;
while s1<>nil do
begin
s:=s1;
s1:=s1^.rlink;
end;
s^.llink:=p^.llink;
s^.rlink:=p^.rlink;
dispose(p);
习题
1、已知一组元素为(46,25,78,62,12,37,70,29),画出按元素排列顺序输入生成的一棵二叉搜索树,再以广义表形式给出该二叉搜索树。
program aa;
type sorttree=^node;
node=record
data:integer;
left,right:sorttree;
end;
var t,q,p:sorttree; x:integer;
procedure seta(var node:sorttree;k:integer);
var p,q,t:sorttree;
begin
new(t);
t^.data:=k;
t^.left:=nil;
t^.right:=nil;
if node=nil then node:=t else
begin
p:=node;
q:=nil;
while p<>nil do
begin
q:=p;
if k<p^.data then p:=p^.left
else p:=p^.right;
end;
if k<q^.data then q^.left:=t
else q^.right:=t;
end;
end;
procedure print(p:sorttree);
begin
if p<>nil then
write(p^.data);
if (p^.left<>nil)or (p^.right<>nil) then
begin
write('(');
if p^.left<>nil then print(p^.left);
if p^.right<>nil then
begin
write(',');
print(p^.right);
write(')');
end
else write(')');
end;
end;
begin
read(x);
while x<>999 do
begin
seta(p,x);
read(x);
end;
print(p);
end.
以广义表形式给出该二叉搜索树:46(25(12,37(29)),78(62(,70)))
哈夫曼树
一、基本术语
在一棵树中,若存在一个序列k1,k2,k3,……,kp使得ki与ki+1是有边相连的结点(即父亲与孩子的关系),则称序列k1,k2,k3,……,kp是一条从k1到kp的路径,其中,路径中所经过的边数p-1称为路径长度。在k1到kp的所有路径中,长度最短的一条叫做最短路径,最短路径的长度叫做最短路径长度。
在许多应用中,常常将树中的结点赋上了一具有某种意义的数,称为权。结点的带权路径长度指结点的权与该结点到根结点的最短路径长度的乘积。
树的带权路径长度,是指所有叶子结点的带权路径长度之和。通常记为:
WPL= wili
其中n表示叶结点数,wi和li分别表示叶结点ki的权值和ki到根的最短路径长度。
二、哈夫曼树
哈夫曼树(Huffman)树又称最优二叉树,它是n个带权叶结点构成的所有二叉树中,带权路径长度最小的二叉树。因为构造这种树的算法最早是由哈夫曼于1952年提出的,所以被称为哈夫曼树。
例如,有四个权分别为2,5,6,8叶结点,它们可以构成不同的二叉树,图18给出了几种,他们的带权路径长度分别为:
(a) WPL=2*2+5*2+6*2+8*2=42
(b) WPL=2*1+5*2+6*3+8*3=54
(c) WPL=8*1+6*2+2*3+5*3=41
可以看出,(c)的WPL是最小的。稍后可知,(c)就是哈夫曼树。
由上面的例子可以看出,树的深度小的不一定带权路径长度小,只有权大的结点放在尽量靠根的层才能得到较优值。
三、构造哈夫曼树
构造哈夫曼树的算法是由哈夫曼提出的,所以称之为哈夫曼算法。具体过程如下:
(1)根据n个权值{w1,w2,w3,…,wn}对应的n个结点构成n棵二叉树的森林F={T1,T2,T3,…,Tn},其中每棵二叉树Ti(1≤i≤n)都有且仅有一个权值为wi的根结点,其左、右子树为空;
(2)在森林F中选出两棵根结点的权值最小的树作为一棵新树的左、右子树,且置新树的附加根结点的权值为其左、右子树上根结点的权值之和;
(3)从F中删除这两棵树,同时把新树加入F中;
(4)重复(2)和(3),直到F中只有一棵树为止,此树便是哈夫曼树。
图19中是用(2,5,6,8)构造哈夫曼树的过程。
在第二步中,若选权最小的有多于一种选法(如第二大的值和第三大的值相等),则选取不同的值就会对应不同的哈夫曼树;若左右子树摆放的顺序不同,也会产生不同的哈夫曼树。但是,不管选择或摆放的顺序是怎样,所构造出来的哈夫曼树的带权树路径长度是不会变的。如图18(c)和图19(d)都是由{2,5,6,8}所产生的哈夫曼树,其带权路径长度都是2*3+5*3+6*2+8*1=41。
例题、如何构建哈夫树:(思想是:权越大离根越近)
program gojiantree;
const n=4;m=7;
type node=record
w:real;
parent,lchild,rchild:0..m
end;
htree=array[1..m] of node;
var htree1:htree;
procedure gjtree(var ht:htree);
var i,j:integer;
small1,small2:real;
p1,p2:0..m;
begin
for i:=1 to m do
with ht[i] do
begin
w:=0;lchild:=0;rchild:=0;parent:=0;
end;
for i:=1 to n do read(ht[i].w);
for i:=n+1 to m do
begin
p1:=0;p2:=0;
small1:=1000;small2:=1000;
for j:=1 to i-1 do
if ht[j].parent=0 then
if ht[j].w<small1 then
begin small2:=small1;small1:=ht[j].w;p2:=p1;p1:=j end
else if ht[j].w<small2 then begin small2:=ht[j].w;p2:=j end;
ht[p1].parent:=i;
ht[p2].parent:=i;
ht[i].lchild:=p1;
ht[i].rchild:=p2;
ht[i].w:=ht[p1].w+ht[p2].w;
end;
end;
begin
gjtree(htree1);
end.
另参考程序:
program hf;
type point=^node;
node=record
data:integer;
l,r:point;
end;
var a:array[1..1000] of point;
i,n,k,v,x:integer;p,s,max:point;
function f(s,t:integer):longint;
var i,min:integer;
begin
min:=s;
for i:=s+1 to t do
if a[i]^.data<a[min]^.data then
min:=i;
f:=min;
end;
function q(x:integer):point;
begin
new(q);q^.data:=x;q^.l:=nil;q^.r:=nil;
end;
procedure w(h:point);
begin
if h<>nil then begin
writeln(h^.data);
w(h^.l);w(h^.r);
end;
end;
begin
readln(n);new(max);
for i:=1 to n do
begin
read(x);a[i]:=q(x);
end;
for i:=1 to n-1 do
begin
k:=f(1,n-i+1);
s:=a[k];
a[k]:=a[n-i+1];
v:=f(1,n-i);
new(p);p^.data:=s^.data+a[v]^.data;p^.l:=a[v];p^.r:=s;
a[v]:=p;
end;
w(p);
end.
四、带权路径长度与合并费用和
在哈夫曼树的构造中,如果合并两个权值最小的树的权值分别为a和b,而合并这两棵树的费用记为a+b,则构造哈夫曼树所用的费用与哈夫曼树的带权路径长度相等。
显然,对于一个权值为x的结点X,在与它的兄弟合并时要累加权值,而它父亲与其兄弟合并时又计算了一遍,然后父亲的父亲又计算一遍……也就是说,当它是第L层时,它的权值就计算了L-1遍,刚好等于根结点到它的最短路径长度。所以带权路径长度与合并费用和是相等的。
五、哈夫曼码
哈夫曼树的应用较广,哈夫曼码是其中非常重要的一种应用例子。
在电报通讯中,电文是以二进制的0、1序列传送的。在发送端需要将电文中的字符序列转换成二进制序列(即编码),在接收端又需要把接收二进制序列转换成对应的字符序列(即译码)。
最简单的二进制编码方式就是等长编码。如,假定电文中只有A、B、C、D、E这五种编码,在进行等长编码时,它们至少需要三位二进制来表示,可以依次编码为:000、001、010、011、100。若用这六个字符作为六个叶子结点生成一棵二叉树,使二叉树的每个结点的左、右分支分别用0、1编码,从根结点到叶结点所经过的分支的0、1编码序列等于叶结点的二进制编码,则对应的编码二叉树如图20所示。
显然,在电文中每个字母出现的频率(次数)一般是不同的。在一份电文中,设这五个字符出现的频率分别为ci,则这份电文的总长度为:
Len=cili
其中n表示电文中的字符种数,li表示第i种字符的编码长度。
若在上面的电文中,字符A、B、C、D、E分别为2、5、6、8、12,则可算出此电文的总长度:
Len=(ci×3)=3×(2+5+6+8+12)=99
所以,当采用等长编码时,编码的总长度为99。
怎样缩短传送电文的总长度,从而节省时间呢?可以想到,若采用不等长编码,让出现频率高的字符具有短的编码,出现频率低的字符具有长的编码,这样有可能缩短传送文件的长度。采用不等长编码必须避免译码的二义性或多义性。假设用0表示字符A的编码,01表示B的编码,当接收到编码串01……时,是用0译出A还是把0留着和1组成01译出B就是一个很大的问题,这样就产生了二义性。因此,若对一个字符集采用不等长编码,则要求字符集中任何字符的编码都不能是另一个字符编码的前缀。符合这个要求的编码叫做前缀编码。显然,等长编码是前编码。
为了使不等长编码为前缀编码,可以用该字符集中的字符作为叶结点构造一棵编码二叉树。将每个字符出现的频率作为对应结点的权值,这样,树的带树路径长度就是电文编码后的长度。
为了使电文的长度尽可能短,可以使用哈夫曼树。在上面的例子中,由{2、5、6、8、12}所生成的哈夫曼树如图21所示,由此,可以给字符以相应的编码:A:000,B:001,C:01,D:10,E:11,电文的最短传送长度为:
Len=WPL=2*3+5*3+6*2+8*2+12*2=73
显然,这比原来的等长传送要短得多。
树的应用
例1、用链表存储方式生成上述二叉树,中序遍历之。
①.将上述二叉树用广义表表示为A(B(D,E(G)),C(F(,H)))
②.根据广义表串(以#结束)生成二叉树。
program ltree;
const n=8;
type trlist=^node;
node=record
da:char;
l,r:trlist;
end;
var s:array[1..n] of trlist;
p,root:trlist;
ch:char;
top,k:integer;
procedure creat(var head:trlist);
begin
read(ch);
top:=0;
while ch<>'#' do
begin
case ch of
'A'..'Z':begin new(p);p^.da:=ch;p^.l:=nil;p^.r:=nil;
if top<>0 then
case k of
1:s[top]^.l:=p;
2:s[top]^.r:=p;
end
end;
'(':begin top:=top+1;s[top]:=p;k:=1;end;
')': top:=top-1;
',': k:=2;
end;
read(ch);
end;
head:=s[1];
end;
procedure inorder(head:trlist);
begin
if head^.l<>nil then inorder(head^.l);
write(head^.da);
if head^.r<>nil then inorder(head^.r);
end;
begin
write('Input tree string:');
creat(root);
inorder(root);
end.
例2、.用顺序存储方式建立一棵有31个结点的满二叉树,并对其进行先序遍历。
program erchashu1;
var b:array[1..31] of char;
e:array[1..63] of byte;
n,h,i,k:integer;
procedure tree(t:integer);
begin
if e[t]=0 then exit
else
begin
write(b[t]);e[t]:=0;
t:=2*t;tree(t);
t:=t+1;tree(t);
end;
end;
begin
repeat
write('n=');readln(n);
until (n>0) and (n<6);
fillchar(e,sizeof(e),0);
k:=trunc(exp(n*ln(2)))-1;
for i:=1 to k do e[i]:=1;
for i:=1 to 26 do b[i]:=chr(64+i);
for i:=1 to 5 do b[26+i]:=chr(48+i);
h:=1 ;tree(h);
writeln;
end.
例3、用顺序存储方式建立一棵如图所示的二叉树,并对其进行先序遍历。
program tree1;
const n=15;
type node=record
data:char;
l,r:0..n;
end;
var tr:array[1..n] of node;
e:array[1..n] of 0..1;
i,j:integer;
procedure jtr;
var i:integer;
begin
for i:=1 to n do
with tr[i] do
readln(data,l,r);
end;
procedure search(m:integer);
begin
with tr[m] do
begin
write(data);
if l<>0 then search(l);
if r<>0 then search(r);
end;
end;
begin
jtr;search(1);writeln;
end.