收藏 分享(赏)

算法与数据结构(c语言)_第5章_二叉树与树.ppt

上传人:dzzj200808 文档编号:3230951 上传时间:2018-10-08 格式:PPT 页数:129 大小:1,011.50KB
下载 相关 举报
算法与数据结构(c语言)_第5章_二叉树与树.ppt_第1页
第1页 / 共129页
算法与数据结构(c语言)_第5章_二叉树与树.ppt_第2页
第2页 / 共129页
算法与数据结构(c语言)_第5章_二叉树与树.ppt_第3页
第3页 / 共129页
算法与数据结构(c语言)_第5章_二叉树与树.ppt_第4页
第4页 / 共129页
算法与数据结构(c语言)_第5章_二叉树与树.ppt_第5页
第5页 / 共129页
点击查看更多>>
资源描述

1、第五章 二 叉 树与树,树形结构是一种十分重要的数据结构。本章讨论的二叉树、树和树林都属于树形结构。 在树形结构中每个结点最多只有一个前驱,但可有多个后继的结构。 它们的共同之处是都表示了一种具有层次的分支关系。,5.1 二叉树及其抽象数据类型,二叉树是一类简单而又重要的树形结构。 本节先介绍它的基本概念和重要性质, 然后引入二叉树的抽象数据类型。,5.1.1基本概念,二叉树可以定义为结点的有限集合,这个集合或者为空集,或者由一个根及两棵不相交的分别称作这个根的左子树和右子树的二叉树组成。 二叉树的定义是个递归定义。 二叉树可以是个空集合,这时的二叉树称为空二叉树。二叉树也可以是只有一个结点的

2、集合,这个结点只能是根;它的左子树和右子树均是空二叉树。,下图表示的是二叉树的五种基本形态。,二叉树相关的一组术语 : 父结点、左(右)子结点、边 兄弟 祖先、子孙 路径、路径长度 结点的层数 结点的度数 二叉树的高度 :二叉树中结点的最大层数称为二叉树的高度。 例如,二叉树t的高度为3。 树叶、分支结点,满二叉树:如果一棵二叉树的任何结点或者是树叶,或有两棵非空子树,则此二叉树称作满二叉树(离散数学中称此树是正则的)。 完全二叉树:如果一棵二叉树至多只有最下面的两层结点度数可以小于2,其余各层结点度数都必须为2,并且最下面一层的结点都集中在该层最左边的若干位置上,则此二叉树称为完全二叉树。

3、完全二叉树不一定是满二叉树。,扩充的二叉树:把原二叉树的结点都变为度数为2的分支结点,也就是说,如果原结点的度数为2,则不变,度数为1,则增加一个分支,度数为0(树叶),则增加两个分支。 新增加的结点(树叶结点)都用小方框表示,称为外部结点,树中原有的结点称为内部结点。把空二叉树扩充得到的扩充二叉树规定为只有一个外部结点组成的二叉树。,在扩充的二叉树中,外部路径长度E定义为在扩充的二叉树中从根到每个外部结点的路径长度之和。内部路径长度I定义为在扩充的二叉树中从根到每个内部结点的路径长度之和。,如在图的扩充二叉树中: E = 2 + 2 + 4 + 4 + 3 + 3 + 3 = 21 I =

4、0 + 1 + 1 + 2 + 2 + 3 = 9,5.1.2 主要性质,性质1 在非空二叉树的i层上至多有2i个结点(i0)。 证明:用归纳法来证。 性质2 高度为k的二叉树中最多有2k+1 - 1个结点(k0)。 证明:假设第i层上的最大结点个数是mi,由性质1可知,深度为k的二叉树中最大结点个数M为:,性质3 对于任何一棵非空的二叉树,如果叶结点个数为n0,度为2的结点个数为n2,则有:n0= n2 + 1 。证明:设一棵非空二叉树中有n个结点,度为1的结点个数为n1,因为二叉树中所有结点的度均不大于2,所以 n = n0 + n1 + n2 (1) 在二叉树中,除根结点外,其余每个结点

5、都有一个分支进入,假设B为分支总数,则有 B = n 1 (2) 又由于二叉树中的分支都是由度为1和2的结点发出的,所以有 B = n1 + 2n2 (3) 综合(1)、(2)、(3)式可得 n0 = n2 + 1,性质4 具有n个结点的完全二叉树的深度k为。 证明:根据性质2和完全二叉树的定义可知 2k - 1 n 2k+1 1 即:2k n 2k+1 对不等式取对数有: k k + 1 由于k为整数,所以有 k =,性质5 对于具有n个结点的完全二叉树,如果按照从上(根结点)到下(叶结点)和从左到右的顺序对二叉树中的所有结点从0开始到n-1进行编号,则对于任意的下标为i的结点,有: (1)

