前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >哈夫曼树 编码-# 哈夫曼树的应用——哈夫曼编码

哈夫曼树 编码-# 哈夫曼树的应用——哈夫曼编码

作者头像
囍楽云
发布2022-12-29 09:17:54
5830
发布2022-12-29 09:17:54
举报
文章被收录于专栏:囍楽云博客

哈夫曼树 “最优”的二叉树

  我们考虑这样一个要求:把成绩从百分制转为五级制。这样的题目你们大一就懂得做了:

代码语言:javascript
复制
`string hundred_to_five( int grade ){
if( grade >= 90 )
    return "优秀";
else
    if( grade >= 80 )
        return "良好"
    else
        if( grade >=70 )
            return "中等"
        else
            if( grade >=60 )
                return "及格"
            else
                return "不及格"
}
`

  这里为了让逻辑更复合我们讨论的内容采用了这种写法,实际工作中不要学。

  我们可以画出这一过程的判定树(下图a):

  不过这里的逻辑是不唯一的,图b和c也一样可以完成这个逻辑。那么我们很自然的就会考虑,哪种方式的效率是最高的?

  需要明确的一点是效率的高低跟分数的分布有关。

  这点很容易理解,如果我们班级有N个人,考虑非常极端的情况,全班所有人的分数都不及格,我们可以很容易算出三种逻辑的判断次数:

  逻辑判断次数

  a

  4N

  b

  3N

  c

  2N

  如果全班同学的分数都在90分以上,那么我们也有:

  逻辑判断次数

  a

  N

  b

  2N

  c

  3N

  这两种情况下,三种逻辑的效率正好相反。所以不同分数段学生的占比决定了处理逻辑的效率。

  因此这个问题实际上是:在人数已经确定的条件下,如何得到效率最高的逻辑。

  我们注意到,每个逻辑都构成一个二叉树,每个判定节点都是一个非叶子节点(实际上还是一个必然存在左右子树的节点),同时每个判定结果都是一个叶子节点。我们给每个叶子节点一个权重,例如上面的例子中,我们可以用人数作为叶子节点的权重:

  分数分布人数

  优秀

  2

  良好

  4

  中等

  5

  及格

  1

  不及格

  1

  我们以上面(b)的情况来看,会得到这样一棵树:

  我们再定义带权路径长度:

  叶子节点的路径长度为其到根节点所经过的边数。叶子节点的带权路径长度为路径长度与叶子节点权重的乘积。树的带权路径长度为所有叶子节点带权路径的和。

  以上图为例,我们可以求得每个节点的路径长度和带权路径长度:

  叶子节点权重路径长度带权路径长度

  优秀

  2

  2

  4

  良好

  4

  2

  8

  中等

  5

  2

  10

  及格

  1

  3

  3

  不及格

  1

  3

  3

  所以这棵树的带权路径长度为:4+8+10+3+3=28,很显然,带权路径长度就是这个逻辑执行之后得到的总的判断次数,可以定量地表示整个逻辑的效率,带权路径长度越小,判断的次数越少,效率也越高。

  我们称这样树为最优二叉树,或者哈夫曼树。

  那么我们的问题就转变为:给N个节点,如何构造这样一棵哈夫曼树。

  哈夫曼树的构造

  我们观察哈夫曼树的形态哈夫曼树 编码,很容易看出,越大的数字应该放在越靠近根节点的位置,这样路径长度比较短:

  构造这种树的算法是一种很好理解的贪心算法:

代码语言:javascript
复制
1. 将所有的叶子节点放入待选集合S。
选S取出最小的两个节点作为左右子树,构建一棵二叉树,根节点的权为两个节点权之和。
将2中生成的2叉树放入集合
S
,将根节点作为待选节点(也就是说不再考虑叶子节点)。
`

  假设有A B C D E F G这几个节点,他们的权分别是:1 1 4 5 8 9 11,我们看如何构造一棵哈夫曼树:

  整个过程还是很容易理解的,每一回合都取出两个最小的节点,构建一棵新树并放入待选集合。重复整个过程直到只剩下一棵树为止。

  那么我们有一个问题,哈夫曼树唯一吗?其实即便在我们上面的例子中,他也不是唯一的哈夫曼树 编码,因为两个节点都可以选择放在左子树或者右子树,我们称这种树为同构树。

  上图中,黄色的两个节点的左右子树和左侧树对应的节点正好相反(镜像),他们都可以通过上面的生成算法生成,只在相关节点选择时,将左右子树交换位置即可。

  如果排除同构的情况,哈夫曼树唯一吗?我们看下面的例子:

  可以看到,在第二步时,我们有3个相同数值的节点可以选择,根据我们选择的不同,最后生成的树的结构也不同。那么这跟“最优”矛盾吗?

  实际上并不矛盾,因为这两棵树有相同的带权路径长度,所以他们都是最优的,你可以自己计算一下。

  哈夫曼树的应用——哈夫曼编码

  哈夫曼树最经典的应用是哈夫曼编码。在介绍哈夫曼编码之前我们先要介绍下可变长度的编码。

  假设我们有一篇文字需要编码,这篇文字只有ABCDE5个字符。其中A出现1次,和B出现1次,C出现2次,D出现4次,E出现9次。我们要如何编码呢?

  一种比较简单的方法是用等长编码:因为有5个字符,每个字符最少需要3个bits,因此我们可以这样编码:

代码语言:javascript
复制
`A : 000
B : 010
C : 011
D : 100
E : 101
`

  这样我们可以得到整篇文章的编码长度=(1+1+2+4+9)∗3=51(1+1+2+4+9)*3=51(1+1+2+4+9)∗3=51

  但这种编码方式的效率不够高,因为我们在每个编码上耗费了相同的空间开销。而实际上,虽然每个字符都是平等的,但有些字符注定比其他字符更平等:)。这里A只出现1次,但E出现了9次,如果我们给E分配较短的编码,那么即使A的编码长度会因此变长,他也是划算的。

  但这要解决一个问题:即怎么才能将保证可以正确的解码。

  举例来说,如果我们给E的编码为:0,那么剩下的其他字符都不能以0开始:

  比如我们令:

代码语言:javascript
复制
`E=0
D=1
A=01
B=001
`

  当我们收到001时,这段编码解释成EED,或者AD,或者B都是可以的。你区分不出来。

  因此假如有一个字符的编码是xxx,那么所有其他字符都不可以以xxx作为其前缀,我们把这种编码方式称为前缀码。

  所以如下的编码是可以的:

代码语言:javascript
复制
`A=10
B=001
C=110
D=1110
E=0
`

  如下的编码则不可行:

代码语言:javascript
复制
`A=00
B=001
`
代码语言:javascript
复制
`A=10
B=101
`

  我们来看要如何构建这个编码,我们先根据节点的权重(出现的次数)构建哈夫曼树:

  编码时,我们从根节点开始往下走,每走一层就编码一位,向左走为0,向右走为1,我们可以得到如下的编码过程:

  从编码的结果可以看到,没有任何一个编码是其他编码的前缀,比如假如我们收到:

代码语言:javascript
复制
`0001000001
`

  这个编码就是BAC,不会有歧义。

  我们再计算下整篇文章的编码长度:

  字符出现次数编码长度字符占用位数

  A

  1

  4

  4

  B

  1

  4

  4

  C

  2

  3

  6

  E

  4

  2

  8

  E

  9

  1

  9

  总长度等于4+4+6+8+9=314+4+6+8+9 = 314+4+6+8+9=31,显著低于等长编码的51。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档