哈夫曼树(完美二叉树)

引子

先来一个例子

例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。

树的带权路径长度:树中所有叶子结点的带权路径长度之和

        记作 :              WPL=\sum_{k=1}^{n}w_{_{k}}l_{k}     w_{k}为权值  l_{k}  为结点到根的路径长度

       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......

由此可见 因为哈夫曼树不唯一 所导致的哈夫曼编码不唯一 同时也导致译文的结果也是不唯一的

哈夫曼树完结撒花!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值