6、 如果i=0,则它是根结点,它没有父结点:如果i0,则它的父结点的下标为(i-1)/2; (2) 如果2i1n1,则下标为i的结点的左子结点的下标为2i1;否则,下标为i的结点没有左子结点: (3) 如果2i+2n1,则下标为i的结点的右子结点的下标为2i+2;否则,下标为i的结点没有右子结点。,性质6 在满二叉树中,叶结点的个数比分支结点个数多1。 性质7 在扩充二叉树中,外部结点的个数比内部结点的个数多1。,性质8 对任意扩充二叉树,外部路径长度E和内部路径长度I之间满足以下关系:E = I + 2n,其中n是内部结点个数。,5.1.3 二叉树的抽象数据类型,ADT BinTree is

7、operations BinTree createEmptyBinTree(void)创建一棵空的二叉树。 BinTree consBinTree(BinTreeNode root, BinTree left, BinTree right)返回一棵二叉树,其根结点是root,左右二叉树分别为left和right。 int isNull ( BinTree t )判断二叉树t是否为空。,BinTreeNode root ( BinTree t )返回二叉树t的根结点。若为空二叉树,则返回一特殊值。 BinTreeNode parent (BinTree t , BinTreeNode p )返回

8、结点p的父结点。当指定的结点为根时,返回一个特殊值。 BinTree leftChild ( BinTree t , BinTreeNode p )返回p结点的左子树,当指定结点没有左子树时,返回一个特殊值。 BinTree rightChild ( BinTree t , BinTreeNode p)返回p结点的右子树,当指定结点没有右子树时,返回一个特殊值。 end ADT BinTree 由于二叉树的概念是递归定义的,二叉树中的每个结点也可标识以这个结点为根的二叉树,所以二叉树类型和二叉树中结点类型在具体实现时常常看成是同一种类型。 在关于二叉树的周游等算法中,采用这种观点描述特别方便。

9、,5.2 二叉树的周游,5.2.1什么是周游二叉树的周游是指按某种方式访问二叉树中的所有结点,使每个结点被访问一次且只被访问一次。 深度优先周游 广度优先周游,5.2.2 周游的分类,深度优先周游,若以符号D、L、R分别表示根结点、左子树、右子树,则二叉树的周游共有六种方式:DLR,LDR,LRD,DRL,RDL和RLD。如果限定先左后右,则只能采用前三种周游方式,即DLR,LRD和LDR,它们分别被称为先根次序(简称先序或前序)周游、后根次序(简称后序)周游和中根次序(简称中序)周游(也叫对称序次序周游)。,先根次序:,若二叉树不空,则先访问根;然后按先根次序周游左子树;最后按先根次序周游右

10、子树。 下图所示二叉树,先根次序周游得到的结点序列为:A,B,D,C,E,G,F,H,I,后根次序,若二叉树不空,则先按后根次序周游左子树;然后按后根次序周游右子树;最后访问根。 前图所示的二叉树,按后根次序周游得到的结点序列为:D,B,G,E,H,I,F,C,A,对称(中根)次序,若二叉树不空,则先按对称序周游左子树;然后访问根;最后按对称序周游右子树。 对于前图所示的二叉树,按对称序周游一棵二叉树得到的结点序列为: D,B,A,E,G,C,H,F,I,通常将按先根次序对一棵二叉树周游得到的线性表称为这棵二叉树的先根序列。 通常将按后根次序周游一棵二叉树得到的线性表称为这棵二叉树的后根序列。

11、 通常将按对称次序周游一棵二叉树得到的线性表称为这棵二叉树的对称(中根)序列,对于给定的二叉树,可以唯一确定它的先根序列、后根序列和对称序列。但是反过来,给定一个二叉树的任意一种周游的序列,无法唯一确定这个二叉树。 一般而言,如果已知一个二叉树的对称序列,又知道另外一种周游序列(无论是先根序列还是后根序列),就可以唯一确定这个二叉树。,广度优先周游若二叉树的高度为h,则从0到h逐层如下处理:从左到右逐个访问存在的结点。前图的二叉树,广度优先周游所得到的结点序列为:A,B,C,D,E,F,G,H,I 广度优先周游一棵二叉树所得到的结点序列,叫作这棵二叉树的层次序列。,5.2.4 周游的抽象算法,

