数据结构最优二叉树.docx
《数据结构最优二叉树.docx》由会员分享,可在线阅读,更多相关《数据结构最优二叉树.docx(7页珍藏版)》请在冰豆网上搜索。
![数据结构最优二叉树.docx](https://file1.bdocx.com/fileroot1/2023-1/30/1ab50f35-dd62-492d-87a5-ee6e5ddd13f7/1ab50f35-dd62-492d-87a5-ee6e5ddd13f71.gif)
数据结构最优二叉树
最优二叉树--哈夫曼树压缩技术的应用
一、数据压缩技术概述、方法
随着社会的发展,人们对信息处理的要求越来越高,要求存储和通讯的信息量也越来越大。
于是信息学中出现了数据压缩这门技术,它的基本特征就是把某些表达式中的字符串(如ASCII)转化成包含相同信息但长度尽量短的一个新串,存储或传送经过压缩的数据可以减少存储或通讯费用。
换句话说,数据压缩的目的是减少数据的冗余度提高数据密度的有效性。
数据压缩有着广泛的应用范围,首先文件存储和分布式系统是个大应用领域;其次数据压缩可用于数据的安全和保密;再者,通过处理压缩数据还可以开发快速算法等等
数据压缩的研究始于二十世纪四十年代,而且发展迅速。
数据压缩实际上是一种编码,即对数据表达式的一种压缩式编码。
编码是源文(源字母表α的字)到码字(码字母表β的字)的一个映射。
编码可分为块-块,块-变量,变量-块,及变量-变量几类,其中块-块表示源文和码字是因定长度的,而变量-变量编码是把可变长的源文映射成可变长的码字。
总的来说,数据压缩方法可分为三类:
静态、动态及混合方法。
所谓静态方法就是在信文传送之前源文集和码子集之间的映射关系就已确定,以后给定信文无论什么时刻出现在信文集中总被表示为相同的码字,静态编码需要二次操作:
第一次计算概率并确定映射,第二次传送码字;动态方法是在信文集和码子集之间建立一种随着时间的变化而变化的映射关系,动态方法也称为自适应编码,即它们不断地修改源信文集与码字的对就关系以便更好地适应于信文集特征随时间的变化,所有的自适应方法都是一次操作方法,即只需对源文集扫描一次;当一个算法既不完全静态又不完全动态时,就称为混合方法。
而我们要提到的基于哈夫曼编码数据压缩技术,就时静态方法实现的一种方式。
二、最优二叉树原理分析
(1)最优二叉树的基本概念
最优二叉树,也称哈夫曼(Haffman)树,是指对于一组带有确定权值的叶结点,构造的具有最小带权路径长度的二叉树。
那么什么是二叉树的带权路径长度呢?
n
∑
k=1
如果设二叉树具有n个带权值的叶结点,那么从根结点到各个叶结点的路径长度与相应结点权值的乘积之和叫做二叉树的带权路径长度,记为:
WPL=Wk·Lk,其中Wk为第k个叶结点的权值,Lk为第k个叶结点的路径长度。
如图
(一)所示的二叉树,它的带权路径长度值WPL=2×2+4×2+5×2+3×2=28。
图
(一)
在给定一组具有确定权值的叶结点,可以构造出不同的带权二叉树。
例如,给出4个叶结点,设其权值分别为1,3,5,7,我们可以构造出形状不同的多个二叉树。
这些形状不同的二叉树的带权路径长度将各不相同。
图
(二)给出了其中5个不同形状的二叉树。
这五棵树的带权路径长度分别为:
(a)WPL=1×2+3×2+5×2+7×2=32
(b)WPL=1×3+3×3+5×2+7×1=29
(c)WPL=1×2+3×3+5×3+7×1=33
(d)WPL=7×3+5×3+3×2+1×1=43
(e)WPL=7×1+5×2+3×3+1×3=29
(a) (b)
(c) (d) (e)
图
(二)具有相同叶子结点和不同带权路径长度的二叉树
由此可见,由相同权值的一组叶子结点所构成的二叉树有不同的形态和不同的带权路径长度,那么如何找到带权路径长度最小的二叉树(即哈夫曼树)呢?
根据哈夫曼树的定义,一棵二叉树要使其WPL值最小,必须使权值越大的叶结点越靠近根结点,而权值越小的叶结点越远离根结点。
哈夫曼(Haffman)依据这一特点于1952年提出了一种方法,这种方法的基本思想是:
a)由给定的n个权值{W1,W2,…,Wn}构造n棵只有一个叶结点的二叉树,从而得到一个二叉树的集合F={T1,T2,…,Tn};
b)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和;
c)在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中;
d)重复(b)(c)两步,当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。
由于这种算法是哈夫曼最早提出的,所以将最优二叉树称为哈夫曼树。
(2)根据最优二叉树构造哈夫曼编码
哈夫曼树被广泛应用在各种技术中,其中最典型的就是在编码技术上的应用。
利用哈夫曼树很容易求出给定字符集及其概率(或频度)分布的最优前缀码。
首先我们来看一个例子:
指令
使用频率(wi)
A
0.40
B
0.30
C
0.15
D
0.05
E
0.04
F
0.03
G
0.03
设有一台模型机,共有7种不同的指令,其使用频率如表
(一)所示。
由于计算机内部只能识别0,1代码,所以采用定长操作码,则需3位(23=8)。
显然,有一条编码没有作用,这是一种浪费。
一段程序中若有n条指令,那么程序的总位数为3×n。
表
(一)指令的使用频率
指令
编码
A
0
B
1
C
00
D
01
E
000
F
001
G
010
为了充分利用编码信息和减少程序的总位数,我们可以采用变长编码。
如果对每一条指令指定一条编码,使得这些编码互不相同且最短,是否可以满足要求呢?
即是否可以如表
(二)所示这样编码呢?
表
(二)
指令的变长编码
这样虽然可以使得程序的总位数达到最小,但机器却无法解码。
例如对编码串0010110该怎么识别呢?
第一个0可以识别A,也可以识别与第二个0组成的串00一起被识别为C,还可以将前三位识别为F,这样一来,这个编码串就有多种译法。
因此,若要设计变长编码,则这种编码必须满足这样一个条件:
任意一个编码不能成为其它任意编码的前缀。
我们把这个条件的编码叫做前缀编码。
指令
编码
A
0
B
10
C
110
D
11100
E
11101
F
11110
G
11111
根据以上描述,利用哈夫曼算法,我们可以设计出最优的前缀编码。
首先,我们以每条指令的使用频率为权值构造哈夫曼树,如图(三)所示
图(三)构造哈夫曼树 表(三)指令的哈夫曼编码
对于该二叉树,我们可以规定向左的分支标记为1,向右的分支标记为0。
这样从根结点开始,沿线到达各频度指令对应的结点,所以经过的分枝代码序列就构成了相应频度指令的哈夫曼编码,如表(三)所示。
可以验证,该编码是前缀编码,若一段程序有1000条指令,其中A大约有400条,B大约有300条,C大约有50条,D大约有40,E大约有30条,F大约有30条。
对于定长编码,该段程序的总位数大约为3×1000=3000.采用哈夫曼编码后,该段程序的总位数大约为1×400+2×300+3×150+5×(50+40+30+30)=2200.可见,哈夫曼编码中虽然大部分编码的长度大于定长编码的长度3,却使得程序的总位数变小了。
可以算出该哈夫曼的平均码长为
7
∑
i=1
li×wi=1×0.40+2×0.30+3×0.15+5×0.05+5×0.40+5×0.30+5×0.30=2.2
根据上述例子我们可以总结出,利用哈夫曼树获得哈夫曼编码,即最优前缀编码的具体操做:
① 用字符ci作为叶子,wi做为叶子ci的权,构造一棵哈夫曼树,并将树中左分支和右分支分别标记为0和1;
② 将从根到叶子的路径上的标号依次相连,作为该叶子所表示字符的编码。
该编码即为最优前缀码(也称哈夫曼编码)。
(3)哈夫曼树求得编码为最优前缀码的原因
① 每个叶子字符ci的码长恰为从根到该叶子的路径长度li,平均码长(或文件总长)又是二叉树的带权路径长度WPL。
而哈夫曼树是WPL最小的二叉树,因此编码的平均码长(或文件总长)亦最小。
②树中没有一片叶子是另一叶子的祖先,每片叶子对应的编码就不可能是其它叶子编码的前缀。
即上述编码是二进制的前缀码。
(4)哈夫曼编码的算法
思路:
给定字符集的哈夫曼树生成后,求哈夫曼编码的具体实现过程是:
依次以叶子C[i](0≤i≤n-1)为出发点,向上回溯至根为止。
上溯时走左分支则生成代码0,走右分支则生成代码1。
注意:
①由于生成的编码与要求的编码反序,将生成的代码先从后往前依次存放在一个临时向量中,并设一个指针start指示编码在该向量中的起始位置(start初始时指示向量的结束位置)。
②当某字符编码完成时,从临时向量的start处将编码复制到该字符相应的位串bits中即可。
③因为字符集大小为n,故变长编码的长度不会超过n,加上一个结束符'\0',bits的大小应为n+1。
(5)哈夫曼编码的算法的实现
①哈夫曼编码的数据结构设计
为了在构造哈夫曼树时能方便的实现从双亲结点到左右孩子结点的操作,在进行哈夫曼编码时能方便的实现从孩子结点到双亲结点的操作。
设计哈夫曼树的结点存储结构为双亲孩子存储结构。
采用仿真指针实现,每个结点的数据结构设计为:
②哈夫曼编码算法C语言实现:
structHaffNode //哈夫曼树的结点结构
{
intweight; //权值
intflag; //标记
intparent; //双亲结点下标
intleftChild; //左孩子下标
intrightChild; //右孩子下标
};
structCode //存放哈夫曼编码的数据元素结构
{
intbit[MaxN]; //数组
intstart; //编码的起始下标
intweight; //字符的权值
};
哈夫曼树构造算法如下:
voidHaffman(intweight[],intn,HaffNodehaffTree[])
//建立叶结点个数为n权值为weight的哈夫曼树haffTree
{
intj,m1,m2,x1,x2;
//哈夫曼树haffTree初始化。
n个叶结点的哈夫曼树共有2n-1个结点
for(inti=0;i<2*n-1;i++)
{
if(i