阅读目录
1 树的定义
之前我们一直在谈的是一对一的线性结构,可现实中,还有很多一对多的情况需要处理,所以我们需要研究这种一对多的数据结构一“树”,考虑它的各种特性,来解决我们在编程中碰到的相关问题。
1.1 定义
树(Tree)是 n(n>=0)个结点的有限集。 n=0时称为空树。在任意一棵非空树中: (1)有且仅有一个特定的称为根 (Root)的结点; (2)当 n>1 时,其余结点可分为 m (m>0) 个互不相交的有限集T1、T2、……、Tm。其中每一个集合本身又是一棵树,并且称为根的子树(SubTree),如下图所示。 |
下图就符合树的定义:
其中根结点A有两个子树:
需要注意的是虽然子树的个数没有限制,但是它们一定是互不交互的。下面的图明显不符合互不交互的原则,所以不是树。
1.2 节点分类
树的结点包含一个数据元素及若干指向其子树的分支。结点拥有的的子树数称为结点的度。度为0的称为叶结点或者终端结点;度不为0的结点称为非终端结点或者分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。如下图所示,因为这棵树结点的度的最大值是结点D的度,为3,所以树的度也为 3 。
1.3 结点间关系
结点的子树的根称为该结点的孩子(Child) ,相应地,该结点称为孩子的双亲
(Parent) 。嗯,为什么不是父或母,叫双亲呢?呵呵,对于结点来说其父母同体,唯一的一个,所以只能把它称为双亲了。同一个双亲的孩子之间互称兄弟 (Sibling) 。结点的祖先是从根到该结点所经分支上的所有结点。所以对于 H 来说,D、B、A都是它的祖先 。反之,以某结点为根的子树中的任一结点都称为该结点的子孙。 B的子孙有D、G、H、I,如下图所示。
1.4 树的其他相关概念
结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。若某结 点在第l层,则其子树的根就在第l+1层。其双亲在同一层的结点互为堂兄弟。显然下图中的D、E、F是堂兄弟,而G、H、丨、J也是。树中结点的最大层次称为树的深度(Depth)或高度,当前树的深度为4。
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。
对比线性表与树的结构,它们有很大的不同,如下图所示。
2 树的抽象数据类型
相对于线性结构,树的操作就完全不同了,这里我们给出一些基本和常用操作。
3 树的存储结构
说到存储结构,就会想到我们前面章节讲过的顺序存储和链式存储两种结构。充分利用顺序存储和链式存储结构的特点,完全可以实现对树的存储结构的表示。我们这里要介绍三种不同的表示法:双亲表示法、孩子表示法、孩子兄弟示法。
3.1 双亲表示法
我们假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点在链表中的位置。也就是说,每个结点除了知道自己是谁以外,还知道它的双亲在哪里。它的结点结构为下图所示。
其中data是数据域,存储结点的数据信息。而parent是指针域,存储该结点的双亲在数组中的下标。
以下是我们的双亲表示法的结点结构定义代码。
/* 树的双亲表示法的结点结构定义 */
#define MAX_TREE_SIZE 100
typedef int TElemType; //树结点的数据类型
typedef struct PTNode //结点结构
{
TElemType data;
int parent; //父结点位置
}PTNode;
typedef struct //树结构
{
PTNode nodes{MAX_TREE_SIZE};
int r, n; //根结点位置,结点数
}Tree;
思路:由于根节点是没有双亲的,所以我们约定根节点的位置域设置为-1。这也意味着我们所有节点都存有它双亲的位置。树结构和树双亲表示法如下所示:
这样的存储结构,我们可以根据结点的parent指针很容易找到它的双亲结点,所 用的时间复杂度为〇(1),直到parent为-1时,表示找到了树结点的根。可如果我们 要知道结点的孩子是什么,对不起,请遍历整个结构才行。
这真是麻烦,能不能改进一下呢?
当然可以。我们增加一个结点最左边孩子的域,不妨叫它长子域,这样就可以很容易得到结点的孩子。如果没有孩子的结点,这个长子域就设置为-1,如下图所示。
对于有0个或1个孩子结点来说,这样的结构是解决了要找结点孩子的问题了。 甚至是有2个孩子,知道了长子是谁,另一个当然就是次子了。
另外一个问题场景,我们很关注各兄弟之间的关系,双亲表示法无法体现这样的 关系,那我们怎么办?嗯,可以增加—个右兄弟域来体现兄弟关系,也就是说,每一个结点如果它存在右兄弟,则记录下右兄弟的下标。同样的,如果右兄弟不存在,则赋值为-1。
但如果结点的孩子很多,超过了 2个。我们又关注结点的双亲、又关注结点的孩 子、还关注结点的兄弟,而且对时间遍历要求还比较高,那么我们还可以把此结构扩展为有双亲域、长子域、再有右兄弟域。所以说存储结构的设计是一个非常灵活的过程。
总结
优点:该存储方式根据结点的parent指针很容易找到它的双亲结点,时间复杂度为O(1)。
缺点:如果需要知道某个结点的所有孩子,需要遍历整棵树。
3.2 孩子表示法
换一种完全不同的考虑方法。由于树中每个结点可能有多棵子树,可以考虑用多重链表,即每个结点有多个指针域,其中每个指针指向一棵子树的根结点,我们把这 种方法叫做多重链表表示法。不过,树的每个结点的度,也就是它的孩子个数是不同的。所以可以设计两种方案来解决。
- 方案一:是指针域的个数就等于树的度,而树的度是树各个结点度的最大值。
- 方案二:每个结点指针域个数等于该结点的度,我们专门取一个位置来存储结点指针域的个数。
对于方案一,当树中各结点的度相差很大时,显然是很浪费空间的,因为有很多的结点,它的指针域都是空的。不过如果树的各结点度相差很小时,那就意味着开辟的空间被充分利用了,这时存储结构的缺点反而变成了优点。
而对于方案二,这种方法克服了浪费空间的缺点,对空间利用率是很高了,但是由于各个结点的链表是不相同的结构,加上要维护结点的度的数值,在运算上就会带来时间上的损耗。
能否有更好的方法,既可以减少空指针的浪费又能使结点结构相同。
仔细观察,我们为了要遍历整棵树,把每个结点放到—个顺序存储结构的数组中是合理的,但每个结点的孩子有多少是不确定的,所以我们再对每个结点的孩子建立一个单链表体现它们的关系。
这就是我们要讲的孩子表示法。具体办法是,把每个结点的孩子结点排列起来, 以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中,如下图所示。
为此,设计两种结点结构,一个是孩子链表的孩子结点,如下图所示。
其中child是数据域,用来存储某个结点在表头数组中的下标。next是指针域,用 来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组的表头结点,如下图所示。
其中data是数据域,存储某结点的数据信息。firstchild是头指针域,存储该结点
的孩子链表的头指针。
以下是我们的孩子表示法的结构定义代码。
/* 树的孩子表示法结构定义 */
#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct CTNode //子结点结构
{
struct CTNode *next;
int child;
}CTNode, *ChildPtr;
typedef struct //表头结构
{
TElemType data;
ChildPtr firstchild;
}CTChain;
typedef struct //树结构
{
CTChain nodes{ MAX_TREE_SIZE };
int r, n; //根结点位置,结点数
}Tree;
总结
缺点: 如果需要知道某个结点的双亲,需要遍历整棵树
改进:孩子兄弟表示法
3.3 孩子兄弟表示法
刚才我们分别从双亲的角度和从孩子的角度研究树的存储结构,如果我们从树结 点的兄弟的角度又会如何呢?当然,对于树这样的层级结构来说,只研究结点的兄弟是不行的,我们观察后发现,任意一棵树,它的结点的第一个孩子如果存在就是唯一 的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
结点结构如下图所示。
其中data是数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地 址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。
结构定义代码如下。
/* 树的孩子兄弟表示法结构定义 */
typedef struct CSNode
{
TElemType data;
struct CSNode *fristchild, *rightsib;
} CSNode, *CSTree;
这种方法实现的示意图如下图所示。
这种表示法,给查找某个结点的某个孩子带来了方便,只需要通过fistchild找到 此结点的长子,然后再通过长子结点的rightsib找到它的二弟,接着一直下去,直到找到具体的孩子。当然,如果想找某个结点的双亲,完全可以再增加一个parent指针域来解决快速查找双亲的问题,这里就不再细谈了。
总结
优点: 可以把一棵复杂的树变成一棵二叉树,这样就可以充分利用二叉树的特性和算法来处理这棵树了。
4 二叉树的定义
4.1 定义
二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成(子树也为二叉树)。 |
4.2 二叉树的特点
二叉树的特点有:
- 每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。
- 左子树和右子树是有顺序的,次序不能任意颠倒。
- 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。
二叉树具有五种基本形态:
- 空二叉树
- 只有一个根结点
- 根结点只有左子树
- 根结点只有右子树
- 根结点既有左子树又有右子树
4.3 特殊二叉树
斜树:所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。
满二叉树:如果所有分支结点都存在左子树和右子树,并且所有的叶子都在同一层上。就称为满二叉树。
完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同。则这棵二叉树称为完全二叉树。
5 二叉树的性质
二叉树有一些需要理解并记住的特性,以便于我们更好地使用它。
性质1:在二叉树的第i层上至多有2i-1个结点(i>=1)。
性质2:深度为k的二叉树至多有2k-1个结点(k>=1)。
性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2+1。
性质4:具有n个结点的完全二叉树的深度为[log2n ] + 1([X]表示不大于X的最大整数)。
性质5:如果对一颗有n个结点的完全二叉树(其深度为[log2n ] + 1)的结点按层序编号(从第1层到第[log2n ] + 1层,每层从左到右),对任一结点i(1<=i<=n)有:
- 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i/2]。
- 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点i。
- 如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
结合下图很好理解:
6 二叉树的存储结构
6.1 二叉树顺序存储结构
前面我们已经谈到了树的存储结构,并且谈到顺序存储对树这种一对多的关系结 构实现起来是比较困难的。但是二叉树是一种特殊的树,由于它的特殊性,使得用顺序存储结构也可以实现。
二叉树的顺序存储结构就是用一维数组存储二叉树中的结点,并且结点的存储位 置,也就是数组的下标要能体现结点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。
对于一般的二叉树,尽管层序编号不能反映逻辑关系,但是可以将其按完全二叉树编号,只不过,把不存在的结点设置为^而已,如下图所示。
对于右斜树,顺序存储结构浪费存储空间:
二叉树的顺序存储结构缺点很明显:不能反应逻辑关系;对于特殊的二叉树(左斜树、右斜树),浪费存储空间。所以二叉树顺序存储结构一般只用于完全二叉树。
6.2 二叉链表
既然顺序存储适用性不强,我们就要考虑链式存储结构。二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的 链表叫做二叉链表。结点结构图如下图所示。
其中data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针。
以下是二叉链表的结点结构定义:
/*二叉树的二叉链表结点结构定义*/
typedef struct BiNode
{
char data; /*结点数据*/
struct BiNode *lchild, *rchild; /*左右孩子指针*/
}BiNode,*BiTree;
结构示意图如下图所示。
就如同树的存储结构中讨论的一样,如果有需要,还可以再增加一个指向其双亲
的指针域,那样就称之为三叉链表。由于与树的存储结构类似,这里就不详述了。
7 遍历二叉树
7.1 二叉树遍历原理
二叉树的遍历是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。 |
这里有两个关键词:访问和次序。
访问其实是要根据实际的需要来确定具体做什么,比如对每个结点进行相关计 算,输出打印等,它算作是一个抽象操作。在这里我们可以简单地假定就是输出结点的数据信
二叉树的遍历次序不同于线性结构,最多也就是从头至尾、循环、双向等简单的 遍历方式。树的结点之间不存在唯一的前驱和后继关系,在访问一个结点后,下一个被访问的结点面临着不同的选择。
7.2 二叉树遍历方法
二叉树的遍历方式可以很多,如果我们限制了从左到右的习惯方式,那么主要就
分为四种:
1. 前序遍历
规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子 树,再前序遍历右子树。如下图所示,遍历的顺序为:ABDGHCEIF。
二叉树的定义是用递归的方式,所以,实现遍历算法也可以采用递归,而且极其 简洁明了。先来看看二叉树的前序遍历算法。代码如下:
/* 二叉树的前序遍历递归算法 */
void PreOrderTraverse(BiTree T)
{
if (T==NULL)
return;
printf("%c",T->data); /* 显示结点数据,可以更改为其他对结点操作 */
PreOrderTraverse(T->lchild); /* 再先序遍历左子树 */
PreOrderTraverse(T->rchild); /* 最后先序遍历右子树 */
}
程序具体的实现在书中有介绍。(每一步都有详细介绍)
2. 中序遍历
规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结
点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。如下图所示,遍历的顺序为:GDHBAEICF。
那么二叉树的中序遍历算法是如何呢?哈哈,别以为很复杂,它和前序遍历算法仅仅只是代码的顺序上的差异。
/* 二叉树的中序遍历递归算法 */
void PreOrderTraverse(BiTree T)
{
if (T==NULL)
return;
PreOrderTraverse(T->lchild); /* 先中序遍历左子树 */
printf("%c",T->data); /* 显示结点数据,可以更改为其他对结点操作 */
PreOrderTraverse(T->rchild); /* 最后中序遍历右子树 */
}
换句话说,它等于是把调用左孩子的递归函数提前了,就这么简单。程序具体的实现在书中有介绍。(每一步都有详细介绍)
3. 后序遍历
规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。如下图所示,遍历的顺序为:GHDBIEFCA。
代码如下:
/* 二叉树的后序遍历递归算法 */
void PreOrderTraverse(BiTree T)
{
if (T==NULL)
return;
PreOrderTraverse(T->lchild); /* 先后序遍历左子树 */
PreOrderTraverse(T->rchild); /* 再后序遍历右子树 */
printf("%c",T->data); /* 显示结点数据,可以更改为其他对结点操作 */
}
4. 层序遍历
规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。如下图所 示,遍历的顺序为:ABCDEFGHI。
有同学会说,研究这么多遍历的方法干什么呢?
我们用图形的方式来表现树的结构,应该说是非常直观和容易理解,但是对于计 算机来说,它只有循环、判断等方式来处理,也就是说,它只会处理线性序列,而我们刚才提到的四种遍历方法,其实都是在把树中的结点变成某种意义的线性序列,这就给程序的实现带来了好处。另外不同的遍历提供了对结点依次处理的不同方式,可以在遍历过程中对结点进行各种处理。
7.3 推导遍历结果
有一种题目为了考査你对二叉树遍历的掌握程度,是这样出题的。已知一棵二叉树的前序遍历序列为ABCDEF,中序遍历序列为CBAEDF,请问这棵二叉树的后序遍历结果是多少?
结果是CBEFDA。具体解题过程在这里不再说明。
8 二叉树的建立
如果我们要在内存中建立一个如下图左这样的树,为了能让每个结点确认是否有左右孩子,我们对它进行了扩展,变成下图右的样子,也就是将二叉树中每个结点的空指针引出一个虚结点,其值为一特定值,比如“#”。我们称这种处理后的二叉树为原二叉树的扩展二叉树。扩展二叉树就可以做到一个遍历序列确定一棵二叉树了。比如下图的前序遍历序列就为AB#D##C##。
有了这样的准备,我们就可以来看看如何生成一棵二叉树了。假设二叉树的结点均为一个字符,我们把刚才前序遍历序列AB#D##c##用键盘挨个输入。实现的算法如下:
/* 按前序输入二叉树中结点的值(一个字符) */
/* #表示空树,构造二叉链表表示二叉树T */
Status CreateBiTree(BiTree *T)
{
TElemType ch;
scanf("%c",&ch);
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch; /* 生成根结点 */
CreateBiTree(&(*T)->lchild); /* 递归构造左子树 */
CreateBiTree(&(*T)->rchild); /* 递归构造右子树 */
}
}
其实建立二叉树,也是利用了递归的原理。只不过在原来应该是打印结点的地 方,改成了生成结点、给结点赋值的操作而已。所以大家理解了前面的遍历的话,对于这段代码就不难理解了。
当然,你完全也可以用中序或后序遍历的方式实现二叉树的建立,只不过代码里生成结点和构造左右子树的代码顺序交换一下。另外,输入的字符也要做相应的更改。比如上图的扩展二叉树的中序遍历字符串就应该为#B#D#A#C#,而后序字符串应该为###DB##CA。
9 线索二叉树
9.1 线索二叉树原理
对一个有n个节点的二叉链表(如上图),整表存在2n个指针域,但分支线只有n-1条,说明空指针域的个数为2n-(n-1) = n+1个,浪费了很多的内存资源。
我们可以通过利用这些空指针域,存放节点在某种遍历方式下的前驱和后继节点的指针。我们把这种指向前驱和后继的指针成为线索,加上线索的二叉链表成为线索链表,对应的二叉树就成为“线索二叉树(Threaded Binary Tree)”,如下图所示。
在决定Ichild是指向左孩子还是前驱,rchild是指向右孩子还是后继上是需要一个区分标志的。因此,我们在每个结点再增设两个标志域ltag和rtag,注意ltag和rtag只是存放0或1数字的布尔型变量。结点结构如下图所示。
其中:
- ltag为0时指向该结点的左孩子,为1时Ichild指向该结点的前驱。
- rtag为0时指向该结点的右孩子,为1时rchild指向该结点的后继。
- 二叉链表图如下图所示。
9.2 线索二叉树结构实现
由此二叉树的线索存储结构定义代码如下:
/* 二叉树的线索存储结构定义 */
typedef enum {Link,Thread} PointerTag; /* Link==0表示指向左右孩子指针, */
/* Thread==1表示指向前驱或后继的线索 */
typedef struct BiThrNode /* 二叉线索存储结点结构 */
{
TElemType data; /* 结点数据 */
struct BiThrNode *lchild, *rchild; /* 左右孩子指针 */
PointerTag LTag;
PointerTag RTag; /* 左右标志 */
} BiThrNode, *BiThrTree;
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
中序遍历线索化的递归函数代码如下:
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); /* 递归右子树线索化 */
}
}
你会发现,这代码和二叉树中序遍历的递归代码几乎完全一样,只不过将本是打印结点的功能改成了线索化的功能。
有了线索二叉树后,我们对它进行遍历时发现,其实就等于是操作一个双向链表结构。
遍历的代码如下:
/* 中序遍历二叉线索树T(头结点)的非递归算法 */
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p=T->lchild; /* p指向根结点 */
while(p!=T)
{ /* 空树或遍历结束时,p==T */
while(p->LTag==Link)
p=p->lchild;
printf("%c",p->data); /* 显示结点数据,可以更改为其他对结点操作 */
while(p->RTag==Thread&&p->rchild!=T)
{
p=p->rchild;
printf("%c",p->data); /* 访问后继结点 */
}
p=p->rchild;
}
return OK;
}
由于线索二叉树充分利用了空指针域的空间(这等于节省了空间),又保证了创建时的一次 遍历就可以终生受用前驱后继的信息(这意味着节省了时间)。所以在实际问题中,如果所用的二叉树需经常遍历或査找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
10 树、森林与二叉树的转换
10.1 树转换成二叉树
分三个步骤:
1)将所有兄弟连起来,(注意不要连堂兄弟)
2)每个结点都保留与第一个孩子的线,与其他孩子的全部断掉。(注意与兄弟结点的关系保留)
3)以根节点为轴顺时针旋转。注意每个结点的第一个孩子将变成左子树。兄弟变成右子树。
如图:
10.2 森林转换成二叉树
步骤如下:
1)将每个树转换成二叉树
2)第一棵树不动,从第二个树开始,依次把后一棵二叉树的根节点作为前一个二叉树的根节点的右孩子(树转成二叉树,根结点是没有右子树的)。当所有二叉树连接起来的时候就得到了一个森林。
3)将得到的森林转换成二叉树。
10.3 二叉树转换成树
也就是树转换成二叉树的逆过程(这棵树一定根结点没有右子树):
1)加线:如果存在左子树。就将这个左孩子的右结点,右孩子的右孩子结点。。。反正就是左孩子的n个右孩子结点都作为次结点的孩子。将该结点与这些右孩子结点连接起来
2)去线:删除之前二叉树中所有结点与右孩子结点的连线
3)层次调整,使之结构分明。
(如下图:假如D还有右孩子H,那么A还要与H连接起来,再去掉D到H的线)
10.4 树与森林的遍历
1.树遍历分为两种方式:
1)先根遍历:先访问树的根结点,然后依次遍历根的每颗子树。(比如上图6-11-4的树,采用这种方式遍历就是ABEFCDG)
2)后根遍历:先依次后根遍历每棵子树,再访问根结点。(6-11-4用这种方法就是:EFBCGDA)
2.森林的遍历也分两种:
1)前序遍历:先访问森林中第一个树的根结点,再依次先根遍历根的每棵子树(也就是先根遍历)。最后用同样的方式去遍历剩余的树。
2)后序遍历:先访问第一个树,用后根遍历的方式遍历。接着把其他的树遍历一下。
总结:
1)森林的前序遍历和二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同。
2)当以二叉链表(每个结点设计成一个数据域和两个指针域(left,right,我觉得还可以加上parent,把这样的链表叫做二叉链表))作为树的存储结构时,
树的先根遍历和后根遍历完全可以借用二叉树的前序遍历和中序遍历的算法来实现。我们就找到了树和森林这种复杂问题的简单解决办法。
11 赫夫曼树及其应用
11.1 赫夫曼树定义与原理
百度百科上说:给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,这样的二叉树称为最优二叉树(也叫赫夫曼树huffman tree)。
哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
如何构造赫夫曼树:
1)根据给定的n个权值{w1, w2, w3 … wn}构成n棵二叉树的集合F={T1, T2, … Tn}。其中每棵二叉树Ti中只有一个带权为wi根节点,其左右子树均为空。
2)在F中选取两棵根节点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根节点的权值为其左右子树上跟结点的权值之和。
3)在F中删除这两棵树,同时将新得到的二叉树加入F中。
4)重复2和3,直到F只包含一棵树为止,这棵树就是赫夫曼树。
11.2 赫夫曼编码
假设我们要给别人发送BADCADFEED,全部展开成二进制,是3*10共30位。
但其实出现的是有概率的,比如E出现的多,F出现的少,我们就可以假定6个字母的频率为A27 B8 C15 D15 E30 F5.
再把它们转换成赫夫曼树,然后将权值左分支改为0,右分支改为1,得到下图:
得到一个新的编码对应关系:
那么解码过程呢:
要设计长短不等的编码,则必须是任意字符都不是另一个字符的前缀。这种编码称为前缀编码。比如不能存在这样的编码,10,100,1001。解码时还要用到赫夫曼树,接收方和发送方必须要约定好同样的赫夫曼编码规则。