12、这里给出的算法,都是建立在上节关于抽象数据类型基本运算的基础上。它们不依赖于二叉树的具体存储结构,所以称为抽象算法。,递归算法,三种深度优先周游的递归算法:,先根次序周游: void preOrder( BinTree t) 中根次序周游: void inOrder(BinTree t) 后根次序周游: void postOrder(BinTree t),非递归算法,利用栈的帮助,可以写出各种深度优先周游的非递归算法。由于本节给出的算法,基本都不涉及具体的存储结构,所以对于栈或者队列的使用,算法中也不依赖于具体的表示方式。读者在实习时,需要经过适当精化后,才能上机运行。,先根次序周游非递归,采

13、用非递归算法实现先根次序周游二叉树的主要思路是:首先把根结点压入栈中;然后从栈顶中取出元素(包括退栈),只要取出元素非空,就访问该结点,然后顺序将其右子结点和左子结点进栈,重复执行上述过程,直到当从栈顶中取出的元素(包括退栈)为空,并且栈也为空时,周游结束。 程序实现:void nPreOrder(BinTree t) 算法中每个二叉树恰好进栈、出栈各一次,所以它的时间代价为O(n),其中n为二叉树中子二叉树(也是结点)的个数。,对称序周游非递归,采用非递归算法实现对称序周游二叉树的主要思路是:若二叉树不为空时,则沿其左子树前进,在前进过程中,把所经过的二叉树逐个压入栈中,当左子树为空时,弹出

14、栈顶元素,并访问该二叉树的根,如果它有右子树,再进入当前二叉树的右子树,从头执行上述过程;如果它没有右子树,则弹出栈顶元素,从前面继续执行。直到当前二叉树为空并且栈也为空时,周游结束。 程序实现:void nInOrder(BinTree t),后根次序周游非递归,与先根次序周游及对称序周游二叉树的方法不同,在后根次序周游的过程中,对一个二叉树的根结点访问之前,要两次经过这个二叉树:首先是由该二叉树找到其左子树,周游其左子树,周游完返回到这个二叉树;然后是由该二叉树找到其右子树,周游其右子树,周游完再次返回到这个二叉树,这时才能访问该二叉树的根结点。,从上面的分析中可以看出,在后根次序周游二叉

15、树时,一个二叉树可能要进、出栈各两次,只有在它第二次出栈之后,才可以访问该二叉树的根结点。为了区分同一二叉树的两次出栈,需要给栈中二叉树增加一标志量tag,定义为:若tag=1,则本二叉树是第一次进栈,所有下次出栈,不能访问本二叉树的根结点;若tag=2,则本二叉树是第二次进栈,所有下次出栈,应该访问本二叉树的根结点。,二叉树进、出栈时,其标志量tag也同时进、出栈,因此,可以把二者合并成一个结构变量,将栈中元素的数据类型设置为下面的形式: typedef struct int tag; /* 标记 */BinTree t; /* 二叉树 */ Elem; 程序实现:void nPostOrd

16、er1(BinTree t) 这里虽然是一个两重循环,但是每个子树进栈和出栈两次,所以时间代价仍然是O(n)。,以上算法可以从两个方面加以改进:一方面是取消标志tag,以节省栈的空间;另一方面是每个二叉树只进栈和出栈一次,以节省执行时间。为此必须在算法中二叉树出栈时增加判断;如果是从栈顶二叉树的左子树回来,就直接进入右子树周游,如果是从栈顶二叉树的右子树回来,就执行出栈,访问该二叉树的根结点。 程序实现:void nPostOrder2(BinTree t),广度优先周游(非递归 ),根据广度优先周游的思想不难想到,可以利用一个队列实现其算法:首先把二叉树送入队列;其后,每当从队首取出一个二叉

