引子
先来一个例子
例1:编一个程序,将学生的百分制成绩转换成五分制成绩 要求如下
<60:E 60-69:D 70-79:C 80-89:B 90-100:A
很显然 可以使用switch语句或者if语句实现,这里使用if语句进行讲解
代码:
if(score<60)
grade='E';
else if (score<70)
grade='D';
else if(score<80)
grade='C';
else if(score<90)
grade='B'
else
grade='A';
将此串代码转换为二叉树:
这种用于描述分类过程的二叉树叫做判断树
判断的过程用判断树可以很清晰的描述出来
若学生的成绩数据共10000个;分布如下图
如图
E类型的占5%,且每次都需要比较1次(是否为<60)
所以一共需要比较10000*5%*1=500次
D类型的占15%,且每次需要比较2次(先判断是否为60,再判断是否为70)
所以一共需要比较10000*15%*2=3000次
照葫芦画瓢
C的次数为:10000*40%*3=12000次
B的次数为:10000*30%*4=12000次
A的次数为:10000*10%*4=4000次
所以10000个数据比较的次数表达式为:
10000*(5%*1+15%*2+40%*3+30%*4+10%*4)=31500次
如果每次的输入量很大 就应考虑程序的操作时间
怎么样才能让程序的比较次数更少呢?
我们假设将判断的二叉树变成下图
这时我们发现:E(<60)和D(<70)这两种情况只需要比较三次
C(<80)和B(<90)和A(>90)的三种情况只需要比较两次
因此10000个数据比较的总次数为:10000(3*20%+2*80%)=22000次
显然 这两种判断树的效率是不一样的
我们发现,同样一个程序,只需要把操作的顺序改变一下,就能把比较次数大幅减少,提升程序效率
问题来了 那我们能不能找到一种效率最高的判断树呢?
这就引入了我们的哈夫曼树 也称最优二叉树
哈夫曼树的基本概念
路径:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
结点的路径长度:两结点间路径上的分支数。
例:这是一个九个结点的树
(a)
A>D的路径长度为? A>D之间有A>C C>D这两个分支 所以A>D的路径长度为2
C>I的路径长度为? C>I之间有C>E E>G G>I 三个分支 所以C>I的路径长度为3
(a)从A到B,C,D,E,F,G,H,I的路径长度分别为 1,1,2,2,3,3,4,4。
同样是9个结点的树
(b)
(b)从A到B,C,D,E,F,G,H,I的路径长度分别为 1,1,2,2,2,2,3,3。
树的路径长度:从树根到每一个结点的路径长度之和。记作:TL
还是用刚刚那两张图
TL(a) = 0+1+1+2+2+3+3+4+4=20 (根结点到自身为0)
TL(b) = 0+1+1+2+2+2+2+3+3=16 (根结点到自身为0)
包含结点个数相同的两棵二叉树可能树的路径长度不一样
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
但路径长度最短的 不一定是完全二叉树
权(weight):将树中结点赋给一个有这某种含义的数值,则这个数值称为该结点的权
如成绩<60的占5% 则5%就是这个结点的权
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
打个比方,如图
H的权重是2,则H结点的带权路径长度为A>H的路径长度 * H的权, 而路径长度为3,所以H的带权路径长度为2*3=6。
树的带权路径长度:树中所有叶子结点的带权路径长度之和。
记作 :
为权值
为结点到根的路径长度
Weighted Path Length 带权路径长度
例:有4个结点a,b,c,d,权值分别为7,5,2,4 构造以此4个结点为叶子结点的二叉树
(a)
叶子结点a结点的权值为7,根到结点的路径长度为2。
叶子结点a结点的权值为5,根到结点的路径长度为2
叶子结点a结点的权值为2,根到结点的路径长度为2
叶子结点a结点的权值为4,根到结点的路径长度为2
树的带权路径长度=所有叶子结点的带权路径长度之和
所以这棵二叉树的带权路径长度为:(a)WPL=7*2+5*2+2*2+4*2=36
-同样以这四个结点为叶子结点的二叉树
(b)
所以这棵二叉树的带权路径长度为:(b)WPL=7*3+5*3+4*2+2*1= 46
所以这就引出了我们的哈夫曼树:最优树
哈夫曼树:最优树 带权路径长度(WPL)最短的树
注:"带权路径长度最短"是在"度相同"的树中比较而得的结果,因此有最优二叉树、最优三叉树之称等等
哈夫曼树:最优二叉树 带权路径长度(WPL)最短的二叉树
因为构造这种数的算法是由哈夫曼教授于1952年提出的,所以被称为哈夫曼树,相应的算法被称为哈夫曼算法
已经算出这两种二叉树的带权路径长度为(a)WPL=36与(b)WPL=46了
(a) (b)
我们现在又引入二叉树c和d,同样的四个叶子结点的二叉树计算他的WPL
(c) (d)
(c)WPL=7*1+5*2+4*3+2*3=35
(d)WPL=7*1+5*2+4*3+2*3=35
我们发现 二叉树c与二叉树d的带权路径长度都为35,比a和b二叉树的带权路径长度少
二叉树c和d就称为最优二叉树 也就是哈夫曼树
总结:
由二叉树a我们可以发现:
满二叉树不一定哈夫曼树
将二叉树c和d与二叉树a与b作对比我们可以发现
哈夫曼树里权值越小的叶子离根结点越远 权值越大的叶子离根结点越近
由二叉树c和二叉树d两棵哈夫曼树我们可以发现
具有相同带权结点的哈夫曼树并不惟一
哈夫曼树的构造算法
已知哈夫曼树中权越大的叶子结点离根越近 我们可以利用这一特点可以用来构造哈夫曼树
贪心算法:构造哈夫曼树时首选选择权值最小的叶子结点(哈夫曼算法属贪心算法中的一种)
哈夫曼算法(构造哈夫曼树的方法)
(1)根据n个给定的权值{w1,w2,...,wn}构成的n棵二叉树的森林
F={T1,T2,...,Tn},其中Ti只有一个带权为Wi的根结点。->构造森林全是根
(2)在F中选取两棵根结点的权值最小的树作为左右子树,构造一课新的二叉树,且设置新的
二叉树的根节点的权值为其左右子树上根结点的权值之和。->选用两小造新树
(3)在F中删除这两棵树,同时将新得到的二叉树加入森林中。->删除两小添新人
(4)重复(2)和(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树。->重复(2)(3)剩单根
例:有4个结点a,b,c,d,权值分别为7,5,2,4,构造哈夫曼树
第一步:构造森林全是根 第二步:选用两小造新树
将四个结点构造成森林 作为4个根 将四个根中最小的2,4取出来,并为他们添 上双亲结点,双亲结点的权值为两子树之和
第三步:删除两小添新人 第四步:重复(2)(3)剩单根
将之前取走的2与4根结点删除 将第二步与第三步进行重复使用
并为其添加使用2与4结点造的新树 最后只留下一个根结点的树,就是哈夫曼树
我们可以发现 哈夫曼树结点的度数为0或者2,没有度为1的结点
所以包含n个叶子结点的哈夫曼树中一共有2n-1个结点
为什么是2n-1个结点呢?
包含n棵树的森林要经过n-1(根结点两两合并)次合并才能形成哈夫曼树,
共产生n-1(每一次合并都会产生一个双亲结点)个新结点
度为0的结点为n个 度为2的结点为n-1个 所以n+n+1=2n+1 故此得证
例:有5个结点a,b,c,d,e权值分别为7,5,5,2,4,构造哈夫曼树
第一步:构造森林全是根 第二步:选用两小造新树
将五个结点构造成森林 作为5个根 将五个根中最小的2,4取出来,并为他们添 上双亲结点,双亲结点的权值为两子树之和
第三步:删除两小添新人 第四步:重复(2)(3)剩单根
将之前取走的2与4根结点删除 将5与5组成新树 并非5与6(易踩坑)重复(2)(3)
并为其添加使用2与4结点造的新树 最后只留下一个根结点的树,就是哈夫曼树
结果为
易错:容易惯性思维,在2与4组成新树后,将5跟2与4的新树组合
更正:应该将5与5组成新树 第二步选用两小造新树 哪怕是权值相同的也是可以的
谨记谨记容易踩坑
总结:
1.在哈夫曼算法中,初始时有n棵二叉树,要经过n-1次合并最终形成二叉树
2.经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点。
可见:哈夫曼树中共有n+n-1=2n-1个结点,且其所有的分支结点的度均不为1。
哈夫曼树构造算法的实现
所有的算法应该考虑如何存储 再来考虑实现
在这我们可以使用顺序存储结构或者链式存储结构
我们采用顺序存储结构 比链式存储结构来得简单
√ 采用顺序存储结构 —— 一维结构数组
顺序存储结构:也就是用一个数组来存放数据
√结点类型定义
weight:权值 parent:双亲 lch:左孩子 rch:右孩子
(1)如图:数组大小为2n-1 我们不使用0下标 因此数组大小为2n。
数组为什么大小为2n-1?
原因:我们在构造哈夫曼树的时候已经学过 n个结点 我们需要进行n-1次合并 每一次合并都产生一个新结点 所以一共产生n+n-1=2n-1个结点 所以数组大小为2n-1
(2)其次用这些数据类型来定义一个哈夫曼树的指针类型 Huffman Tree
(3)再用这个指针类型定义一个H
那么这个H就可以表示一个指针,也可以表示一个数组
例如,第一个结点权值为5 ,即可表示为H[1].weight = 5; i个结点就为H[i]
如图
这就是用我们的一维的结构数组来存储
那我们怎么将这些数据的双亲是谁 孩子是谁给构造出来呢 来看下面这个例子
例,有n=8,权值为W={7,19,2,6,21,3,21,10}用来构造哈夫曼树
n=8个 构造数组的大小就为有8个结点+产生的7个结点 = 15
所以就可以定义一个[16]的数组(0-15) 就可以存1-15个数据 (0不使用)
如图
图1 图2
第一步:将这八个权值填入下标1-8中 ,进行初始化(图1)
第二步:
构造森林全是根
如图,准备构造哈夫曼树 由题意已知n=8 如果设9为i的话 那么i=n+1 又因为n个结
点会进行n-1次合并,所以i<=n+n-1 -> i<=2n-1
所以由i为n+1开始,为n-1结束 可以得到一个for循环:for(i=n+1;i<=2n-1;i++)
(图2)
第三步:
选用两小造新树:
用表中最小的3号结点(权值:2)与6号结点(权值3)组成一个新树,权值为5
新生成的结点的下标为9,是3和6号结点的双亲
第四步:
删除两小添新人
将3与6号结点的双亲结点栏改为双亲所在的下标9
将9号结点的左孩子和右孩子栏分别改为3与6 权值栏改成5
重复选用两小造新树 ,删除两小添新人 两步剩单根
如上,如此反复只剩一个根没有双亲节点时,哈夫曼树就构造完成了
哈夫曼树的构造算法 程序代码
例:设n=8,w={5,29,7,8,14,23,3,11}
试设计 Huffman code(哈夫曼树)
解:已知n=8,那么m=2*8-1=15
所以我们要定义一个大小为16的数组(0-15)但为了方便我们将0不使用,所以为(1-15)
如图:
进行初始化
将前8个结点的权值输入进去 双亲和左右孩子全部置为0
哈夫曼树(HT)的初态:
经过哈夫曼树的构造步骤后形成
哈夫曼树终态:
有了这么一个数组,我们就知道哈夫曼树的形态了
哈弗曼编码
图A
这种固定每个字符对应的二进制长度的称为定长的编码方式
缺点:比较浪费空间,每一个字符都需要两位数字对应,要是不止ABCD四个字符,且每一个字符都需要更多的二进制位与之对应,就会浪费大量空间
所以我们需要将它进行压缩存储
若将编码设计为长度不等的二进制编码,即让待转字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。
如图:
图B
刚刚图A需要14个二进制位的字符 现在图B只需要9位。
但是我们发现里面会有重码
如图
图C
000011010前面的4个0,会被理解成多重意思,如AAAA,ABA,BB,
原因就是A(0)为B(00)的前缀
这就是长度不等的编码的问题
要想设计这种长度不等的编码,则必须使任一字符的编码都不是另一个字符的编码的前缀
——这种编码被称作前缀编码
问题:什么样的前缀码能使得电文总长度最短呢?
—— 哈弗曼编码
方法:1、统计字符集中每个字符在电文中出现的平均频率(频率越大,要求编码越短)。
2、利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的频率只作为权值,构建哈夫曼树。则频率越大的结点,路径越短。
3、在哈夫曼树的每个分支上标上0或1:
结点的左分支标 0 ,右分支标 1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码
例:要传输的字符集D={A,B,C,D,;},
字符出现频率 w={2,4,2,3,3}
如图
然后将左右分支标好0和1
从根节点出发到每个字符所路过的分支上的0或1 就构成了该字符的编码
如图:
例:电文是{CAS;CAT;SAT;AT},那么编码是什么呢?
按照这些字符的编码,可以得电文编码为11010111011101000011111000011000
反之如果编码是1101000
也可以得到电文为{CAT}
哈夫曼编码能保证是前缀编码和保证字符编码总长最短
1.为什么哈弗曼编码能保证是前缀编码
2.为什么哈弗曼编码能够保证字符编码总长最短
因为哈夫曼树是带权路径最短的树,故字符编码的总长最短,权就是他的频率,
字符编码的总长=每个字符编码的位数 (从根结点到字符的路径长度)* 字符出现的次数(频率)
我们又知道:
结点的带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积
有没有发现很相似?
算出来的带权路径最短也就等同于字符编码的总长最短了
所以哈夫曼树完美满足这个条件
因此引出两条性质
· 性质1:哈弗曼编码是前缀码
· 性质2:哈弗曼编码是最优前缀码
最优前缀码:用这种方式进行字符转换我们得到的编码总长是最短的。
例:设组成电文的字符集D及其概率分布W为
D={A,B,C,D,E,F,G}
W={0.40 0.30 0.15 0.05 0.04 0.03 0.03} 设计哈弗曼编码
哈夫曼树不唯一,所以答案不唯一
答案实例:
如图是一个哈夫曼树,以及标好了0和1(左分支画0,右分支画1)
每个字符的编码如图
关于哈夫曼树子树是否用权值区分左右子树
因为哈夫曼树不惟一,自然就不分左右子树,
然而左右子树的区分关系着哈夫曼编码,自然哈夫曼编码也不唯一
我比较习惯写哈夫曼树时权值大的做右子树,权值小的做左子树
当然,只要你严格按照了构造哈夫曼树的方法,左右子树谁大谁小随便你。
哈夫曼编码的算法实现
核心:不断往上找双亲
左0右1标记好
将构造出来的哈夫曼树用结构数组存储起来
那么如何用结构数组找出他们的哈夫曼编码呢?
设找出7号节点的哈夫曼编码
1.取7号节点的parent的值 我们发现是8
那么7号节点是8号节点的左子树还是右子树呢?
可以从8号节点那看出,7号是8号的左子树,因为左0右1的关系,我们标记为0。
2.再由8号节点找双亲节点的值 我们发现是10。
又由10的左右子树得出8号节点是他的左子树,所以我们又得到一个0。
3.照葫芦画瓢,我们继续往上找双亲,10的双亲是11,10是11的左子树,我们得到如图
我们又得到一个0。
4.再找11的双亲是12,11是12的左子树
左0右1原则
我们又得到一个0。
5.最后找到12的双亲是13,12是13的右子树
我们得到一个1。
我们发现13号是根节点,根节点没有双亲节点,标记为0,那这次找哈夫曼编码的行动就能结束了
哈夫曼编码是从上到下 从根出发,到叶子节点这样算的,所以应该是10000,
误区:从下往上算,从叶子节点往根节点算,得到00001。
每个字符都是由若干个0或1组成的哈夫曼编码
N个字符就需要由N个由0或1组成的字符串
HC[i]就是由0或1组成的字符串 具体值需要我们计算
哈夫曼编码是从根到叶子节点的过程中得到,
又因叶子节点不断回溯至根的过程我们会得到若干个0或1
所以将结果翻过来,我们就能得到哈夫曼编码了
我们得到的0或者1需要找一个地方保存起来
这里我们用一个字符数组cd(码的意思)来保存起来
这个数组具体要设计多大的空间呢
我们如果有N个节点,那么最高也只有N-1分支,所以编码也最多只有N-1位,数组长度为N-1就够
我们一共有A-G 7个字符 所以数组长度只需要6个就行了
这里我们将他作为字符串看待,多一个位置用来存放字符串结束标志\0
我们需要倒着放(原因是从节点往根走,如果不倒着放得到的结果并不是哈夫曼编码),所以我们数组的下标从N-1开始
如图
又用结构数组找双亲的方法
得到各个节点的哈夫曼编码
哈夫曼编码的算法实现 程序代码
哈夫曼编码的编码及译码
文件的编码和译码
一、编码
1.输入各字符及其权值
2.构造哈夫曼树——HT[i]
3.进行哈夫曼编码———HC[i]
4.查HC[i],得到各字符的哈夫曼编码
二、解码
1.构造哈夫曼树
2.依次读入二进制码
3.读入0,则走向左孩子,读入1,则走向右孩子
4.一旦到达某叶子节点时,即可译出字符
假设有如下编码 求出原码报文OC
解题思路:
1.先将这五个字母对应的频度作为权值
2.求出哈夫曼树,注意 哈夫曼树不唯一
比如b站王卓老师这个哈夫曼树如图(根节点应为29)
我的图为
我的数据结构老师的图为
因为哈夫曼树的不同,所以从根节点出发的译文结果也是不同的!!
b站王卓老师的结果:
我的结果:
xuyvx......
老师的结果为:
yvw......
由此可见 因为哈夫曼树不唯一 所导致的哈夫曼编码不唯一 同时也导致译文的结果也是不唯一的
哈夫曼树完结撒花!!