8.3 树
1、树的定义
树结构是一种非常重要的非线性结构,该结构中一个数据元素可以有两个或两个以上的直接后继元素,树可以用来描述客观世界中广泛存在的层次结构关系。
8.3.1 树与二叉树的定义
1、树的定义
树是 n(n≥0)个节点的有限集合,当 n=0 时称为空树。
在任一非空树(n>0)中,有且仅有一个称为根的节点;其余节点可分为 m(m≥0)个互不相交的有限集 T1,T2,...,Tm,其中每个 Ti又都是一棵树,并且称为根节点的子树。
树的定义是递归的,它表明了树本身的固有特性,也就是一棵树由若干棵子树构成,而子树又由更小的子树构成。
该定义只给出了树的组成特点,若从数据结构的逻辑关系角度来看,树中元素之间有明显的层次关系。
对树中的某个节点,它最多只和上一层的一个节点(即其双亲节点)有直接的关系,而与其下一层的多个节点(即其孩子节点)有直接关系,如果 8-16所示。
通常,凡是分等级的分类方案都可以用具有严格层次关系的树结构来描述。
2、树的基本概念
(1)双亲、孩子和兄弟:节点的子树的根称为该节点的孩子;相应地,该节点称为其子节点的双亲。具有相同双亲的节点互为兄弟。
(2)节点的度:一个节点的子树的个数记为该节点的度。
(3)叶子节点:也称为终端节点,指度为 0 的节点。
(4)内部节点:度不为 0 的节点称为分支节点或非终端节点。出根节点之外,分支节点称为内部节点。
(5)节点的层次:根为第一层,根的孩子为第二层,以此类推,若某节点在第 i 层,则其孩子节点在第 i+1 层。
(6)树的高度:一棵树的最大层次树记为树的高度(或深度)。
(7)有序(无序)树:若树中节点的各子树看成是从左到右具有次序的,即不能交换,则称该树为有序树,否则称为无序树。
3、二叉树的定义
二叉树是 n (n≥0)个节点的有限集合,它或者是空树(n = 0),或者是由一个根节点及两棵不相交的且分别成为左、右子树的二叉树所组成。可见,二叉树同样具有递归性质。
特别需要注意的是,尽管树和二叉树的概念之间有许多联系,但它们是两个不同的概念。
树和二叉树之间最主要的区别是:
二叉树节点的子树要区分左子树和右子树,即使在节点只有一颗子树的情况下,也要明确指出该子树是左子树还是右子树。
另外,二叉树节点最大度为2,而树中不限制节点的度数,如图 8-17所示,
8.3.2 二叉树的性质与存储结构
1、二叉树的性质
(1)、二叉树第 i 层(i≥1)上至多有 个节点。
此性质只要对层数 i 进行数学归纳证明即可。
(2)高度为 k 的二叉树至多有 个节点(k≥1)。
由性质 1,每一层的节点数都取最大值 即可。
(3)对任何一棵二叉树,若其终端节点数为 ,度为2的节点数 为,则=+1。
(4)具有 n 个节点的完全二叉树的深度为。
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
(5)对一棵有 n 个节点的完全二叉树的节点按层次自左至右进行编号,则对任一节点 i(1≤i≤n)有:
① 若 i=1 ,则节点 i 二叉树的根,无双亲;若 i >1 ,则其双亲为 。
② 若 2i > n,则节点 i 没有左孩子,否则其左孩子为 2i。
③ 若 2i +1>n,则节点 i 没有右孩子,否则其右孩子为 2i+1;
若深度为k的二叉树有 个节点,则称其为满二叉树。可以对满二叉树中的几点进行连续编号:
约定编号从根节点起,自上而下、自左而右依次进行。
深度为k、有n 个节点的二叉树,当且仅当其每一个节点都与深度为 k的满二叉树中编号从 1 至 n 的节点一一对应时,称之为完全二叉树。满二叉树和完全二叉树的示意图如:8-18所示。
2、二叉树的存储结构
1)二叉树的顺序存储结构
用一组地址连续的存储单元存储二叉树的节点,必须把节点排成一个适当的线性序列,并且节点在这个序列中的相互位置能反映出节点之间的逻辑关系。
对于深度为 k 的完全二叉树,除第 k 层外,其余各层中含有最大的节点数,即每一层的节点数恰为其上一层节点数的两倍,由此从一个节点的编号可推知其双亲、左孩子和右孩子的编号。
假设有编号为 i 的节点,则有:
(1)若 i=1 ,该节点为根节点,无双亲。
(2)若 i>1 ,该节点的双亲节点为(i+1)/2(取整数)。
(3)若 2i≤ n,则该节点的左孩子编号为2i,否则无左孩子。
(4)若 2i+1 ≤ n,则该节点的右孩子编号为 2i+1,否则无右孩子。
(5)若 i 为奇数且不为 1,则该节点左兄弟的编号为 i-1,否则无左兄弟。
(6)若 i 为偶数且小于 n,则该节点右兄弟的编号为i+1,否则无右兄弟。
完全二叉树的顺序存储结构如图 8-19(a)所示。
显然,完全二叉树采用顺序存储结构既简单又节省空间,对于一般的二叉树,则不宜采用顺序存储结构。
因为一般的二叉树也必须按照完全二叉树的形式存储,也就是要添上一些实际并不存在的“虚节点”,这将造成空间的浪费,如图 8-19(b)所示。
最坏情况下,一个深度为 k 且只有 k 个节点的二叉树(单支树)却要 个存储单元。
2)二叉树的链式存储结构
由于二叉树的节点中包含有数据元素、左子树的根、右子树的根及双亲等信息,因此可以用三叉链表或二叉链表(即一个节点含有三个指针或两个指针)来存储二叉树,链表的头指针指向二叉树的根节点,如图 8-20 所示。
设节点中的数据元素为整型,则二叉链表的节点类型定义如下:
typedef struct BiTnode{
int data;
struct BiTnde *lchild,*rchild;
}BiTnode,*BiTree;
在不同的存储结构中,实现二叉树的运算方法也不同,具体应采用什么存储结构,除考虑二叉树的形态还应考虑需要进行的运算特点。
8.3.3 二叉树的遍历
遍历是按某种策略访问树中的每个节点,且仅访问一次的过程。
由于二叉树所具有的的递归性质,一棵非空的二叉树可以看作是由根节点、左子树和右子树三部分构成的,因此若能依次遍历这三部分,也就遍历了整棵二叉树。
按照先遍历左子树后遍历右子树的约定,根据访问根节点位置的不同,可得到二叉树的先序、中序和后序三种遍历方法。
此外,对二叉树还可进行层序遍历。
【函数】二叉树的先序遍历。
【函数】二叉树的中序遍历。
【函数】二叉树的后序遍历。
从树的根节点出发,三种方法的遍历路线如图 8-21所示。
改路线从根节点出发,逆时针沿着二叉树的外缘移动,对每个节点均途径三次。
若第一次经过节点时进行访问,则是先序遍历;若第二次(或第三次)经过节点时访问节点,则是中序遍历(或后序遍历)。
因此,只要将遍历路线上所有第一次、第二次和第三次经过的节点信息分别输出,即可分别得到该二叉树的先序、中序和后序遍历序列。
粗略地讲,若去掉三种遍历算法中的打印 输出语句,则三种遍历方法基本相同。这说明三种遍历过程路线相同。
遍历二叉树的基本操作就是访问节点,不论按照哪种次序遍历,对含有 n 个节点的二叉树,遍历算法的时间复杂度都为Ο(n)。
因为在遍历的过程中,每进行一次递归调用,都是将函数的“ 活动记录 ”压入栈中,因此,栈的最大长度恰为树的高度。
所以,在最坏的情况下,二叉树是有 n 个节点且高度为 n 的单枝树,遍历算法的空间复杂度也为 Ο(n)。
借助于一个栈,可将二叉树的递归遍历算法转换为非递归算法。
下面以中序遍历为例给出中序遍历的非递归算法。
【函数】二叉树的中序非递归遍历算法。
遍历二叉树的过程实质上是按一定规则将树中的节点排成一个线性序列的过程,因此遍历操作得到的是树中节点的一个线性序列。
在每一种序列中,有且仅有一个起始点和一个终节点,其余节点有且仅有唯一的直接前驱和直接后继。显然,关于节点的前驱和后继的讨论是针对某一个遍历序列而言的。
对二叉树还可以进行层序遍历。设二叉树的根节点所在层数为 1 ,层序遍历就是从树的根节点出发,首先访问第 1 层的树根节点,然后从左到右依次访问第二层上的节点,其次是第三层上的节点,依此类推,自上而下、自左至右逐层访问树中各节点的过程就是层序遍历。
【算法】二叉树的层序遍历算法
8.3.4 线索二叉树
1、线索二叉树的定义
二叉树的遍历实质上是对一个非线性结构进行线性化的过程,它使得每个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。
但在二叉链表存储结构中,只能找到一个节点的左、右孩子,而不能直接得到节点在任一遍历序列中的前驱和后继,这些信息只有在遍历的动态过程中才能得到,因此,引入线索二叉树来保存这些动态过程得到的信息。
2、建立线索二叉树
为了保存节点在任一序列中的前驱和后继信息,可以考虑在每个节点中增加两个指针域来存放遍历时得到的前驱和后继信息,这样就可以为以后的访问带来方便。
但增加指针信息会降低存储空间的利用率,因此可考虑采用其他方法。
若 n 个节点的二叉树采用二叉链表作存储结构,则链表中必然有 n+1 个空指针域,可以利用这些空指针域来存放节点的前驱和后继信息。
线索链表的节点结构如下所示。
若二叉树的二叉链表采用以上所示的节点结构,则相应的链表称为线索链表,其中指向节点前驱、后继的指针称为线索。
加上线索的二叉树称为线索二叉树。对二叉树以某种次序遍历使其称为线索二叉树的过程称为线索化。
中序线索二叉树及其存储结构如图 8-22 所示。
那么如何进行线索化呢?按某种次序将二叉树线索化,实质上是在遍历过程中用线索取代空指针。因此,若设指针 p 指向正在访问的节点,则遍历时设立一个指针 pre,使其始终指向刚刚访问过的节点,这样就记下了遍历过程中节点被访问的先后关系。
在遍历的过程中,设指针 p 指向正在访问的节点。
(1)若 p 所指向的节点有空指针域,则将相应的标志域置为 1。
(2)若 pre!=NULL 且 pre 所指节点的 rtag 等于 1,则令 Pre->rchild =p。
(3)若 p 所执行节点的 ltag 等于 1,则令 p->lchild = pre。
(4)使 pre 指向刚刚访问过的节点,即令 pre=p。
需要说明的是,用这种方法得到的线索二叉树,其线索并不完整,也就是说,部分节点的前驱或后继信息还不能从其存储结构中直接得到。
3、访问线索二叉树
如何在线索二叉树中查找节点的前驱和后继呢?
以中序遍历为例,令 p 指向树中的某个节点,查找 p 所指节点的后继节点的方法如下。
(1)若 p->rtag==1,则 p->rchild 即指向其后继节点。
(2)若 p->rtag==0,则 p所指节点的中序后继必须是其右子树中进行中序遍历得到的第一个节点。也就是说,从 p 所指节点的右子树的根节点出发,沿左孩子指针链向下查找,直到找到一个没有左孩子的节点时为止,这个节点就是 p 所指交界点的直接后继节点,也称其为 p 的右子树中 “ 最左下 ”的节点。
令 p 指向中序线索树中的某个节点,则查找 p 所指节点的直接前驱的方法如下。
(1)若 p-> ltag==1,则 p->lchild 即指向其前驱节点。
(2)若 p-> ltag==0,则 p 所指节点的中序前驱必然其坐姿书中进行中序遍历得到的最后一个节点。也就是说,从 p 所指节点的左子树的根节点出发,沿右孩子指针链向下查找,知道找到一个没有右孩子的节点为止,这个节点就是 p 所指交界点的直接前驱节点,也称其为 p 的左子树中“ 最右下 ”的节点。
8.3.5 最优二叉树
1、最优二叉树
最优二叉树又称为哈夫曼树,是一类带权路径长度最短的树。
路径是从树中一个节点到另一个节点之间的通路,路径上的分支数目称为路径长度。
树的路径长度是从树根到每一个叶子之间的路径长度之和。
节点的带权路径长度为从该节点到树根之间的路径长度与该节点带权的乘积。
树的带权路径长度为树中所有叶子节点的带权路径长度之和,记为
其中, n 为带权叶子节点数目,为叶子节点的权值,为叶子节点到根的路径长度。
哈夫曼树是指权值为d 的 n 个叶子节点的二叉树中带权路径长度最小的二叉树。
例如,图 8-23 所示的具有 4 个叶子节点的二叉树,其中以图 8-23(b)所示二叉树带权路径长度最小。
那么如何构造最优二叉树呢?构造最优二叉树的哈夫曼算法如下:
(1)根据给定的 n 个权值{ },构成 n 棵二叉树的集合 F={ },其中每棵树 中只有一个带权为 的根节点,其左右子树均空。
(2)在 F 中选取两棵权值最小的树作为左、右子树构造一棵新的二叉树,置新构造二叉树的根节点的权值为其左、右子树根节点的权值之和。
(3)从 F 中删除这两棵树,同时将新得到的二叉树加入到 F 中。
重复 (2)、(3)步,直到 F 中只含一棵树时为止,这棵树便是最优二叉树(哈夫曼树)。
由此算法可知,以选中的两棵子树构成新的二叉树,谁作为左子树,谁作为右子树,并没有明确。所以具有 n 个叶子节点的权值为 的最优二叉树不唯一,但其 WPL 值是唯一确定的。
当给定了 n 个权值后,构造出的最优二叉树中的节点数目 m 就确定了,即 m = 2×n-1,所以可用一维的结构数组来存储最优二叉树,下面举例说明。
【函数】创建最优二叉树。
2、哈夫曼编码
若对每个字符编制相同长度的二进制码,则称为等长编码。
例如,英文字符集中的 26个字符采用 5 位二进制串表示,按等长编码格式构造一个字符编码表。发送方按照编码表对信息原文进行编码后送出电文,接收方对接收到的二进制代码按每 5 位一组进行分割,通过查字符的编码表即可得到对应字符,实现译码。
等长编码方案的实现方法比较简单,但对通信中的原文进行编码后,所得电文的码串过长,不利于提高通信效率,因此希望缩短码串的总长度。
如果对每个字符设计长度不等的编码,且让电文出现次数较多的采用尽可能短的编码,那么传送的电文码串总长度则可减少。
要设计长度不等的编码,必须满足下面的条件:任一字符的编码都不是另一个字符的编码的前缀,这种编码也称为前缀码。
对给定的字符集 D={}及字符的使用频率 W={},构造其最优前缀码的方法为:以 作为叶子节点, 作为叶子节点的权值,构造出一棵最优二叉树,然后将树中每个节点的左分支标上 0,有分支标上 1,则每个叶子节点代表的字符的编码就是从根到叶子的路径上的 0、1组成的串。最优前缀编码方法如图 8-24所示。
【函数】从每个叶子节点出发追溯到树根,逆向找出最优二叉树中叶子节点的编码。
利用哈夫曼译码的过程为:从根节点出发,按二进制位串中的 0 和 1 确定是进入左分支还是右分支,当到达叶子节点时译出一个字符。若位串未结束,则回溯到根节点接续上述译码过程。
【函数】用最优二叉树进行译码。
8.3.6 树和森林
1、树的存储结构
(1)树的双亲表示法:用一组地址连续的单元存储树的节点,并在每个节点中附设一个指示器,指出其双亲节点在该存储结构中的位置(节点所在数组元素的下标)。
显然,这种表示对于求指定节点的双亲或祖先都十分方便,但对于求指定节点的孩子及后代则需要遍历整个数组,如图:8-25 所示。
(2)树的孩子表示法:在存储结构中用指针指示出节点的每个孩子,由于树中的每个节点的子树数目不尽相同,因此在采用链式存储结构时可以考虑多重链表。因为每个节点的指针数目不好确定,对于定长的节点可依据树的度来设置节点中的指针,显然会造成极大的浪费;若设置节点中的指针数目不相等,则运算时又不方便,为此可以考虑为树中每个节点的孩子建立一个链表,即令每个节点的所有孩子节点构成一个用单链表表示的线性表,则 n 个节点的树具有 n 个单链表。将这 n 个单链表的头指针又排成一个线性表,如图 8-26(a)所示。
也可以将双亲表示法和孩子表示法结合起来,形成树的双亲孩子表示结构,如图 8-26 (b)所示。
(3)孩子兄弟表示法:又称为二叉链表表示法。
在链表的节点中设置两个指针域分别指向该节点的第一个孩子和下一个兄弟,如图 8-27所示。
树的孩子兄弟表示法为实现树、森林与二叉树之间的转换提供了基础,充分利用二叉树的有关算法来实现树及森林的操作,对难于把握规律的树和森立有着重要的现实意义。
2、树和森林的遍历
1)树的遍历
由于树中的每个节点可以有两棵以上的子树,因此遍历的方法有两种:先根遍历和后根遍历。
(1)树的先根遍历。
树的先根遍历是先访问树的根节点,然后以此先根遍历根的各棵子树。对树的先序遍历等同于对转换所得的二叉树的先序遍历。
(2)树的后根遍历。
树的后根遍历是先依次后根遍历的各棵子树,然后访问树根节点。树的后根遍历等同于对转换所得的二叉树进行中序遍历。
2)森林的遍历
按照森林和数的相互递归定义,可以得出森林的两种遍历方法。
(1)先序遍历森林。
若森林非空,访问森林中第一棵树的根节点,先序遍历第一棵子树根节点的子树森林,再先序遍历第一棵树之外剩余的树所构成的森林。
(2)中序遍历森林。若森林非空,先中序遍历森林中第一棵树的子树森林,然后访问第一棵树的根节点,最后中序遍历除第一棵树之外剩余的树所构成的森林。
3、树、森林和二叉树之间的相互转换
树、深林和二叉树之间可以互相进行转换,即任何一个森林或一棵树可以对应一棵二叉树,而任一棵二叉树也能对应到一个森林或一棵树上。
(1)树、森林转换为二叉树。
利用树的孩子兄弟表示法可导出树与二叉树的对应关系,在树的孩子兄弟表示法中,从物理结构上看与二叉树表示法相同,因此就可以用这种同一存储结构的不同解释将一棵树转换为一棵二叉树,如图 8-28 所示。一棵树可转换成唯一一棵二叉树。
由于树根没有兄弟,所以树转换为二叉树后,二叉树的根一定没有右子树。这样,将一个森林转换为一棵二叉树的方法是:先将森林中的每一棵树转换为二叉树,再将第一棵树的根作为转换后的二叉树的根,第一棵树的左子树作为转换后的二叉树根的左子树,第二棵树作为转换后二叉树的右子树,第三棵树作为换后二叉树的右子树的右子树,以此类推,森林就可以转换为一棵二叉树,如图 8-29 所示
2)二叉树转换为树和森林。二叉树可转换为唯一的树或森林,如图 8-30所示。