17、树访问之后,马上把它的子二叉树按从左到右的次序送入队列尾端;重复此过程直到队列为空。 程序实现:void levelOrder(BinTree t),每个二叉树进队列一次出队列一次,所以时间代价为O(n)。主要空间代价是需要队列的附加空间。若二叉树结点个数为n,最坏的情况出现在完全二叉树时,需要大约n/2个队列元素的空间。,5.3 二叉树的实现,顺序表示 链接表示 线索二叉树,5.3.1 顺序表示,二叉树的顺序表示,也是采用一组连续的存储单元来存放二叉树中的结点,但是,由于二叉树是非线性结构,所以结点之间的逻辑关系难以从存储的先后确定。不过,由二叉树的性质5可知,对于完全二叉树,如果按照从上(

18、根结点)到下(叶结点)和从左到右的顺序,对二叉树中的所有结点从0到n-1编号,这样存放到一维数组中。只要通过数组元素的下标关系,就可以确定二叉树中结点之间的逻辑关系。,完全二叉树及其顺序表示,对于一般的二叉树,如果仍采用顺序表示,首先要对它进行扩充,增加一些并不存在的空结点,使之成为一棵完全二叉树,然后再用一维数组顺序存储。在二叉树中人为增加的空结点,在数组所对应的元素中,可以用一个特殊值表示。,采用顺序存储表示的二叉树,可用下列方式定义:,struct SeqBinTree /* 顺序二叉树类型定义 */int MAXNUM /* 完全二叉树中允许结点的最大个数 */int n; /* 改造

19、成完全二叉树后,结点的实际个数 */DataType *nodelist; /* 存放结点的数组 */ ; typedef struct SeqBinTree *PSeqBinTree; /*顺序二叉树类型的指针类型*/,在上述采用顺序表示的二叉树中,nodelist数组中的每个元素表示二叉树中的一个结点,以这个结点为根的子二叉树的根就是它自己,它的左子树以nodelist2*i+1为根,它的右子树以nodelist2*(i+1)为根,它的父结点则是nodelist(i-1)/2(前提是这些结点存在)。n是扩充成完全二叉树以后结点的实际个数。当二叉树为空二叉树时,n=0。MAXNUM是完全二叉

20、树中允许结点的最大个数,它可以作为数组空间的大小参数提供给二叉树的创建函数使用,也可以在数组溢出时,通过程序扩充空间。,运算的实现,返回下标为p的结点的父结点的下标 int parent_seq(PSeqBinTree t, int p) 返回下标为p的结点的左子结点的下标int leftChild_seq(PSeqBinTree t, int p) 返回下标为p的结点的右子结点的下标 int rightChild_seq (PSeqBinTree t, int p),5.3.2 链接表示,二叉树的链接表示是用一个链表来存储一棵二叉树,二叉树中的每个结点对应链表中的一个结点。由于二叉树是非线性

21、结构,每个结点最多有两个后继,所以最常用的链接表示方式是左-右指针表示法,这种表示法在每个结点中,除了存储结点本身的数据外,再设置两个指针字段:llink和rlink,分别存放结点的左子结点和右子结点的位置,当结点的某个子树为空时,则相应的指针为空指针。,struct BinTreeNode; /* 二叉树中结点 */ typedef struct BinTreeNode * PBinTreeNode; /* 结点的指针类型 */ struct BinTreeNode DataType info; /* 数据域 */PBinTreeNode llink; /* 指向左子结点 */PBinTre

22、eNode rlink; /* 指向右子结点 */ ;,由于递归是二叉树的固有特性,二叉树的许多处理都可以用递归算法来描述,因此,为了运算和参数传递的方便,不再对二叉树进行封装,直接将二叉树定义为指向结点的指针类型: typedef struct BinTreeNode *BinTree; 在实际应用中,将二叉树作为参数传递时,可能需要传递二叉树根结点指针的地址,因此,为了说明方便,可以引入二叉树类型的指针类型: typedef BinTree *PBinTree;,运算的实现,返回结点p的左子结点的地址 PBinTreeNode leftChild_link(PBinTreeNode p)

23、返回结点p的右子结点的地址 PBinTreeNode rightChild_link(PBinTreeNode p),实现求父结点的操作就比较困难,只能从t出发,使用前面介绍的周游算法,通过算法中的访问函数(visit),检查当前结点是否所求结点的父结点。最坏的时间代价与周游整个二叉树的代价相同。为了提高求父结点操作的速度,可以采用的另一种链接表示方式是三叉链表表示,即给二叉树中的每个结点增加一个指向父结点的指针域。采用三叉链表表示,既便于查找子结点,又便于查找父结点,但是相对于左右指针表示而言,它增加了空间开销。,5.3.3 线索二叉树,线索二叉树是对于左右指针表示法的一种修改。它利用结点的

