66 赫夫曼树及其应用++.docx
- 文档编号:10242161
- 上传时间:2023-02-09
- 格式:DOCX
- 页数:19
- 大小:66.23KB
66 赫夫曼树及其应用++.docx
《66 赫夫曼树及其应用++.docx》由会员分享,可在线阅读,更多相关《66 赫夫曼树及其应用++.docx(19页珍藏版)》请在冰豆网上搜索。
66赫夫曼树及其应用++
6.6赫夫曼树及其应用
赫夫曼(Huffman)树,又称最优树,是一类带权路径长度最短的树,有着广泛的应用。
本节先讨论最优二叉树。
6.6.1最优二叉树(赫夫曼树)
首先给出路径和路径长度的概念。
从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上的分支数目称做路径长度。
树的路径长度是从树根到每一结点的路径长度之和。
6.2.1节中定义的完全二叉树就是这种路径长度最短的二叉树。
若将上述概念推广到一般情况,考虑带权的结点。
结点的带权路径长度为从该结点到树根之间的路径长度与结点上权的乘积。
树的带权路径长度为树中所有叶子结点的带权路径长度之和,通常记作WPL=
。
(注:
WK为第K个叶子结点的权,LK为第K个叶子结点到根结点的路径长度。
所谓带权路径长度就是结点的权与该结点路径长度的乘积。
)
假设有n个权值{w
,w
,…,w
},试构造一棵有n个叶子结点的二叉树,每个叶子结点带权为w
,则其中带权路径长度WPL最小的二叉树称做最优二叉树或赫夫曼树。
例如,图6.22中的3棵二叉树,都有4个叶子结点a、b、c、d,分别带权7、5、2、4,它们的带权路径长度分别为
(a)WPL=7×2+5×2+2×2+4×2=36
(b)WPL=7×3+5×3+2×1+4×2=46
(c)WPL=7×1+5×2+2×3+4×3=35
其中以(c)树的为最小。
可以验证,它恰为赫夫曼树,即其带权路径长度在所有带权为7、5、2、4的4个叶子结点的二叉树中居最小。
(将权比较大的叶子结点尽量靠近根结点!
)
在解某些判定问题时,利用赫夫曼树可以得到最佳判定算法。
例如,要编制一个将百分制转换成五级分制的程序。
显然,此程序很简单,只要利用条件语句便可完成。
如:
if(a<60)b=“bad”;
elseif(a<70)b=“pass”;
elseif(a<80)b=“general”;
elseif(a<90)b=“good”;
elseb=“excellent”;
这个判定过程可以图6.23(a)的判定树来表示。
如果上述程序需反复使用、,而且每次的输入量很大,则应考虑上述程序的质量问题,即其操作所需时间。
因为在实际生活中,学生的成绩在5个等级上的分布是不均匀的。
假设其分布规律如下表所示:
分数
0-59
60-69
70-79
80-89
90-100
比例数
0.05
0.15
0.45
0.30
0.10
则80%以上的数据需进行3次或3次以上的比较才能得出结果。
假定以5,15,40,30和10为权构造一棵有5个叶子结点的赫夫曼树,则可得到如图6.23(b)所示的判定过程,它可使大部分的数据经过较少的比较次数得出结果。
但由于每个判定框都有两次比较,将这两次比较分开,我们得到如图6.23(c)所示的判定树,按此判定树可写出相应的程序。
假设现有10000个输入数据,若按图6.23(a)的判定过程进行操作,则总共需进行31500次比较;而若按图6.23(c)的判定过程进行操作,则总共仅需进行22000次比较。
那么,如何构造赫夫曼树呢?
赫夫曼最早给出了一个带有一般规律的算法,俗称赫夫曼算法。
现叙述如下:
(1)根据给定的n个权值{w
,w
,…,w
}构成n棵二叉树的集合F={T
,T
,…,T
},其中每棵二叉树T
中只有一个带权为w
的根结点,其左右子树均空。
(2)在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。
(3)在F中删除这两棵树,同时将新得到的二叉树加入F中。
(4)重复
(2)和(3),直到F只含一棵树为止。
这棵树便是赫夫曼树。
例如,图6.24展示了图6.22(c)的赫夫曼树的构造过程。
其中,根结点上标注的数字是所赋的权。
算法的具体描述和实际问题所采用存储结构有关,将留在下节讨论。
——————————————————————————————————
参考材料R6_12:
(摘自宁正元《数据结构》P135-136页)
6.6.1基本术语
1.路径和路径长度
若在一棵树中存在着一个结点序列k
,k
,…,kj,使得k
是k
的双亲(1≤i≤j),则称此结点序列是从k
到kj的路径,因树中每个结点只有一个双亲结点,所以它也是这两个结点之间的唯一路径。
从k
到kj与所经过的分支数称为这两点之间的路径长度,它等于路径上的结点数减1。
2.结点的权和带权路径长度
在许多应用中,常常将树中的结点赋上个有着某种意义的实数,我们称此实数为该结点的权。
结点的带权路径长度规定为:
从树根结点到该结点之间的路径长度与该结点上权的乘积。
3.树的带权路径长度
wpl=
树的带权路径长度定义为树中所有叶子结点的带权路径长度之和,通常记为:
其中i表示叶子结点的数目,w
和l
分别表示叶子结点k
的权值和根到k
之间的路径长度。
例如:
图6.21中的三棵二叉树,都有四个叶子结点a,b,c,d,分别带权为7、5、2、4,由它们构成的三棵不同的:
叉树,如图中(a)~(c)所示。
每—棵二叉树的带权路径长度WPL分别为:
(a)wpl=7×2+5×2+2×2+4×2=36
(b)wpl=7×3+5×3+2×1+4×2=46
(c)wpl=7×1+5×2+2×3+4×3=35
4.哈夫曼树
哈夫曼树(HuffmanTree)又称最优二叉树。
它是n个带权叶子结点构成的所有二叉树中带权路径长度WPL最小的二叉树。
因为构造这种树的算法是最早由哈夫曼于1952年提出的,所以被称之为哈夫曼树。
例如:
图6.21的三棵二叉树中(c)的wpl为最小。
可以验证,此树就是哈夫曼树。
由上面可以看出,由n个带权叶子结点所构成的二又树中,满二叉树不一定是最优二叉树。
权值越大的结点离根越近的二叉树才是最优二叉树。
6.6.2构造哈夫曼树
构造最优二叉树的算法是由哈夫曼提出的,所以称之为哈夫曼算法,具体叙述如下:
(1)根据与n个权值{w
,w
,…,w
}对应的n个结点构成n棵二叉树的森林F={T
,T
,…,T
},其中每棵二叉树T
(1≤i≤n)都只有一个权值为w
的根结点,其左、右子树为空;
(2)在森林F中选出两棵根结点的权值量小的树作为一棵新树的左、右子树,且置新树的根结点的权值为其左、右子树上根结点的权值之和;
(3)从P中删除这两棵树,同时把新树加入F中;
(4)重复
(2)和(3)步,直到F中只含有一棵树为止,此树便是哈夫曼树。
例如:
权值{5,4,7,2,5},根据哈夫曼算法构造的哈夫曼树的过程如图6.22所示
——————————————————————————————————
6.6.2赫夫曼编码
目前,进行快速远距离通信的主要手段是电报,即将需传送的文字转换成由二进制的字符组成的字符串。
例如,假设需传送的电文为‘ABACCDA’,它只有4种字符,只需两个字符的串便可分辨。
假设A、B、C、D的编码分别为00、01、10和11,则上述7个字符的电文便为‘00010010101100’,总长14位,对方接收时,可按二位一分进行译码。
当然,在传送电文时,希望总长尽可能地短。
如果对每个字符设计长度不等的编码,且让电文中出现次数较多的字符采用尽可能短的编码,则传送电文的总长便可减少。
如果设计A、B、C、D的编码分别为0、00、l和01,则上述7个字符的电文可转换成总长为9的字符串’000011010’。
但是,这样的电文无法翻译,例如传送过去的字符串中前4个字符的子串‘0000’就可有多种译法,或是‘AAAA’,或是‘ABA’,也可以是‘BB’等。
因此,若要设计长短不等的编码,则必须是任一个字符的编码都不是另一个字符的编码的前缀,这种编码称做前缀编码。
可以利用二叉树来设计二进制的前缀编码。
假设有一棵如图6.25所示的二叉树,其4个叶子结点分别表示A、B、C、D这4个字符,且约定左分支表示字符’0’,右分支表示字符’1’,则可以从根结点到叶子结点的路径上分支字符组成的字符串作为该叶子结点字符的编码。
读者可以证明,如此得到的必为二进制前缀编码。
如由图6.25所得A、B、C、D的二进制前缀编码分别为0、10、110和111。
又如何得到使电文总长最短的二进制前缀编码呢?
假设每种字符在电文中出现的次数为w
;,其编码长度为l
;,电文中只有n种字符,则电文总长为
。
对应到二叉树上,若置w
为叶子结点的权,l
恰为从根到叶子的路径长度。
则
恰为二叉树上带权路径长度。
由此可见,设计电文总长最短的二进制前缀编码即为以n种字符出现的频率作权,设计一棵赫夫曼树的问题,由此得到的二进制前缀编码便称为赫夫曼编码。
下面讨论具体做法。
由于赫夫曼树中没有度为1的结点(这类树又称严格的(strict)(或正则的)二叉树)则一棵有n个叶子结点的赫夫曼树共有2n-1个结点(请自己验证!
),可以存储在一个大小为2n-1的一维数组中。
如何选定结点结构?
由于在构成赫夫曼树之后,为求编码需从叶子结点出发走一条从叶子到根的路径;而为译码需从根出发走一条从根到叶子的路径。
则对每个结点而言,既需知双亲的信息,又需知孩子结点的信息。
——————————————————————————————————
参考材料R6_13:
(摘自宁正元《数据结构》P136-137页)
哈夫曼编码
哈夫曼算法的应用很广泛,本节以哈夫曼编码(HaffmanCodes)为例来说明它的应用。
在进行快速远距离电报通信中,需将传送的文字转换成由二进制的字符组成的字符串。
例如,假设需传送的电文为‘ABACCDA’,它只有四种字符,只需两个字符的串便可分辨。
假设A、B、C,D的编码分别为00、01、10、11,则上述七个字符的电文便为‘00010010101100’,总长14位,对方接收时,可按二位一分进行译码。
当然,在传送电文时,希望总长尽可能短。
如果对每个字符设计长度不等的编码,且让电文中出现次数较多的字符采用尽可能短的编码,则传送电文的总长便可减少。
如果设计A、B、C、D的编码分别为0、00、1和01,则上述七个字符的电文可转换成总长为9的字符串‘000011010’。
但是,这样的电文无法翻译,例如传送过去的字符串中前四个字符的子串‘0000’就可有多种译法,或是‘AAAA’,或是‘ABA’,也可以是‘BB’等。
因此,若要设计长短不等的编码,则必须是任一个字符的编码都不是另一个字符的编码的前缀,这种编码称做前缀编码(PrefixCode)。
所谓前缀编码系指所编的码字可以通过前缀唯一地正确识别并译出。
可以利用二叉树来设计二进制的前缀编码。
假设有一棵如图6.23所示的二叉树,其四个叶子结点分别表示A、B、C、D四个字符,且约定左分支表示字符‘0’,右分支表示字符‘1’,则可以从根结点到叶子结点的路径上分支字符组成的字符串作为该叶子结点字符的编码。
读者可以证明,如此得到的必为二进制前缀编码。
如由图6.23所得A、B、C、D的二进制前缀编码分别为0、10、110和111。
又如何得到使电文总长最短的二进制前缀编码呢?
假设每种字符在电文中出现的次数为w
,其编码长度为l
则电文中只有n种字符,则电文总长
为对应到二叉树上,若置w
为叶子结点的权,l
恰为从根到叶子的路径长度。
则恰为二叉树上带权路径长度。
由此可见,设计电文总长最短的二进制前缀编码即为以n种字符出现的频率作权,设计一棵哈夫曼树的问题,由此得到的二进制前缀编码称为哈夫曼编码。
例如:
假设电文中出现5个字符A、B、C、D、E,已知它们在电文中出现的频率是5、4、7、2、5。
用{5,4,7,2,5}为权值生成的哈大曼树如图6.22(e)所示,则得编码A:
00,B:
101,C:
11,D:
100,E:
01。
——————————————————————————————————
由此,设定下述存储结构:
//-----赫夫曼树和赫夫曼编码的存储表示-----
typedefstruct{
unsignedintweight;//权重(整型)
unsignedintparent,lchild,rchild;//双亲序号,左孩子序号,右孩子序号
}HTNode,*HuffmanTree;//动态分配数组存储赫夫曼树
typedefchar**HuffmanCode;//动态分配数组存储赫夫曼编码表
(注意:
HuffmanCode是指向指针的指针型!
其应用在下面的程序中可以看到。
)
求赫夫曼编码的算法如算法6.12所示。
voidHuffmanCoding(HuffmanTree&HT,HuffmanCode&HC,int*w,intn){
//w是一块连续存储整数的存储区首址,连续存放着n个字符的权值(均>0),
//本算法构造赫夫曼树HT,并求出n个字符的赫夫曼编码HC。
if(n<=1)return;
m=2*n-1;//m是欲构造的哈夫曼树的结点数,m的值与叶子结点数有关
HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));
//HT为正在建的哈夫曼树的指针;共申请m+1个结点的内存空间。
//序号为0的单元不准备用。
for(p=HT,i=1;i<=n;++i,++p,++w)*p={*w,0,0,0};
//对n个叶子结点进行初始化;为*p赋值即为序号为p的结点单元赋值;
//*w是地址为w的单元的值,它是一个叶子结点的权值。
//*w后的3个0分别表示该结点的双亲结点、左孩子结点、右孩子结点的序号
//这3个参数在初始化时暂时设为0;
for(;i<=m;++i,++p)*p={0,0,0,0};
//对叶子结点以外的其它结点进行初始化.请注意结点的4个参数值全部赋为0
//即叶子结点以外的其它结点的权重、双亲序号、左孩子序号、右孩子序号
//在初始化时全部赋值为0;这其中的有些参数以后还会被修改,此处是暂时的。
for(i=n+1;i<=m;++i){//建赫夫曼树
Select(HT,i-1,s1,s2);//这个函数的程序代码段省略;
//Select函数在HT[1..i-1)选择parent为0且weight最小的两个结点,其中
//结点的parent为0说明该结点的parent值在初始化后尚未被修改过。
//该函数将挑选到的两个符合条件的结点的序号分别赋值给变量s1和s2;由于
//循环控制变量i的值不同,结点挑选的范围也不相同。
HT[s1].parent=i,HT[s2].parent=i;
//给这两个结点的双亲域赋值;
HT[i].Lchild=s1,HT[i].rchild=s2;
//分别给它们的双亲结点的左孩子域和右孩子域赋值;
HT[i].weight=HT[s1].weight+HT[s2].weight;
//给它们的双亲结点的weight域赋值;
}
//---从叶子到根逆向求每个字符的赫夫曼编码---
HC=(HuffmanCode)malloc((n+1)*sizeof(char*));
//分配n个字符的头指针向量
cd=(char*)malloc(n*sizeof(char));//分配求编码的工作空间
cd[n-1]=“\0”;//编码结束符。
//cd是一个临时的连续字符存储区,一个结点的编码在生成过程中暂时放在此//处,因此也可以认为是一个求哈夫曼编码的工作空间。
从cd[0]到cd[n-1]
//共有n个字符位置,但n个叶子结点中最长的哈夫曼编码也不会超过n-1位,
//故可将该连续字符存储区最后的那一位作为存放编码结束符“\0”来使用,
//前面部分用于存放结点的编码。
for(i=1;i<=n;++i){//逐个字符求赫夫曼编码
//只须要对n个叶子结点求哈夫曼编码,每趟循环求一个结点的编码故须n趟.
start=n-1;//编码结束符位置,也就是连续的字符存储区的位置序号。
//下面这个for循环体的功能是求出一个结点的哈夫曼编码。
注意,循环
//起始部分中的“c=i”,那个i就是准备求哈夫曼编码的结点的序号。
for(c=i,f=HT[i].parent;f!
=0;c=f,f=HT[f].parent){
//从叶子到根逆向求编码。
即从指定的序号为i的叶子结点起在双亲域的
//指引下逆向走到根结点求出该(序号为i的)叶子结点的哈夫曼编码。
//而变量f的值准备用来存放当前结点的双亲结点的序号,通过对f的值的//判断可以知道当前结点是否为根结点。
//注意这个循环停止条件f!
=0,是因只有根结点的双亲序号才=0(初始化
//后未曾被修该过),故f!
=0即表明当前结点不是根结点。
if(HT[f].lchild==c)cd[--start]=“0”;
//若当前结点是双亲的左孩子,则当前编码结束位置的前一个位置(即当前//码位)上放一个“0”,否则放一个“1”。
由cd[--start]可知码位的
//使用顺序是逆向使用的。
elsecd[--start]=“1”;
}//for
HC[i]=(char*)malloc((n-start)*sizeof(char));
//为第i个字符编码分配空间
strcpy(HC[i],&cd[start]);//从cd复制编码(串)到HC
}
//HC[i]是一个指针,存放的是相应的HT[i]即树HT的结点i(1<=i<=n)
//的哈夫曼编码串的首地址。
于是可知,HC是一个指针的指针,前边说过。
free(cd);//释放工作空间
算法6.12
向量HT前n分量表示叶子结点,最后一个分量表示根结点。
各字符的编码长度不等,所以按实际长度动态分配空间。
在算法6.12中,求每个字符的赫夫曼编码是从叶子到根逆向处理的。
也可以从根出发,遍历整棵赫夫曼树,求得各个叶子结点所表示的字符的赫夫曼编码,如算法6.13所示,请自行研读。
//-----无栈非递归遍历赫夫曼树,求赫夫曼编码
HC=(HuffmanCode)malloc((n+1)*sizeof(char*));
p=m;cdlen=0;//m是结点数,也是根结点的序号;cdlen是编码的长度
for(i=1;i<=m;++i)HT[i].weight=0;//遍历赫夫曼树时用作结点状态标志
//因准备用.weight域值作为结点的状态标志,故在此先将所有结点的.weight
//域值清为0,为何如此大胆呢?
因为哈夫曼树已经建成,使用.weight域值
//标示权重的任务已经完成。
下面准备用它来标示结点在编码过程中的状态。
//用结点的.weight域值标示结点状态的方法是:
①结点未编码时weight=0;
//②若发现当前结点的weight=0则使其weight=1并且:
该结点如有左孩子就
//使指针指向左孩子,同时将左孩子对应的编码位写上“0”。
③若发现该结点
//的weight=1则将其改写为weight=2并且:
该结点如有右孩子就使指针指向
//右孩子,同时将右孩子对应的编码位写上“1”。
④遇到叶子结点就将编码工
//作中区得到的编码复制到与该结点对应的指定位置。
⑤若该结点的weight=2
//则将其改写为weight=0并且将当前指针指向其双亲结点。
⑥在从根结点往上
//返回其双亲时会发现根结点的双亲序号为0(即p=0)造成循环结束算法终止。
//下一句即while语句开始从根到各个叶子结点逐个求其哈夫曼编码。
while(p){//p为结点序号;只有根结点的双亲结点序号为0(初始化造成)。
if(HT[p].weight==0){//向左
HT[p].weight=1;
if(HT[p].1child!
=0){p=HT[p].lchild;cd[cdlen++]=“0”;}
elseif(HT[p].rchild==0){//登记叶子结点的字符的编码
//结点左子树为0右子树为0则该结点就是叶子结点,故登记其编码。
HC[p]=(char*)malloc((cdlen+1)*sizeof(char));
cd[cdlen]=“\0”;strcpy(HC[p],cd);//复制编码(串)
}
}
elseif(HT[p].weight==1){//向右
HT[p].weight=2;
if(HT[p].rchild!
=0){p=HT[p].rchild;cd[cdlen++]=“1”;}
}else{//HT[p].weight==2,退向
HT[p].weight=0;p=HT[p].parent;--cdlen;
//退到父结点,编码长度减1
}//else
}//While
算法6.13
(注:
关于算法6.13,请读者画一个比较简单的哈夫曼树模拟程序走一遍就会明白。
另外,算法6.13是算法6.12后半部分即求哈夫曼编码部分的可选择替代程序,也就是说,可以将算法6.13看成是算法6.12的求哈夫曼编码部分。
)
译码的过程是分解电文中字符串,从根出发,按字符‘0’或‘1’确定找左孩子或右孩子,直至叶子结点,便求得该子串相应的字符。
具体算法留给读者去完成。
例6-2已知某系统在通信联络中只可能出现8种字符,其概率分别为0.05,0.29,0.07,0.08,0.14,0.23,0.03,0.11,试设计赫夫曼编码。
设权w=(5,29,7,8,14,23,3,11),n=8,则m=15,按上述算法可构造一棵赫夫曼树如图6.26所示。
其存储结构HT的初始状态如图6.27(a)所示,其终结状态如图6.27(b)所示,所得赫夫曼编码如图6.27(c)所示。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 66 赫夫曼树及其应用+ 赫夫曼树 及其 应用