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

2022-12-29 09:17:54 浏览数 (1)

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

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

代码语言: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。

0 人点赞