24、空的左指针(llink)存储该结点在某种周游序列中的前驱结点的位置;利用结点的空的右指针(rlink)存储该结点在同种周游序列中的后继结点的位置。这种附加的指向前驱结点和后继结点的指针称作线索,加进了线索的二叉树左右指针表示称作线索二叉树。把二叉树左右指针表示改造成线索二叉树的过程称为线索化。,为区分左右指针和线索,需要在每个结点里增加两个标志位ltag和rtag,令:ltag=0,则llink是指针,指向结点的左子结点;ltag =1,则llink是线索,指向结点的对称序的前驱结点;rtag=0,则rlink是指针,指向结点的右子结点;rtag =1,则rlink是线索,指向结点的对称序的后

25、继结点。 这样,增加了标志域的结点结构为 :,struct ThrTreeNode; /* 线索二叉树中的结点 */ typedef struct ThrTreeNode * PThrTreeNode; /* 指向线索二叉树结点的指针类型 */ struct ThrTreeNode /* 线索二叉树中结点的定义 */DataType info;PThrTreeNode llink, rlink;int ltag, rtag; typedef struct ThrTreeNode * ThrTree;/* 线索二叉树类型的定义 */ typedef ThrTree * PThrTree; /*

26、线索二叉树类型的指针类型 */,按对称序线索化二叉树,在未线索化之前,所有结点的llink和rlink都是指向子结点指针,因此所有ltag和rtag的初始状态都为0。给出一棵二叉树,要将它按对称序线索化,其做法就是按对称序周游此二叉树,在周游的过程中用线索代替空指针。 void thread(ThrTree t),用线索树的最大优点是:由于有了线索的存在,在某些情况下可以很方便地找到指定结点在某种周游序列中的前驱和后继,而不必再对二叉树重新周游。另外,在线索树上进行某种次序周游要比在一般二叉树上进行这种周游容易得多。,按对称序周游对称序线索树,要按对称序周游对称序线索二叉树,算法十分简洁。只要

27、首先找到对称序列中的第一个结点,然后依次找到结点的后继结点,直至其后继结点为空即可。第一个结点也很容易找,只要从根结点出发沿着左指针不断往下走,直至左指针为空,到达“最左下”的结点,这就是对称序第一个结点。找任意结点的对称序后继时,也非常容易做:一个结点的右指针字段如果是线索,则根据定义,它就指向该结点在对称序下的后继;如果不是线索,则它指向该结点右子树的根,而该结点在对称序下的后继应是此右子树的最左下结点。 void nInOrder(ThrTree t ),5.4 二叉树的应用,5.4.1 堆与优先队列 堆的定义:n个元素的序列K=(k0,k1,kn-1) 称为堆,当且仅当满足条件:或 (

28、i=0,1,n/2-1),如果把堆看为一棵完全二叉树的顺序表示,可以有助于我们形象地理解堆的实质。满足条件(1)的堆,在完全二叉树中等价于:每个子二叉树的根均大于等于其左、右子结点;满足条件(2)的堆,在完全二叉树中等价于:每个子二叉树的根均小于等于其左、右子结点。这一特征称之为堆序性。 因此在一个堆中,根结点是最大(或最小)结点。如果堆中根结点最小,则称为小根堆;若堆中根结点最大,则称为大根堆。本节主要讨论小根堆。,优先队列优先队列是一种常见的抽象数据类型,它与第四章介绍的“队列”不同,不遵循“先进先出”的原则,而遵循“最小元素先出”的原则。优先队列的基本操作有三个:向优先队列里插入一个元素

29、(add);在优先队列中找出最小元素(min)和删除优先队列中最小元素(removeMin)。这三个操作代表了优先队列的主要特征。,ADT PriorityQueue is Operations PriorityQueue createEmptyPriQueue( void) 创建一个空优先队列。 int isEmpty(PriorityQueue S) 若S为空,则返回1,否则返回0。 void add(PriorityQueue S, DataType e) 向S中添加元素e。 DataType min(PriorityQueue S) 返回S中的最小元素。 void removeMin(

30、PriorityQueue S) 删除S中的最小元素。 end ADT PriorityQueue,优先队列的实现最常用来表示优先队列的方法是前面介绍的堆。由于堆与二叉树的内在联系,下面表示优先队列的定义与二叉树的顺序表示基本一样:struct PriorityQueue int MAXNUM; /*堆中的元素个数的上限 */int n; /*堆中的实际元素个数*/DataType *pq; /*堆中元素的顺序表示*/ ; /*优先队列类型*/ typedef struct PriorityQueue * PPriorityQueue; /*指向优先队列的指针类型*/,操作实现,插入要把一个新

