一、树的定义
1、树是一种一对多的数据结构。树是n(n>=0)个结点的有限集,当n=0时称为空树。在任意一棵非空树中:
- 有且仅有一个特定的称为根(root)的结点;
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、......、Tm,其中每一个集合本身又是一棵树,并且称为根的子树,如下图。结点B、D、E、F组成的树和结点C、G、H组成的树就是结点A的子树;结点D、E、F组成的树又是结点B的子树。
注意:当n>0时,根结点(root)是唯一的,不可能存在多个根结点(root);当m>0时,子树的个数没有限制,但它们一定是互不相交的。如下图所示的两个结构就不符合树的定义,因为它们都有相交的子树。
2、结点分类:树的结点包含一个数据元素以及若干指向其子树的分支。结点拥有的子树数目称为结点的度。度为0的结点称为叶结点或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。如下图所示,此树的度就为3。
3、结点间关系:结点的子树的根称为该结点的孩子。相应地,该结点称为孩子的双亲。同一个双亲的孩子之间互称为兄弟。结点的祖先是从根到该结点所经分支上的所有结点。如下图所示对于E来说,B、A都是它的祖先。反之,以某结点为根的子树中的任一结点都称为该结点的子孙,如下图所示B的子孙有D、E、F。
4、结点的层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第L层,则其子树的根就在第L+1层。其双亲在同一层的结点互为堂兄弟。树中结点的最大层次称为树的深度或高度。下图树的深度即为3。
5、其它概念:如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。森林是m(m>=0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。
二、树的存储结构之双亲表示法
1、双亲表示法:假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示其双亲结点在数组中的位置的元素;也就是说,每个结点除了知道自己之外,还知道它的双亲。如下图,其中data是数据域,存储结点的数据信息,而parent存储该结点的双亲在数组中的下标。
2、双亲表示法的结点结构定义代码如下:
// 树的双亲表示法结点结构定义
#define MAX_TREE_SIZE 100
typedef int ElemType; // 树结点的数据类型
// 结点结构
typedef struct Node
{
ElemType data; // 结点数据
int parent; // 双亲位置
}Node;
// 树结构
typedef struct
{
Node nodes[MAX_TREE_SIZE]; // 结点数组
int r, n; // r:根的位置,n:结点数目
}Tree;
3、由于根结点是没有双亲的,所以约定根结点的parent设置为-1,如下图所示是采用双亲表示法表示的树结构。
4、采用上面的表示法,这样的存储结构就可以根据结点的parent指针找到它的双亲结点,所用的时间复杂度为O(1),当parent为-1时,表示找到了树结点的根。但是要知道结点的孩子是什么,则要遍历整个结构才行。所以可以增加一个结点最左边孩子的域,如果结点没有孩子,则此域就设置为-1。如下图,对于有0个或1个孩子的结点,这样的结构解决了找结点孩子的问题,但是对于有1个以上孩子的结点,这样的结构还是只能快速找到该结点的第一个左孩子,要找到其他孩子还是需要遍历。当然可以为结点的每个孩子设置一个域来指向其孩子,但是需要首先知道树的度,也就是要知道需要设置多少个域才能存储结点的孩子;就算知道了这个度,因为每个结点的孩子个数不一样,所以这样会造成很大的空间浪费。
5、如果要查找结点的兄弟结点,则可以增加一个右兄弟域,即一个结点如果它存在右兄弟,则记录下右兄弟的下标,没有,则赋值为-1,如下图。
总之存储结构的设计非常的灵活。
三、树的存储结构之孩子表示法
1、由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,把这种方法叫做多重链表表示法。
2、如果采用多重链表表示法,由于树的每个结点的度,也就是它的孩子个数是不同的,所以可以采用以下两种方案来解决:
- 一种是指针域的个数就等于树的度,因为树的度是树各个结点度的最大值。如下图,此结构对于树中各结点的度相差很大时,是很浪费空间的,因为有很多的结点,它的指针域都是空的。
- 第二种是每个结点指针域的个数等于该结点的度,专门取一个位置来存储结点指针域的个数,如下图,其中data为数据域,degree为度域,也就是存储该结点的孩子结点的个数,剩下的就是指针域,指向该结点的各个孩子的结点(可变个数)。此种方式克服了空间浪费的缺点,但是由于各个结点的结构是不相同的结构(因为孩子数不一样,指向孩子的指针域个数就不同),加上要维护结点的度的数值,所以在运算上还会带来时间的损耗,这就是以时间换空间。
3、从以上的两种多重链表表示法来看都不太理想。为了要遍历整棵树,把每个结点放到一个顺序存储结构的数组中是合理的,但这样每个结点的孩子是不确定的,所以再对每个结点的孩子建立一个单链表来体现它们的关系,这就是孩子表示法,具体办法:把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。如下图。
4、采用孩子表示法,则需要设计两种结点结构:
- 一个是孩子链表的孩子结点,如下图,其中child是数据域,用来存储某个结点在表头数组中的下标;next是指针域,用来存储指向某结点的下一个孩子结点的指针。
- 另一个是表头数组的表头结点,如下图,其中data是数据域,存储某结点的数据信息;firstchild是头指针域,存储该结点的孩子链表的头指针。
5、采用孩子表示法的结构定义代码如下:
// 树的孩子表示法结构定义
#define MAX_TREE_SIZE 100
typedef char ElemType;
// 孩子结点
typedef struct CTNode
{
int child; // 孩子结点在数组中的下标
struct CTNode *next; // 指向下一个孩子结点的指针
}CTNode;
// 表头结构
typedef struct CTBox
{
ElemType data; // 存储树结点的数据
CTNode *firstChild; // 指向第一个孩子的指针
}CTBox;
// 树结构
typedef struct
{
CTBox nodes[MAX_TREE_SIZE]; // 结点数组
int r, n; // r:根的位置,n:结点数目
}Tree;
6、采用以上的孩子表示法对于查找某结点的孩子、找某结点的兄弟只需查找这个结点的孩子单链表即可;对于遍历整棵树只需对头结点的数组循环即可。但是如果要知道某结点的双亲,需要整棵树遍历才行,此时可以把双亲表示法和孩子表示法综合起来,如下图。把这种方法称为双亲孩子表示法。
四、树的存储结构之孩子兄弟表示法
1、任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。所以设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。结点结构如下图。其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。
2、树的孩子兄弟表示法结构定义代码:
typedef int ElemType;
// 树的孩子兄弟表示法结构定义
type struct CSNode
{
ElemType data;
struct CSNode *firstchild, *rightsib;
}CSNode;
3、采用孩子兄弟表示法的结构如下图。
4、采用此种表示法有利于查找某个结点的某个孩子,对于查找某个结点的双亲是有缺陷的,此时可以再增加一个parent指针域来解决快速查找双亲的问题。
5、此表示法最大好处是把一颗复杂的树变成了一棵二叉树,把上图稍微调整一下如下图。
五、二叉树的定义
1、二叉树的定义:二叉树(Binary Tree)是n(n>=0)个结点的有限集合。该集合或者为空集(称为空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。如下图。
2、二叉树的特点:
- 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
- 左子树和右子树是有顺序的,次序不能任意颠倒。即使树中某结点只有一颗子树,也要区分它是左子树还是右子树。如下图所示两棵树是同一棵树,但却是不同的二叉树。
3、二叉树具有五种基本形态:
- 空二叉树。
- 只有一个根结点。
- 根结点只有左子树。
- 根结点只有右子树。
- 根结点既有左子树又有右子树。
4、若只从形态上考虑,三个结点的树只有两种情况:两层或者三层,如下图的第一种和后面四种的任意一种;而对于二叉树来说,因为要区分左右,所以三个结点的二叉树有下面的五种形态。
六、特殊二叉树
1、斜树:所有的结点都只有左子树的二叉树叫左斜树;所有的结点都只有右子树的二叉树叫右斜树。两者统称为斜树。所以斜树的每一层都只有一个结点,结点的个数与二叉树的深度相同。线性表结构就可以理解为是树的一种极其特殊的表现形式。上图的第二种就是左斜树,第三种就是右斜树。
2、满二叉树:在一棵二叉树中,如果所有分支结点都存在左子树和右子树,且所有叶子都在同一层上,这样的二叉树称为满二叉树。如下图。
满二叉树的特点:
- 叶子只能出现在最下一层,出现在其他层就不可能达成整棵树的平衡。
- 非叶子结点的度一定是2。
- 在同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多。
3、完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。如下图。
首先,满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满二叉树的。其次,完全二叉树的所有结点与同样深度的满二叉树,它们按层序编号相同的结点是一一对应的。如下图的树的E结点没有左子树(红色结点和线条是不存在的,画出来只是为了便于区分),却有右子树,所以使得按层序编号的编号J空档了,所以它不是完全二叉树。而上图中的第一棵树虽不是满二叉树,但是编号是连续的,所以它是完全二叉树。
完全二叉树的特点:
- 叶子结点只能出现在最下两层。
- 最下层的叶子一定集中在左部连续位置。
- 倒数二层,若有叶子结点,一定都在右部连续位置。
- 如果结点度为1,则该结点只有左孩子,即不存在只有右子树的情况。
- 同样结点数的二叉树,完全二叉树的深度最小。
判断某二叉树是否是完全二叉树的办法:对树的示意图,给每个结点按照满二叉树的结构逐层顺序编号,如果编号出现空档,就说明不是完全二叉树,否则就是。
七、二叉树的性质
1、二叉树性质一:在二叉树的第i层上最多有2i-1个结点(i>=1)。
2、二叉树性质二:深度为k的二叉树最多有2k-1个结点(k>=1)。
3、二叉树性质三:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。推导:首先再假设度为1的结点数为n1,则二叉树T的结点总数n=n0+n1+n2,其次发现连接数总是等于总结点数n-1,并且等于n1+2*n2,所以n-1=n1+2*n2,所以n0+n1+n2-1=n1+n2+n2,最后n0=n2+1。
4、二叉树性质四:具有n个结点的完全二叉树的深度为⌊log2n⌋+1(⌊x⌋表示不大于x的最大整数,反过来表示大于x的最小整数)。推导:由满二叉树的定义结合性质二可以发现,深度为k的满二叉树的结点数n一定是2k-1;那么对于满二叉树可以通过n=2k-1倒推得到满二叉树的深度为k=log₂(n+1)。由于完全二叉树它的叶子结点只会出现在最下面的两层,可以同样如下推导:那么对于倒数第二层及以上的满二叉树的结点数回推出它的结点数为n=2(k-1)-1。所以完全二叉树的结点数的取值范围是:2(k-1)-1 < n <= 2k-1。由于n是整数,n <=2k-1可以看成n <2k。同理2(k-1)-1 < n可以看成2(k-1) <= n。所以2(k-1) <= n < 2k。不等式两边同时取对数,得到k-1<=log₂n<k。由于k是深度,必须取整,所以k=⌊log₂n⌋+1。
5、二叉树性质五:如果对一棵有n个结点的完全二叉树的结点按层序编号,对任一结点i(1<=i<=n)有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点⌊i/2⌋。
- 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
- 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
八、二叉树的存储结构之顺序存储结构
1、顺序存储对树这种一对多的关系结构实现起来比较困难,但是由于二叉树是一种特殊的树,使得用顺序存储结构也可以实现。
2、二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,且结点的存储位置,即数组的下标要能体现结点之间的逻辑关系,如双亲与孩子的关系等。
3、存储一棵完全二叉树如下图,相应的下标对应其同样的位置。这能看出完全二叉树的优越性。由于其严格定义,在数组直接能表现出逻辑结构。
但是对于一般的二叉树,层序编号不能反映逻辑关系,但可以将其按完全二叉树编号,只不过把不存在的结点设置为“^”,如下图。注意其中画红色边框的结点表示不存在,是虚构出来的。
对于极端的情况,一棵深度为k的右斜树,它只有k个结点,却需要分配2k-1个存储单元空间,很浪费空间,如下图,所以顺序存储结构一般只用于完全二叉树。
九、二叉树的存储结构之二叉链表
1、二叉链表:二叉树每个结点最多有两个孩子,所以用一个数据域和两个指针域来设计,如下图,其中data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针,称这样的链表叫做二叉链表。
2、二叉树的二叉链表结点结构定义代码:
// 二叉树的二叉链表结点结构定义
typedef char ElemType;
// 结点结构
typedef struct BiTNode
{
ElemType data; // 结点数据
struct BiTNode *lchild, *rchild; // 左右孩子指针
}BiTNode;
3、二叉树的二叉链表结构示意图如下。
4、如果有需要还可以增加一个指向其双亲的指针域,那样就称之为三叉链表。
十、遍历二叉树
1、二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
2、二叉树的遍历方式很多,如果是限制了从左到右的方式,主要有以下四种:
- 前序遍历:规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。如下图的二叉树前序遍历的顺序为:ABDGHCEIF。
- 中序遍历:规则是若树为空,则空操作返回。否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后再访问根结点,最后中序遍历右子树。如下图的二叉树中序遍历顺序为:GDHBAEICF。
- 后序遍历:规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。如下图二叉树的后序遍历顺序为GHDBIEFCA。
- 层序遍历:规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。如下图二叉树层序遍历的顺序为ABCDEFGHI。
3、二叉树采用二叉链表的存储结构,前序遍历代码如下:
/* 初始条件: 二叉树T存在 */
/* 操作结果: 前序递归遍历T */
void PreOrderTraverse(BiTree T)
{
if(T == NULL)
{
return;
}
printf("%c", T->data); /* 显示结点数据,可以更改为其它对结点操作 */
PreOrderTraverse(T->lchild); /* 再先序遍历左子树 */
PreOrderTraverse(T->rchild); /* 最后先序遍历右子树 */
}
4、二叉树采用二叉链表的存储结构,中序遍历代码如下:
/* 初始条件: 二叉树T存在 */
/* 操作结果: 中序递归遍历T */
void InOrderTraverse(BiTree T)
{
if(T == NULL)
{
return;
}
InOrderTraverse(T->lchild); /* 中序遍历左子树 */
printf("%c", T->data); /* 显示结点数据,可以更改为其它对结点操作 */
InOrderTraverse(T->rchild); /* 最后中序遍历右子树 */
}
5、二叉树采用二叉链表的存储结构,后序遍历代码如下:
/* 初始条件: 二叉树T存在 */
/* 操作结果: 后序递归遍历T */
void PostOrderTraverse(BiTree T)
{
if(T == NULL)
{
return;
}
PostOrderTraverse(T->lchild); /* 先后序遍历左子树 */
PostOrderTraverse(T->rchild); /* 再后序遍历右子树 */
printf("%c", T->data); /* 显示结点数据,可以更改为其它对结点操作 */
}
6、推导遍历结果:
1)已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
2)已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。
3)已知前序遍历序列和后序遍历序列,不可以唯一确定一棵二叉树。
十一、二叉树的建立
1、如下图所示,采用二叉链表作为其存储结构,为了让每个结点确认是否有左右孩子,将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,如“#”。称这种处理后的二叉树为原二叉树的扩展二叉树。扩展二叉树就可以做到一个遍历序列确定一棵二叉树。
2、采用二叉链表构建二叉树,其中假设二叉树的结点元素值均为一个字符,按扩展前序遍历序列输入每个结点元素值,代码如下:
/* 按前序输入二叉树中结点的值(一个字符) */
/* #表示空树,构造二叉链表表示二叉树T */
void CreateBiTree(BiTree *T)
{
TElemType ch;
scanf("%c", &ch);
if(ch == '#')
{
*T = NULL;
}
else
{
*T = (BiTree)malloc(sizeof(struct BiTNode));
if(!*T)
{
exit(OVERFLOW);
}
(*T)->data = ch; /* 生成根结点 */
CreateBiTree(&(*T)->lchild); /* 构造左子树 */
CreateBiTree(&(*T)->rchild); /* 构造右子树 */
}
}
3、但是由于中序遍历不能首先建立根结点,用加“#”的方法是不可能创建一个二叉树的。由扩展后序遍历序列是可以构建出二叉树的,不过不能向前序那样一边输入一边构建,而是先用一个字符数组将扩展后序序列保存下来,然后从最后一位开始向前构建,代码如下:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#define MAXSIZE 100
int index;
char String[MAXSIZE];
typedef char TElemType;
struct BiTNode
{
TElemType data;
struct BiTNode *lchild, *rchild;
};
typedef struct BiTNode *BiTree;
void CreateBiTree(BiTree *T)
{
TElemType ch = String[--index];
if(ch == '#')
{
*T = NULL;
}
else
{
*T = (BiTree)malloc(sizeof(struct BiTNode));
if(!*T)
{
exit(OVERFLOW);
}
(*T)->data = ch;
CreateBiTree( &(*T)->rchild );
CreateBiTree( &(*T)->lchild );
}
}
int main(void)
{
BiTree T;
if(scanf("%s", String) != EOF)
{
index = strlen(String);
CreateBiTree(&T);
}
return 0;
}
十二、线索二叉树
1、计算采用二叉链表存储的二叉树的空指针域个数:对于一个有n个结点的二叉链表,每个结点有指向左右孩子的两个指针域,所以一共有2n个指针域;而n个结点的二叉树一共有n-1条分支线数,所以存在的空指针域个数为:2n-(n-1) = n + 1个。
2、在二叉链表中利用空指针域存放指向结点在某种遍历次序下的前驱和后继结点的地址,把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树。如下图是进行中序遍历后将所有的空指针域中的rchild改为指向它的后继结点。
3、如上图所示是修改了rchild,同理可以修改lchild指向它的前驱结点。下图是用空心箭头实线表示指向其前驱,虚线黑箭头表示指向其后继表示上图中二叉树的线索二叉树,可以发现其实线索二叉树等于是把一棵二叉树转变成了一个双向链表。对二叉树以某种次序遍历使其变为线索二叉树的过程称做是线索化。
4、在线索化后,有些指针域是指向的其前驱或后继,而有些指针域是指向的其左右孩子,为了区分到底是指向的是前、后驱还是左右孩子,在每个结点再增设两个标识域ltag和rtag,注意ltag和rtag只是存放0或1数字的布尔型变量,其占用的内存空间要小于像lchild和rchild的指针变量。结点结构如下图,其中ltag为0时指向该结点的左孩子,为1时指向该结点的前驱;rtag为0时指向该结点的右孩子,为1时指向该结点的后继。
5、二叉树的线索存储结构定义代码:
/* 二叉树的二叉线索存储结构定义 */
typedef char TElemType;
typedef enum {Link, Thread} PointerTag; /* Link==0表示指向左右孩子指针, */
/* Thread==1表示指向前驱或后继的线索 */
/* 二叉线索存储结点结构 */
struct BiThrNode
{
TElemType data; /* 结点数据 */
struct BiThrNode *lchild, *rchild; /* 左右孩子指针 */
PointerTag LTag;
PointerTag RTag; /* 左右标志 */
};
typedef struct BiThrNode *BiThrTree;
6、线索化的实质是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
7、中序遍历线索化的递归函数代码如下:可以发现和二叉树中序遍历的递归代码类似,指是将本是打印结点的功能改成了线索化的功能。
BiThrTree pre; /* 全局变量,始终指向刚刚访问过的结点 */
/* 中序遍历进行中序线索化 */
void InThreading(BiThrTree p)
{
if(p)
{
InThreading(p->lchild); /* 递归左子树线索化 */
if(!p->lchild) /* 没有左孩子 */
{
p->LTag = Thread; /* 前驱线索 */
p->lchild = pre; /* 左孩子指针指向前驱 */
}
if(!pre->rchild) /* 前驱没有右孩子 */
{
pre->RTag = Thread; /* 后继线索 */
pre->rchild = p; /* 前驱右孩子指针指向后继(当前结点p) */
}
pre = p; /* 保持pre指向p的前驱 */
InThreading(p->rchild); /* 递归右子树线索化 */
}
}
8、对线索二叉树进行遍历其实就等于是操作一个双向链表结构,如下图所示是在二叉树线索链表上添加一个头结点,并令其lchild域的指针指向二叉树的根结点(①),其rchild域的指针指向中序遍历时访问的最后一个结点(②)。反之,令二叉树的中序序列中的第一个结点的lchild域指针和最后一个结点的rchild域指针指向头结点(③和④),这样的好处是既可以从第一个结点起顺着后继进行遍历,也可以从最后一个结点起顺着前驱进行遍历。
9、采用上述存储结构,遍历代码如下:此段代码等于是一个链表的扫描,所以其时间复杂度为O(n)。
/* 中序遍历二叉线索链表表示的二叉树T */
/* T指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。*/
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p = T->lchild; /* p指向根结点 */
while(p != T) /* 空树或遍历结束时,p == T */
{
while(p->LTag == Link) /* 当LTag==0时循环到中序序列第一个结点 */
{
p = p->lchild;
}
printf("%c", p->data); /* 显示结点数据 */
while(p->RTag == Thread && p->rchild != T)
{
p = p->rchild;
printf("%c", p->data);
}
p = p->rchild; /* p进至其右子树根 */
}
return OK;
}
10、在实际中,如果所用的二叉树需要经常遍历或查找结点时需要某种遍历序列中的前驱或后继,则采用线索二叉链表的存储结构很合适。
十三、树、森林与二叉树的转换
1、树转换为二叉树的步骤:
1)加线:在所有兄弟结点之间加一条连线。
2)去线:对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
3)层次调整:以树的根结点轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子。如下图。
2、森林转换为二叉树步骤:
1)把每个树转换为二叉树。
2)第一棵二叉树不动,从第二颗二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到了由森林转换来的二叉树。如下图是将森林的三棵树转化为一棵二叉树。
3、二叉树转换为树的步骤(即树转换为二叉树的逆过程):
1)加线:若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点.......,反正就是左孩子的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
2)去线:删除原二叉树中所有结点与其右孩子结点的连线。
3)层次调整:使之结构层次分明。如下图。
4、判断一棵二叉树能够转换成一棵树还是森林,只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树。
5、二叉树转换为森林步骤:
1)从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除......,直到所有右孩子连线都删除为止,得到分离的二叉树。
2)再将每棵分离后的二叉树转换为树即可。如下图。
十四、树与森林的遍历
1、树的遍历分为两种方式:
1)一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。
2)另一种是后根遍历,即先依次后根遍历每棵子树,然后在访问根结点。如下图所示,它的先根遍历序列为ABEFCDG,后根遍历序列为EFBCGDA。
2、森林的遍历分为两种方式:
1)前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样的方式遍历除去第一棵树的剩余树构成的森林。如十三.5二叉树转森林中三棵树的森林,前序遍历序列为ABCDEFGHJI。
2)后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次用同样方式遍历除去第一棵树的剩余树构成的森林。如十三.5二叉树转森林中三棵树的森林,后续遍历序列为BCDAFEJHIG。
3、通过对十三.5二叉树转森林中的那棵二叉树做前序和中序遍历,然后再对它转换后的森林做前序和后序遍历,可以发现森林的前序遍历和二叉树的前序遍历结果相同,森林的后续遍历和二叉树的中序遍历结果相同。所以当以二叉链表作为树的存储结构时,树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。这也证明了找到了对树和森林这种复杂问题的简单解决办法。
十五、赫夫曼树及其应用
1、从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。如下图的叶子结点带权的二叉树a中,根结点到结点D的路径长度为4,二叉树b中根结点到结点D的路径长度为2。
2、树的路径长度就是从树根到每一结点的路径长度之和。如上图所示其二叉树a的路径长度为1+1+2+2+3+3+4+4=20。
3、如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。
4、假设有n个权值{w1,w2,......,wn},构造一棵有n个叶子结点的二叉树,每个叶子结点带权wk,每个叶子的路径长度为lk,则其中带权路径长度WPL最小的二叉树称做赫夫曼树。有时也称做最优二叉树。如上图二叉树a的WPL=5x1+15x2+40x3+30x4+10x4=315。
5、构造赫夫曼树:(采用上图的二叉树a中的叶子结点和权值作为例子)
1)先把有权值的叶子结点按照权值从小到大的顺序排列成一个有序序列,即:A5、E10、B15、D30、C40。
2)取头两个最小权值的结点作为一个新结点N1的两个子结点,注意相对较小的是左孩子,如下图所示,其中新结点N1的权值为两个叶子权值的和5+10=15。
3)将N1替换A与E,插入有序序列中,保持从小到大排列,即:N115、B15、D30、C40。
4)重复步骤2)和3)。将N1与B作为一个新结点N2的两个子结点。如下图,N2的权值为15+15=30,然后用N2替换N1和B,插入有序序列中,保持从小到达排列,即:N230、D30、C40。
5)重复步骤2)和3)。将N2与D作为一个新结点N3的两个子结点。如下图,N3的权值为30+30=60,然后用N3替换N2和D,插入有序序列中,保持从小到达排列,即:C40、N360。
6)重复步骤2)和3)。将C与N3作为一个新结点T的两个子结点。如下图,由于T即是根结点,完成赫夫曼树的构造。WPL=40x1+30x2+15x3+10x4+5x4=205。
6、构造赫夫曼树的赫夫曼算法描述:
1)根据给定的n个权值{w1,w2,......,wn}构成n棵二叉树的集合F={T1,T2,......,Tn},其中每棵二叉树Ti中只有一个带权为wi的根结点,其左右子树均为空。
2)在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
3)在F中删除这两棵树,同时将新得到的二叉树加入到F中。
4)重复2和3步骤,直到F只含一棵树为止。这棵树便是赫夫曼树。
7、赫夫曼编码
7.1、若要设计长短不等的编码,则必须是任一字符的编码都不是另一字符的编码的前缀,这种编码称做前缀编码。
7.2、一般地,设需要编码的字符集为{d1,d2,......dn},各个字符在电文中出现的次数或频率集合为{w1,w2,......,wn},以d1,d2,......dn作为叶子结点,以w1,w2,......,wn作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是赫夫曼编码。一棵赫夫曼树如下。