610 likes | 766 Views
第五章 树. 树形结构的逻辑特征是:有且仅有一个开始结点,可有若干个终端结点,其余的内部结点都有且仅有一个前趋结点,可以有若干个后继结点,也就是说结构中的数据元素间存在着一对多的层次关系。 本章首先简单介绍树的基本概念,然后重点讨论二叉树的逻辑结构、存储结构及其运算,线索二叉树和线索二叉链表以及如何利用线索来实现遍历运算,并分析树、森林与二叉树之间的相互转换问题,最后介绍二叉树和树的典型应用。. 主要内容:. 5 . 1 树的定义 5 . 2 二叉树 二叉树的定义及性质 二叉树的存储 二叉树的遍历及实现算法 5 . 3 线索二叉树
E N D
树形结构的逻辑特征是:有且仅有一个开始结点,可有若干个终端结点,其余的内部结点都有且仅有一个前趋结点,可以有若干个后继结点,也就是说结构中的数据元素间存在着一对多的层次关系。树形结构的逻辑特征是:有且仅有一个开始结点,可有若干个终端结点,其余的内部结点都有且仅有一个前趋结点,可以有若干个后继结点,也就是说结构中的数据元素间存在着一对多的层次关系。 • 本章首先简单介绍树的基本概念,然后重点讨论二叉树的逻辑结构、存储结构及其运算,线索二叉树和线索二叉链表以及如何利用线索来实现遍历运算,并分析树、森林与二叉树之间的相互转换问题,最后介绍二叉树和树的典型应用。
主要内容: • 5.1 树的定义 • 5.2 二叉树 二叉树的定义及性质 二叉树的存储 二叉树的遍历及实现算法 • 5.3 线索二叉树 中序线索二叉树的定义 中序线索二叉树上遍历的实现 利用中序线索实现前序遍历和后序遍历 • 5.4 树和森林 • 5.5 哈夫曼树
5.1 树的定义 一、树的定义 1、树的二元组定义: 设tree=(D,S),其中D是数据元素的集合,S是D中数据元素之间关系的集合。 设关系r∈S,相对r,满足下列条件: (1)D中有且仅有一个开始结点,该结点被称为树的根(Root); (2)除树根结点外,D中其余的结点有且仅有一个前趋结点; (3)从根到其余结点都有路径。 则称tree是相对r的树形结构。
如图所示的树: 该树采用二元组表示如下: 设tree=(D,S),r∈S D={A, B, C, D, E, F, G, H, I} r={<A,B>,<A,C>,<A,D>, <B,E>,<B,F>,<D,G>,<G,H>,<G,I>} 其中A是开始结点,即树的根;除根A外,其余的结 点有且仅有一个前趋结点,但对于后继结点却没有限制,A有三个后继结点B、C和D,B和G分别有两个后继结点,D只有一个后继结点,剩下的结点E、F、C、H、I都没有后继,属于终端结点。
树形结构与线性结构比较: 在线性结构中,有且仅有一个开始结点和一个终端结点,其余的内部结点都有且仅有一个前趋和一个后继。 在树形结构中也是有且仅有一个开始结点(称为根),但终端结点(称为叶子)可以为任意多个,其余的内部结点都有且仅有一个前趋,但可以有任意多个后继。 树形结构中放宽了对结点的后继的限制。线性结构中每个元素的后继最多为一个,而树形结构的后继可以为多个。若树中每个非终端结点的后继刚好为一个时,就是线性表,线性结构是树形结构的一种特殊形式。
2、树的递归定义 树是一种递归的数据结构,也可以用递归的形式来定义树,树的递归定义如下: 树是n(n>0)个结点的有限集合(记作T),它满足两个条件: (1)有且仅有一个特定的称为根的结点; (2)其余的结点可分为m(m≥0)个互不相交的有限集合T1,T2,…,Tm,其中每个集合又是一棵树,并称其为根的子树(Subtree)。
3、树的表示方法 除了前面介绍的树形表示法和二元组表示法外,还有其他三种常用的表示方法:凹入表表示法、嵌套集合表示法和广义表表示法。其中,树形表示法是最常用的表示方法,本书主要采用树形表示法(由于连线的箭头全部向下,所以省略箭头)。
4、树中常用的一些基本术语: (1)与层次相关的术语: 在树中,有且仅有一个开始结点,称为根结点(Root),在树中处于最上层; 除根结点外的其余所有结点都有且仅有一个前趋,每个结点的前趋结点称为该结点的父(双亲)结点(Parent),在树中处于该结点的上一层; 树中的每个结点都可以有若干个后继结点,每个结点的后继点称为该结点的子(孩子或子女)结点(Child),在树中处于该结点的下一层,它们是该结点的子树的根。 没有后继的结点称为叶子结点(Leaf),叶子结点是树的终端结点,可以为多个。 双亲相同的结点互称为兄弟(Silbing),在树中处于同一层。
(2)树中结点之间具有分支: 一个结点的子树的个数,称为该结点的度(Degree),树中度最大的结点的度数称为该树的度。 (3)路径(Path)是树中结点的序列,设结点序列为b1,b2,……,bj,若序列中任意两个相邻结点都满足bi是bi+1的双亲,1≤i≤j-1,则称该结点序列为从b1 到bj的路径。路径长度为序列中结点总数减1,即所经过的边的数目。若树中存在一条从b 到bj的路径,则称b是bj的祖先(Ancestor),而bj是b的子孙(Descendant)。 (4)如果树中所有子树都看成是从左到右的次序排列(子树不能互换),则称此树为有序树(Ordered Tree)。而不考虑子树排列顺序的树,称为无序树(Unordered Tree)。 (5)森林(Forest)是m(m≥0)棵互不相交的树的集合。
5.2 二叉树 一、二叉树的定义及性质 1、二叉树(Binary Tree)是n(n≥0)个结点的有限集合,满足: 当n=0时,为空二叉树。 当n>0时,是由一个根结点和两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成。 在二叉树中,每个结点左子树的根为该结点的左孩子(Left Child),右子树的根为该结点的右孩子(Right Child)。
2、二叉树通常有五种基本形态: 3、二叉树具有以下重要的性质: 性质 1二叉树第i层上最多有2i-1(i≥1)个结点。 性质 2深度为k的二叉树最多有2k-1(i≥1)个结点。 性质 3在任意一棵二叉树中,若叶子结点数为n0,度为2的结点数为n2,则n0=n2+1。
两种特殊的二叉树:满二叉树和完全二叉树 每层结点数都达到最大值的二叉树称为满二叉树(Full Binary Tree)。 除最下面一层外其余各层的结点数都达到最大值,并且最下一层上的结点都集中在最左边的若干位置上,则此二叉树为完全二叉树(Complete Binary Tree)。 (a)满二叉树 (b)完全二叉树 (c)类似完全二叉树 显然,满二叉树是完全二叉树,但完全二叉树不一定是满二叉树。如果将满二叉树的最下层上结点从最右边开始连续删去若干个,就可以得到一棵完全二叉树。
性质 4 具有n个结点的完全二叉树的深度为 或 。 性质 5 对一棵具有n个结点的完全二叉树,按照层次从上到下、每一层从左到右的次序(亦称二叉树的层次次序)将所有结点依次排列,可得到结点的层序序列,其中根结点的序号为1,其余结点的序号连续排列。对任一序号为i的结点(称结点i),有: (1)若结点i有双亲(除根结点外,即i>1),则双亲的序号为i/2。 (2)若结点i有左孩子(即2i≤n时),则左孩子为2i,即任意结点左孩子的序号一定为偶数。 (3)若结点i有右孩子(即2i+1≤n时),则右孩子为2i+1,即任意结点右孩子的序号一定为奇数。 (4)当i为偶数且i≠n时,它有右兄弟,右兄弟为i+1;当i为奇数且i≠1时,它有左兄弟,左兄弟为i-1。
二、二叉树的存储 1.顺序存储 (1)完全二叉树的顺序存储 对一棵具有N个结点的完全二叉树,将所有结点按照从上到下、从左到右的次序依次排列,得到完全二叉树中结点的层序序列,其中根结点的序号为1,然后将这个序列依次存储在长度为N+1的数组bt中(为了保持序号和下标一致,下标为0的单元不用),完全二叉树及其顺序存储结构如图所示。
(2)一般二叉树的顺序存储 一般二叉树与完全二叉树对照,在空缺的位置处添加虚点(用特殊字符,如“#”表示),从而将一般的二叉树扩充成完全二叉树,然后将扩充后的完全二叉树顺序存储。即带着添加的虚点一起将完全二叉树中的结点按层序排列,按照序号将所有结点依次存放在一维数组中,其中根结点的编号为1。
2.链接存储 每个结点除了存储本身的数据外,需要附加两个指针域分别指向该结点的左孩子和右孩子,这就是二叉链表。 有时为了运算方便,每个结点再附加一个指针域存储双亲结点的指针,即带父指针的二叉链表,也称为三叉链表 。 二叉链表是二叉树的链接存储结构中为最常用的一种,其中lchild和rchild分别表示指向结点左、右孩子的指针。
二叉链表存储结构的C语言描述为: typedef char DataType; typedef struct Node { DataType data; struct Node *lchild,*rchild; }BiTree; BiTree *root; /*根结点的指针*/ 二叉树中的每个结点都为BiTree类型,它们之间通过lchild指针和rchild指针联系在一起,可以通过保存根结点的指针root来唯一标识一棵二叉树。
3、二叉链表的建立 算法的实现步骤如下: (1)初始化队列和二叉链表; (2)读取用户输入的结点信息,若不是虚点,则建立一个新结点,同时将结点的地址入队; (3)新结点若为第一个结点,则将此结点的地址存入根指针中;否则,从队头中取出双亲结点的指针,将此结点作为左孩子(或右孩子)链接到双亲结点上,若此结点的编号为偶数则为左孩子,否则为右孩子。当一个结点的两个孩子都已链接完毕,即结点编号为奇数时,队头元素出队,此时的队头元素必定是下一个新结点的双亲结点; (4)重复步骤2~3,直到遇到结束标志符@。
#define MAXSIZE 100 typedef struct /*顺序队列的定义 */ { BiTree * data[MAXSIZE]; int front,rear; }SeQueue; BiTree * createTree() { SeQueue Q,*q=&Q; /* 队列 */ BiTree *root,*s; char ch; root=NULL; /* 置空二叉树 */ q->front =1; q->rear = 0; /* 置空队列,队尾保存新创建结点的指针,队头保存新结点的双亲的指针 */ ch=getchar() ; /* 输入一个字符 */ while(ch!='@') /* 若输入字符不是结束符@,则反复处理 */ { s=NULL; if(ch!='#') /*不是虚点,则创建新结点,若是虚点,s=NULL*/ { s=(BiTree *)malloc(sizeof(BiTree)); s->data=ch; s->lchild=NULL; s->rchild=NULL; } q->rear++; /* 队尾指针后移 */ q->data[q->rear]=s; /* 新结点地址或虚点地址(NULL)入队 */ if(root==NULL) root=s; /* 若新结点为第一个结点,则保存此结点为根结点 */ else { if(q->data[q->front]!=NULL) /* 双亲不是虚点时,将新结点链接在双亲上 */ { if(q->rear%2==0) q->data[q->front]->lchild=s; /* 新结点为左孩子 */ else q->data[q->front]->rchild=s; /* 新结点为右孩子 */ } if(q->rear%2==1) q->front ++; /* 左右孩子处理完毕后,出队*/ } ch=getchar(); /* 输入下一个字符 */ } return root; }
三、二叉树的遍历及实现算法 二叉树的遍历(Traversal)是指沿着某条搜索路径访问二叉树中的每个结点,且每个结点仅访问一次。 “访问”是指对结点的操作,其含义可以很广,如可以是输出结点的信息、修改结点数据等。 二叉树由根结点、左子树、右子树三个基本部分组成,遍历一棵非空二叉树的问题可分解为:访问根结点、遍历左子树和遍历右子树。 若分别用L、D、R分别表示上述三个子问题,则可有6种不同的遍历次序:DLR、LDR、LRD、DRL、RDL、RLD。 约定左子树的访问在右子树之前,则讨论三种遍历方法: DLR:前序遍历(PreOrder LDR:中序遍历(InOrder) LRD:后序遍历(PostOrder)。
二叉树是递归的数据结构,因为它的左、右子树也是二叉树,因此对左、右子树的遍历仍然是遍历二叉树的问题,要用相同的次序来完成。所以,可以用递归的方法来定义二叉树的遍历运算:二叉树是递归的数据结构,因为它的左、右子树也是二叉树,因此对左、右子树的遍历仍然是遍历二叉树的问题,要用相同的次序来完成。所以,可以用递归的方法来定义二叉树的遍历运算: (1)前序遍历二叉树—DLR 若二叉树非空,则: 1)访问根结点; 2)前序遍历左子树; 3)前序遍历右子树。 (2)中序遍历二叉树—LDR 若二叉树非空,则: 1)中序遍历左子树; 2)访问根结点; 3)中序遍历右子树。
(3)后序遍历二叉树—LRD 若二叉树非空,则: 1)后序遍历左子树; 2)后序遍历右子树; 3)访问根结点。
编写递归算法需要把握两个方面内容:递归项和递归出口(亦称终止条件)。这里的递归项比较明显,由于对左、右子树的遍历就是对二叉树的遍历,则遍历左子树和遍历右子树都为递归项。另外,只要二叉树不为空,就可以将其分成根、左子树、右子树三个部分,分别进行访问。但若二叉树为空树,遍历运算就结束了,所以递归出口是二叉树为空。由此,可以得出二叉树遍历的递归算法。编写递归算法需要把握两个方面内容:递归项和递归出口(亦称终止条件)。这里的递归项比较明显,由于对左、右子树的遍历就是对二叉树的遍历,则遍历左子树和遍历右子树都为递归项。另外,只要二叉树不为空,就可以将其分成根、左子树、右子树三个部分,分别进行访问。但若二叉树为空树,遍历运算就结束了,所以递归出口是二叉树为空。由此,可以得出二叉树遍历的递归算法。 void PreOrder(BiTree * T) /* 前序遍历二叉树*/ { if (T) /* 当二叉树非空,进入递归过程;否则,遍历运算结束*/ { printf("%c",T->data); /* 访问根结点 */ PreOrder(T->lchild); /* 前序遍历左子树 */ PreOrder(T->rchild); /* 前序遍历右子树 */ } }
void InOrder (BiTree * T) /* 中序遍历二叉树*/ { if (T) { InOrder (T->lchild); /* 中序遍历左子树 */ printf("%c",T->data); /* 访问根结点 */ InOrder (T->rchild); /* 中序遍历右子树 */ } } void PostOrder (BiTree * T) /* 后序遍历二叉树*/ { if (T) { PostOrder (T->lchild); /* 后序遍历左子树 */ PostOrder (T->rchild); /* 后序遍历右子树 */ printf("%c",T->data); /* 访问根结点 */ } }
二叉树的中序遍历 递归算法的执行过程:
中遍历算法执行时 的系统栈变化 :
仿照系统栈在实现递归过程中的作用,自己定义一个栈将递归算法转化为非递归的算法。仿照系统栈在实现递归过程中的作用,自己定义一个栈将递归算法转化为非递归的算法。 非递归算法的C函数如下: #define MAXSIZE 100 typedef BiTree* SElemType ; typedef struct { SElemType data[MAXSIZE]; /* 顺序栈中保存的是结点指针 */ int top; }SeqStack; void InOrder(BiTree *p) { SeqStack *s; s= initSeqStack(); while(1) { while(p) { push(s,p); p=p->lchild;} if (empty(s)) break; p= top(s); pop(s); printf(“%c”, p->data); p=p->rchild; } }
5.3 线索二叉树 当结点的左指针域为空(无左孩子)时,用它的左指针域存放该点在某种遍历次序下的前趋结点的指针,当结点的右指针域为空(无右孩子)时,用它的右指针域存放该点在某种遍历次序下的后继结点的指针。 简单地说:左空指前趋,右空指后继。 以这种结点构成的二叉链表称为线索二叉链表。 指向前趋或后继的指针称为线索(Thread)。 加上线索的二叉树称为线索二叉树。 以某种次序遍历,使二叉树变为线索二叉树的过程称作线索化。 对每个指针域再附加一个标志,当指针域中存放的是孩子指针时,标志为0;当指针域中存放的是线索时,标志为1。
线索二叉链表存储结构的C语言描述如下: typedef char DataType; typedef struct Node { DataType data; struct Node *lchild,*rchild; int ltag,rtag; }BiThrTree;
一、中序线索二叉树的定义 若线索二叉树的线索中保存的是结点在前序遍历下的前趋和后继的指针,则这种线索称为前序线索;同理,若保存的是中序遍历下的前趋和后继的指针,称为中序线索;若保存的是后序遍历下的前趋和后继的指针,称为后序线索。对应的线索二叉树有前序线索二叉树、中序线索二叉树和后序线索二叉树,线索链表有前序线索链表、中序线索链表和后序线索链表。
若要实现中序线索化的运算,在内存中建立中序线索二叉链表,需要先按照线索树中的结点结构建立二叉链表存储结构,即每个结点的左、右标志域均为0。若要实现中序线索化的运算,在内存中建立中序线索二叉链表,需要先按照线索树中的结点结构建立二叉链表存储结构,即每个结点的左、右标志域均为0。
在这样的存储结构上进行中序线索化,只要按中序遍历二叉链表,在遍历过程中用线索取代空指针即可。设两个指针,一个指针pre始终指向刚刚访问过的结点,一个指针p指向当前正在访问的结点。具体方法是:在这样的存储结构上进行中序线索化,只要按中序遍历二叉链表,在遍历过程中用线索取代空指针即可。设两个指针,一个指针pre始终指向刚刚访问过的结点,一个指针p指向当前正在访问的结点。具体方法是: (1)若结点*p有空指针域,则将相应的标志域置为1; (2)若结点*p有中序前趋结点*pre(pre!=NULL)则: 1)若结点*pre的右标志为1(pre->rtag = =1),则令pre- >rchild为指向其中序后继结点*p的指针(右线索),即pre- >rchild=p; 2)若结点*p的左标志为1(p->ltag= =1),则令p- >lchild为指向其中序前趋结点*pre的指针(左线索),即p- >lchild=pre; (3)将pre指向刚刚访问过的结点*p,即pre=p,保留前趋结点指针。
中序线索化算法的C函数: BiThrTree *pre=NULL; /*全局变量,前趋指针*/ void inthreaded (BiThrTree * p) /* 中序线索化 */ { if(p) { inthreaded(p->lchild); /* 线索化左子树 */ if(p->lchild==NULL) p->ltag=1; /* 根据左右孩子修改当前结点*p的标志域 */ if(p->rchild==NULL) p->rtag=1; if(pre!=NULL) /* 当前结点*p有前趋(只有中序遍历的第一个结点无前趋) */ { if(pre->rtag==1) pre->rchild=p;/* 设置前趋结点*pre的后继 */ if(p->ltag==1) p->lchild=pre; /* 设置当前结点*p的前趋 */ } pre=p; /* 保存刚刚访问的结点指针*/ inthreaded(p->rchild); /* 线索化右子树 */ } }
二、中序线索二叉树上遍历的实现 首先要找到中序遍历的第一个结点(二叉树中最左下点,其左线索为空),然后依次找到该结点的中序后继,直到中序遍历的最后一个结点(其右线索为空),算法结束。 同理,可从中序遍历的最后一个结点出发,依次找该结点的中序前趋,直到中序遍历的第一个结点,算法结束。 在中序线索下查找*p结点的中序后继有两种情况: (1)若*p的右标志为1(p->rtag==1,右子树为空),则p->lchild为右线索,指向*p结点的中序后继; (2)若*p的右标志为0(p->rtag==0,右子树非空),则*p的中序后继为右子树的最左下结点。也就是从*p的右孩子开始,沿左指针往下找,直到找到一个没有左孩子的结点*q(q->ltag= =1),则*q就是*p的中序后继。
中序遍历算法的C函数如下: void InOrderThread(BiThrTree *root) /* 中序线索下的中序遍历 */ { BiThrTree *p; p=root; while(p->ltag==0) p=p->lchild; /* 找中序遍历的第一个结点,二叉树的最左下点 */ while(p) { printf(“%c”,p->data); /* 输出结点 */ if(p->rtag==1) p=p->rchild; /*分两种情况查找结点后继 */ else { p=p->rchild; while(p->ltag==0) p=p->lchild; } } }
三、利用中序线索实现前序遍历和后序遍历 利用中序线索不但能方便地进行中序遍历,还可以方便地进行前序和后序遍历。 如果可以利用中序线索找到每个结点在前序遍历下的前趋或后继,便可以进行前序遍历。由于前序遍历的次序为根结点、左子树、右子树,所以利用中序线索找*p结点前序遍历下的后继的方法为: (1)若*p有左孩子,则左孩子为前序后继; (2)若*p无左孩子但有右孩子,则右孩子为前序后继; (3)若*p既无左孩子也无右孩子,则沿着*p结点的右线索(q->rtag=1)一直向上走,直到找到*q结点,q->rchild不是线索是指针(q->rtag=0),此时*(q->rchild)结点就是*p的前序后继。 同理,根据中序线索可以找到每个结点后序遍历下的前趋,从而进行后序遍历。
利用中序线索进行前序遍历算法的C函数如下:利用中序线索进行前序遍历算法的C函数如下: void PreOrderThread(BiThrTree *root) /* 中序线索下的前序遍历 */ { BiThrTree *p; p=root; /* 查找前序序遍历的第一个结点,根结点 */ while(p) /* 不断找前序后继 */ { printf("%c",p->data); / * 输出结点 */ if(p->ltag==0) p=p->lchild; /* 查找结点前序后继 */ else { while(p&&p->rtag==1) p=p->rchild; if(p) p=p->rchild; } } }
利用中序线索进行后序遍历算法的C函数如下:利用中序线索进行后序遍历算法的C函数如下: void PostOrderThread(BiThrTree *root) /* 中序线索下的后序遍历 */ { BiThrTree *p; p=root; /* 查找后序遍历的最后一个结点,根结点 */ while(p) /* 不断找后序前趋 */ { printf(“%c”,p->data); if(p->rtag==0) p=p->rchild; /*查找结点前趋 */ else { while(p&&p->ltag==1) p=p->lchild; if(p) p=p->lchild; } } }
5.4 树和森林 一、树和森林的遍历 树的先根遍历定义为:若树非空,先访问根结点,然后从左到右依次先根遍历每棵子树。 树的后根遍历定义为:若树非空,先从左到右依次后根遍历每棵子树,最后访问根结点。 显然,树的先根遍历和后根遍历过程都是递归过程。 图中树的先根遍历序列为: A,B,F,D,C,G,E,H。 后根遍历序列为: F,B,D,G,H,E,C,A。
先根遍历森林定义为:若森林非空,则首先先根遍历森林中的第一棵树,然后从左到右依次先根遍历除第一棵树外其它树组成的森林。先根遍历森林定义为:若森林非空,则首先先根遍历森林中的第一棵树,然后从左到右依次先根遍历除第一棵树外其它树组成的森林。 后根遍历森林定义为:若森林非空,则首先后根遍历森林中的第一棵树,然后从左到右依次后根遍历除第一棵树外其它树组成的森林。 同样,森林的先根遍历和后根遍历过程也都是递归过程。 先根序列: A,B,F,D,C,G,E,H,I,J,K,L,M,N,O 后根序列: F,B,D,G,H,E,C, A,J,L,K,I,O,N,
二、森林与二叉树的转换 森林转化为二叉树的定义为: (1)若森林为空,则二叉树为空; (2)若森林不空,则二叉树的根为森林中第一棵树的根;二叉树的左子树为第一棵树去掉根之后的子树森林转换成的二叉树;二叉树的右子树为森林除去第一棵树后剩下的树组成的森林转化成的二叉树。 》》
简单的转换规则: (1)在森林中的所有兄弟之间添加一条连线,所有的根结点认为是兄弟; (2)保留父点与第一个孩子之间的连线,去掉父点与其他孩子之间的连线; (3)将此时树中的水平连线和垂直连线顺时针旋转45°,整理成二叉树中的左右子女。
二叉树也可以转化为森林。其转换规则为: (1)若二叉树为空,则对应的森林为空; (2)若二叉树不空,则森林中第一棵树的根即为二叉树的根,第一棵树根的各个子树为二叉树的左子树对应的森林;森林中其余的树为二叉树的右子树对应的森林。
三、树的存储 1、双亲表示法 用一组连续的存储空间(一维数组)存储树中的各个结点,数组中的一个元素存储树中的一个结点,数组元素为结构体类型,其中包括结点本身的信息以及结点的双亲在数组中的序号,树的这种存储方法称为双亲表示法。
2. 孩子链表表示法 将结点的所有孩子用链接方式存储在一个单链表中,没有孩子的结点后面的单链表为空。树中结点用顺序方式存储在一个长度为n(n为树中结点数)的数组中,数组的每一个元素由两个域组成,一个域用来存放结点信息,另一个用来存放该结点的孩子链表的头指针。单链表的结构也由两个域组成,一个存放孩子结点在一维数组中的序号,另一个是指针域,指向下一个孩子结点。
3. 树的二叉链表(孩子——兄弟链)表示法 实际上就是将树转换为对应的二叉树,然后再采用二叉链表存储结构。将树转换为对应的二叉树后,树中每个结点的第一个孩子是对应二叉树中该结点的左孩子,而每个结点的右兄弟对应二叉树中该结点的右孩子,所以该方法又称孩子—兄弟链表示法。每个结点除其信息域外,再增加两个指针域,分别指向该结点的第一个孩子结点和下一个兄弟结点。
5.5 哈夫曼树 一、哈夫曼树的定义及建立 结点的路径长度就是从根结点到每个结点的路径长度,其值为路经上的结点数减1。 赋予了权值的结点的路径长度与该结点权值的乘积即为结点的带权路径长度(Weighted Path Length of Node)。 树的带权路径长度(Weighted Path Length of Tree,缩写为WPL)定义为:树中所有叶子结点的带权路径长度之和,记为: 其中n表示叶子结点数,wi表示第i个叶子结点的权值,li代表第i个叶子结点的路径长度。
WPLa=2×2+3×2+6×2+8×2=38 WPLb=8×1+6×2+2×3+3×3=35 WPLc=2×1+6×3+8×3+3×2=50 WPLd=8×1+2×3+3×3+6×2=35 哈夫曼树(Huffman Tree)又称最优二叉树,是在含有n个叶子结点,权值分别为w1,w2,……,wn的所有二叉树中,带权路径长度WPL最小的二叉树。
哈夫曼(D.A.Huffman)在上世纪五十年代初便提出了一个非常简单的算法来建立哈夫曼树,其算法描述如下:哈夫曼(D.A.Huffman)在上世纪五十年代初便提出了一个非常简单的算法来建立哈夫曼树,其算法描述如下: (1)将给定的n个权值{w1,w2,...,wn}作为n个根结点的权值,构造一个具有n棵二叉树的森林{T1,T2,...,Tn},其中每棵二叉树只有一个根结点; (2)在森林中选取两棵根点权值最小的二叉树分别作为左、右子树,增加一个新结点作为根,从而将两棵树合并成一棵树,新根结点的权值为左右子树根结点权值之和。森林中因此也减少了一棵树; (3)重复上面步骤(2)的处理过程,直到森林中只有一棵二叉树为止,这棵二叉树就是哈夫曼树。