31、元素加入优先队列,堆里将增加一个元素。根据堆的顺序表示,显然必须有某个元素要放到紧接着原有元素后面的位置,并且还需要保持堆序性。一个解决方法是:先把新元素放在最后位置,然后通过反复比较,必要时交换该结点与对应的父结点,直到堆序性重新被满足(也就是说,直到新结点升到了某一位置,发现它的父结点比它小或者它已经是根)为止。 程序实现:void add_heap(PPriorityQueue papq, DataType x),void add_heap(PPriorityQueue papq, DataType x) /*向优先队列中插入x,保持堆序性*/int i;if (papq-n = MAX

32、NUM) printf(“Full!n“); return; for (i = paqu-n; i 0 /*将x插入*/,删除 删除操作的处理方式与插入时类似,但筛选的方向相反。在最小结点被删后,根结点形成一个空位,这时我们考虑能否把处在堆中最后位置的结点填入这里。由于这样做可能破坏堆序性,所以选择这个元素与根的两个子结点三者中最小的结点填入,选择的结果可能使得原来的空位向叶结点方向传递。如此反复交换,最终到堆中最后结点小于等于空位的两个子结点时,将最后结点填入这个空位。,void removeMin_heap(PPriorityQueue papq) int s, temp ,i, chil

33、d;if (isEmpty_heap(papq) printf(“Empty!n“); return;s = -papq-n; /*先删除,*/temp = papq-pqs; /*把最后元素移到temp*/*从根结点调整papq所指的完全二叉树为堆*/i =0; child = 1;while (child pqchild papq-pqchild + 1) child+; /*选择比较小的子结点*/if (temp papq-pqchild) /*空位向叶结点移动*/ papq-pqi = papq-pqchild; i = child; child = 2 * i + 1; else b

34、reak; /*已经找到适当位置*/papq-pqi = temp; /*把最后元素填入*/ ,在删除操作过程中,由于对每层最多只需要做2次比较,而且循环是从树根到树叶进行的,所以这个删除程序的复杂性也是O(log n)。判断优先队列是否为空和取优先队列中的最小元素都非常容易实现。读者自己不难给出。时间代价均为O(1)。,5.4.2 哈夫曼算法及其应用 哈夫曼树,前面我们介绍了扩充二叉树及其外部路径长度概念。若用表示某扩充二叉树的外部路径长度,则有: 其中:li为从根到第i个外部结点的路径长度,m为外部结点的个数。,如果扩充二叉树中的外部结点都有一定的权值,我们可将这一概念加以推广。设扩充二叉

35、树具有m个带权值的外部结点,那么从根结点到各个外部结点的路径长度与相应结点权值的乘积的和,叫做扩充二叉树的带权的外部路径长度。记作 其中:wi是第i个外部结点的权值,li为从根到第i个外部结点的路径长度,m为外部结点的个数。,假设有一组(无序)实数w1 , w2 , w3 , wm,现要构造一棵以wi(i = 1,2,,m)为权的m个外部结点的扩充的二叉树,使得带权的外部路径长度WPL最小。满足这一要求的扩充二叉树就称为哈夫曼树或最优二叉树。,例如,给出权是2,3,4,11,我们可以构造出不同的扩充二叉树,下页图中所示为其中三种。它们的带权外部路径长度分别为(a) WPL = 111 + 24

36、 + 32 + 33 = 34 (b) WPL = 23 + 34 + 311 + 12 = 53 (c) WPL = 22 + 211 + 23 + 24 = 40,哈夫曼树的构造,Huffman算法的基本思想: (1)由给定的m个权值 w1 , w2 , wm ,构造m棵由空二叉树扩充得到的扩充二叉树T1,T2,Tm。每个Ti (1im)只有一个外部结点(也是根结点),它的权值置为wi;(2)在已经构造的所有扩充二叉树中,选取根结点的权值最小和次最小的两棵,将它们作为左、右子树,构造成一棵新的扩充二叉树,它的根结点(新建立的内部结点)的权值置为其左、右子树根结点权值之和;(3)重复执行步骤

37、(2),每次都使扩充二叉树的个数减少一,当只剩下一棵扩充二叉树时,它便是所要构造的哈夫曼树。,下面以w = 9, 6, 3, 2 为例说明这个算法思想。下图给出了根据这个权值集合逐步构造出一棵哈夫曼树的过程。,数据结构在这里我们介绍一种存储表示,该存储结构是在二叉树的llink和rlink基础上增加一个父结点的指针,并且所有结点顺序存放在一个顺序表中。在顺序表中,每个结点的结构由四部分组成,struct HtNode /* 哈夫曼树结点的结构 */int ww;int parent,llink,rlink; struct HtTree /* 哈夫曼树结构 */int m; /* 外部结点的个数

38、 */int root; /* 哈夫曼树根在数组中的下标 */struct HtNode *ht; /*存放2*m-1个结点的数组 */ ; typedef struct HtTree *PHtTree;/ 哈夫曼树类型的指针类型 */,算法在算法开始时,先按照算法思想的第一步,在数组ht中,由给定的m个权值构造成m棵只有一个外部结点的扩充二叉树;后m - 1个为内部结点,根据算法第二步的思想,在构造过程中逐个确定。根据前面表示的哈夫曼树,可以把哈夫曼算法的思想加以精化,得到下面给出的构造一棵哈夫曼树的算法。 程序实现:PHtTree huffman(int m, int *w),例如,对于一

39、组权值w = 2,3,5,7,11,13,17,19,23,29,31,37,41,按照上述算法构造出的哈夫曼树如图5.20所示,其存储结构的初始状态如图5.21(a) 所示,终结状态如图5.21(b) 所示。,哈夫曼树的应用,1、哈夫曼编码 哈夫曼树可以直接应用于通讯及数据传送中的二进制编码。设: d = d1 ,d2,dn 为需要编码的字符集合。 w = w1 ,w2,wn 为d中各字符出现的频率。现要对d中的字符进行二进制编码,使得:(1) 按给出的编码传输文件时,通迅编码总长最短;(2) 若didj,则di的编码不可能是dj的编码的开始部分(前缀)。满足上述要求的二进制编码称为最优前缀

40、编码。,对于这个问题,可以利用哈夫曼树加以解决:用d1 ,d2,dn作为外部结点,用w1 ,w2,wn作为外部结点的权,构造哈夫曼树。在哈夫曼树中把从每个结点引向其左子结点的边标上二进制数“0”,把从每个结点引向右子结点的边标上二进制数“1”,从根到每个叶结点的路径上的二进制数连接起来,就是这个叶结点所代表字符的最优前缀编码。通常把这种编码称为哈夫曼编码。,例如: d = d1 ,d2,dm w = 2,3,5,7,11,13,17,19,23,29,31,37,41 利用哈夫曼算法构造出如下图所示的哈夫曼树。,从而得到各字符的编码为: d1:1011110, d2:1011111, d3:1

41、01110, d4:10110, d5:0100, d6:0101, d7:1010, d8:000, d9:001, d10:011, d11:100, d12:110, d13:111,解码时也十分容易:只要从二叉树的根结点开始,用需要解码的二进制位串,从头开始与二叉树根结点到子结点边上标的0、1相匹配,确定一条到达树叶结点的路径。一旦到达树叶结点,则译出一个字符。然后再回到根结点,从二进制位串中的下一位开始继续解码。,2、二路归并排序哈夫曼树也可以直接应用于二路归并排序以提高排序的效率。假设现在有m个已经排序的文件 d1 ,d2,dn ,每个文件包含的记录个数对应为 w1 ,w2,wn

42、;可以采用两两合并的方法,把所有文件的记录合到一个大文件中,使这个文件中的记录全部排序。问:采用怎样的合并次序才能使得移动记录个数最少?答案是:按照哈夫曼树的结构从外部结点到根结点逐层进行合并,一定是一种最佳的(但并非唯一的)合并顺序。,5.5 树及其抽象数据类型,树结构在客观世界中是大量存在的。例如一个家族中,A有儿子B,C;B和C分别有儿子D,E,F,G和H;E有儿子I,J。则这个家族的成员及血统关系可用图5.23(a)这样的一棵倒置树来描述。此外,象行政区(国家省县区),组织机构(例如公司处科室),物种分类(门纲类科目种),书籍目录(书章节小节)等等,都可用树来描述。,树在具体应用中的几

43、种不同表现形式 :,5.5.1 基本概念,树是包括()个结点的有穷集合,当非空时满足: (1) 有且仅有一个特别标出的称作根的结点;(2) 除根结点之外,其余结点分为若干个不相交的非空集合T1, T2,,Tm,而这些集合中的每一个又都是树。树T1, T2,,Tm,都称作这个根结点的子树。,空树 父结点、子结点、路径、路径长度 结点的度数 树的度数 无序树、有序树 结点的次序 最左子结点、长子、次子 右兄弟,5.5.2 抽象数据类型,ADT Tree is operations Tree createEmptyTree (void)创建一棵空树。 Tree consTree(Node p ,Tr

44、ee t1, Tree ti)以P为根,t1 ti 为子树 创建一棵树。 int isNull ( Tree t )判断树t是否为空树。,Node root ( Tree t )返回树t的根结点。若为空树,则返回一特殊值。 Node parent (Node p )返回结点p的父结点。当指定的结点为根时,它没有父结点,返回一个特殊值。 Tree leftChild (Tree t )返回树t的长子树。当指定树没有子树时,返回一特殊值。 Tree rightSibling (Tree t )返回树t的右兄弟树。当指定树没有右兄弟子树时,返回一特殊值。 end ADT Tree,5.5.3 树的周

45、游,树的周游是一种按某种方式系统地访问树中的所有结点的过程,它使每个结点都被访问一次且只被访问一次。换句话说,通过一次周游,可使树中所有结点,按照某种(线性)序列进行一次处理。 深度优先周游:先根次序、后根次序 广度优先周游,深度优先周游:先根次序,首先访问根结点;然后从左到右按先根次序周游根结点的每棵子树。 和二叉树的先根次序周游类似。 程序实现: 递 归:void preOrder( Tree t ) 非递归:void nPreOrder ( Tree t ),深度优先周游:后根次序,首先从左到右按后根次序周游根结点的每棵子树;最后访问根结点。 和二叉树的后根次序周游类似。 程序实现: 递

46、 归:void postOrder( Tree t ),广度优先周游,先访问层数为0的结点;然后从左到右逐个访问层数为1的结点;依此类推,直到访问完树中的全部结点。根据广度优先周游树的定义不难想到,可以利用一个队列实现其算法。首先把被周游的树送入队列,其后,每当从队首取出一棵树,访问其根结点之后,马上把它的子树按从左到右的次序送入队列尾端;重复此过程直到队列为空。程序实现:void levelOrder(Tree t),5.6 树的实现,父指针表示法 子表表示法 长子-兄弟表示法,5.6.1 父指针表示法,由树的定义可以知道,树中除根以外的每个结点都有唯一的一个父结点。根据这一特性,可用一组连

47、续的存储空间,即用一个数组存储树中的各个结点。数组中的一个元素为一个结构,其中包括结点本身的信息以及本结点的父结点在数组中的下标,树的这种存储方法称为父指针表示法。,结点的结构可定义为: struct ParTreeNode DataType info; /* 结点中的元素 */int parent; /* 结点的父结点位置 */;树的类型定义为: struct ParTreeint MAXNUM /* 树中最大结点个数 */int n; /* 树中已有结点的个数 */struct ParTreeNode * nodelist; /* 存放树中结点的数组 */; typedef struct

48、ParTree * PParTree; / 树类型的指针类型 */,在这种表示中,求某结点的父结点及其所有的祖先(包括根)的运算是很方便的。但求结点的子结点和兄弟就需要查询整个数组。 另外,这种存储结构中没有表示出结点之间的左右次序,所以无法求树中某个指定结点的最左子结点和右兄弟结点等基本运算。 在实际应用中,如果需要实现这些运算,可以对上述存储结构稍加改进。改进方法是按一种周游次序在数组中存放结点,其中较常见的一种方法是依次存放树的先根序列,如下页图 所示。,这种改进的父指针表示法,充分反映了树中结点的左右次序,任何结点的全部子孙都在该结点之后连续存放。现在我们可以比较方便地实现求某个结点的父母、最左子结点、右兄弟和求树的根等基本运算。教材中给出了求某个结点的右兄弟运算的实现和求某个结点的最左子结点运算的实现。 int rightSibling_partree(PParTree t, int p) int leftChild_partree(PParTree t, int p),父指针表示方法的主要优点是存储空间比较节省,对求某个结点的父母和求某个结点的最左子结点操作都很方便,但是对求某个结点的右兄弟运算比较慢,

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高等教育 > 大学课件

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报