这个作业属于哪个班级 | 数据结构--网络2011/2012 |
---|---|
这个作业的地址 | DS博客作业03--树 |
这个作业的目标 | 学习树结构设计及运算操作 |
姓名 | 胡旻轩 |
0.PTA得分截图
1.本周学习总结(5分)
1.1 二叉树结构
1.1.1 二叉树的2种存储结构
- 二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。普通二叉树转完全二叉树的方法很简单,只需给二叉树额外添加一些节点,将其"拼凑"成完全二叉树即可。如下图所示:
- 完全二叉树的顺序存储,仅需从根节点开始,按照层次依次将树中节点存储到数组即可。完全二叉树存储状态示意图如下:
-
其实二叉树并不适合用数组存储,因为并不是每个二叉树都是完全二叉树,普通二叉树使用顺序表存储或多或多会存在空间浪费的现象,这样就推出了链式存储结构。
-
一棵普通的二叉树,若将其采用链式存储,则只需从树的根节点开始,将各个节点及其左右孩子使用链表存储即可。因此,图 1 对应的链式存储结构如下图所示:
- 采用链式存储二叉树时,其节点结构由 3 部分构成(如图下图所示):
指向左孩子节点的指针(Lchild);
节点存储的数据(data);
指向右孩子节点的指针(Rchild);
- 表示该节点结构的 C 语言代码为:
typedef struct BiTNode{
TElemType data;//数据域
struct BiTNode *lchild,*rchild;//左右孩子指针
struct BiTNode *parent;
}BiTNode,*BiTree;
1.1.2 二叉树的构造
- 这里,给出两种常用的二叉树的创建方法,一是根据类前序序列作为输入构造二叉链表的二叉树,二是根据前序遍历序列和中序遍历序列做为输入构造二叉链表的二叉树。此外,二叉树的创建非常适合使用递归的方法来实现,逻辑简单易于实现;但是在非递归的环境下也是可以实现的,但是实现逻辑较为复杂。如下图为例:
-
1)根据类前序序列构造二叉链表的二叉树
-
如上图所示的二叉树可表示为如下图5所示的扩展二叉树,即为每一个结点的空指针引入一个虚结点“#”,该扩展二叉树的前序序列表示为:ABD##EF#H#CG#H###,其中结点的顺序是前序序列,结点之间的“#”代表空结点。因此,从键盘输入该序列,可以唯一的构造一个二叉树。
-
编程思路:根据创建二叉链表的思维方式,当输入的是结点字符时,需要创建一个新结点,然后递归创建左子树,递归创建右子树。若输入的是“#”,则表明该二叉树为空树,即R=NULL。
-
2)根据前序遍历序列和中序遍历序列构造二叉链表的二叉树
-
如上图所示的二叉树的前序序列和中序序列为:ABDEFCGH和DBFEAGHC,则如何创建二叉链表的二叉树呢,编程思路如下:
所有创建二叉树的思路有其通用性,都是先创建根结点,再递归创建左子树和右子树,那么本题就转化成如何查找根结点和左右子树的问题。
如何找到二叉树的根呢?这就需要在前序序列中找根结点,就是当前前序序列数组的首字符;
b.如何找出这个根的左子树和右子树呢?根据前序序列[1..n]中找出的根结点,查找在中序序列[1..n]中的位置pos,则前序序列中[2,pos]部分是左子树,[pos,n]部分就是右子树;
c.递归创建:将[2.pos]看成是一棵二叉树,反复进行步骤a和 b;将[pos, n]看成是一棵二叉树,反复进行步骤a和 b,直到该树为空结束。
1.1.3 二叉树的遍历
二叉树的四种遍历方式:
-
二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问依次且仅被访问一次。
-
四种遍历方式分别为:先序遍历、中序遍历、后序遍历、层序遍历。
- 1)首先来看前序遍历,所谓的前序遍历就是先访问根节点,再访问左节点,最后访问右节点,如下图所示:
如上图所示,前序遍历结果为:ABDFECGHI
- 再者就是中序遍历,所谓的中序遍历就是先访问左节点,再访问根节点,最后访问右节点,如下图所示:
如上图所示,中序遍历结果为:DBEFAGHCI
- 最后就是后序遍历,所谓的后序遍历就是先访问左节点,再访问右节点,最后访问根节点,如下图所示:
如上图所示,后序遍历结果为:DEFBHGICA
1.2 多叉树结构
1.2.1多叉树遍历
多叉树的设计、建立、层次优先遍历和深度优先遍历
多叉树中的节点有两个域,分别表示节点名以及一个数组,该数组存储其子节点的地址。实现了一个多叉树建立函数,用于输入格式为A B。A表示节点的名字,B表示节点的子节点个数。建立函数根据用户的输入,首先建立一个新的节点,然后根据B的值进行深度递归调用。用户输入节点的顺序就是按照深度递归的顺序。另外,我们实现了一个层次优先遍历函数。该函数用一个队列实现该多叉树的层次优先遍历。首先将根节点入队列,然后检测队列是否为空,如果不为空,将队列出队列,访问出队列的节点,然后将该节点的子节点指针入队列,依次循环下去,直至队列为空,终止循环,从而完成整个多叉树的层次优先遍历。
首先,用户的多叉树数据存储在一个文件中,格式如下:
每行的第一个元素指定一个节点,其中第一行指定了该多叉树的根节点。第二个元素表示该节点有几个子节点,紧接着后面跟了几个子节点。
根据以上数据文件,其对应的多叉树应该是如下:
我们想得到结果是将书中的节点按深度进行输出,比如先输出深度最深的节点:x e j,然后输出深度为2的节点:d f i,之后再输出深度为1的节点:g cC z bBbB,最后输出根节点:aA。
按照深度将节点输出,很显然是用层次优先遍历的方法解决。层次优先遍历的实现原理就是从根节点开始,利用队列实现。
另外,我们想得到从根节点开始到叶子节点直接所有节点名字加起来最长的一个路径,比如上面的树中存在以下几条路径:
aA g d x
aA g d e
aA g d j
aA cC
aA z f
aA z i
aA bBbB
显然,在这些路径中,aA bBbB是所有路径上节点名字加起来最长的一个路径。求解从根节点到叶子节点上的所有路径,利用深度优先遍历更为合适。
下面我们讨论一下多叉树节点应该如何建立。首先多叉树的节点应该如何定义,节点除了有自身的名字外,还要记录其子节点有多少个,每个子节点在哪里,所以我们需要增加一个记录子节点个数的域,还要增加一个数组,用来记录子节点的指针。另外,还要记录多叉树中每个节点的深度值。
在读取数据文件的过程中,我们顺序扫描整个文件,根据第一个名字,建立新的节点,或者从多叉树中找到已经有的节点地址,将后续的子节点生成,并归属于该父节点,直至扫描完整个数据文件。
读取完整个文件后,也就建立了多叉树,之后,我们利用队列对多叉树进行广度优先遍历,记录各个节点的深度值。并将其按照深度进行输出。
获取从根节点到子节点路径上所有节点名字最长的路径,我们利用深度优先遍历,递归调用深度优先遍历函数,找到最长的那个路径。
初次之外,还需定义队列结构体,这里使用的队列是循环队列,实现相关的队列操作函数。还有定义栈的结构体,实现栈的相关操作函数。另外对几个内存分配函数、字符串拷贝函数、文件打开函数进行了封装。需要注意的一点就是当操作完成后,需要对已经建立的任何东西都要销毁掉,比如中途建立的队列、栈、多叉树等,其中还包含各个结构体中的指针域。
另外,函数测试是用户在命令行模式下输入程序名字后面紧跟数据文件的形式。
该程序的主要部分有如下几点:
1.多叉树节点的定义和生成一个新节点
2.数据文件的读取以及多叉树的建立
3.根据节点名字在多叉树中查找节点的位置
4.多叉树的层次优先遍历
5.多叉树的深度优先遍历
6.队列的定义以及相关操作函数实现
7.栈的定义以及相关操作函数实现
8.消毁相关已经建立好的队列、栈、多叉树等
9.测试模块
1.3 哈夫曼树
1.3.1 哈夫曼树定义
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
应用:
1、哈夫曼编码
在数据通信中,需要将传送的文字转换成二进制的字符串,用0,1码的不同排列来表示字符。例如,需传送的报文为“AFTER DATA EAR ARE ART AREA”,这里用到的字符集为“A,E,R,T,F,D”,各字母出现的次数为{8,4,5,3,1,1}。现要求为这些字母设计编码。要区别6个字母,最简单的二进制编码方式是等长编码,固定采用3位二进制,可分别用000、001、010、011、100、101对“A,E,R,T,F,D”进行编码发送,当对方接收报文时再按照三位一分进行译码。显然编码的长度取决报文中不同字符的个数。若报文中可能出现26个不同字符,则固定编码长度为5。然而,传送报文时总是希望总长度尽可能短。在实际应用中,各个字符的出现频度或使用次数是不相同的,如A、B、C的使用频率远远高于X、Y、Z,自然会想到设计编码时,让使用频率高的用短码,使用频率低的用长码,以优化整个报文编码。
哈夫曼树
哈夫曼树(4张)
为使不等长编码为前缀编码(即要求一个字符的编码不能是另一个字符编码的前缀),可用字符集中的每个字符作为叶子结点生成一棵编码二叉树,为了获得传送报文的最短长度,可将每个字符的出现频率作为字符结点的权值赋予该结点上,显然字使用频率越小权值越小,权值越小叶子就越靠下,于是频率小编码长,频率高编码短,这样就保证了此树的最小带权路径长度效果上就是传送报文的最短长度。因此,求传送报文的最短长度问题转化为求由字符集中的所有字符作为叶子结点,由字符出现频率作为其权值所产生的哈夫曼树的问题。利用哈夫曼树来设计二进制的前缀编码,既满足前缀编码的条件,又保证报文编码总长最短。
哈夫曼静态编码:它对需要编码的数据进行两遍扫描:第一遍统计原数据中各字符出现的频率,利用得到的频率值创建哈夫曼树,并必须把树的信息保存起来,即把字符0-255(28=256)的频率值以2-4BYTES的长度顺序存储起来,(用4Bytes的长度存储频率值,频率值的表示范围为0--232-1,这已足够表示大文件中字符出现的频率了)以便解压时创建同样的哈夫曼树进行解压;第二遍则根据第一遍扫描得到的哈夫曼树进行编码,并把编码后得到的码字存储起来。
哈夫曼动态编码:动态哈夫曼编码使用一棵动态变化的哈夫曼树,对第t+1个字符的编码是根据原始数据中前t个字符得到的哈夫曼树来进行的,编码和解码使用相同的初始哈夫曼树,每处理完一个字符,编码和解码使用相同的方法修改哈夫曼树,所以没有必要为解码而保存哈夫曼树的信息。编码和解码一个字符所需的时间与该字符的编码长度成正比,所以动态哈夫曼编码可实时进行。
2、哈夫曼译码
在通信中,若将字符用哈夫曼编码形式发送出去,对方接收到编码后,将编码还原成字符的过程,称为哈夫曼译码。
1.3.2 哈夫曼树的结构体
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。