霍夫曼编码#

问题定义#

有一种语言,它由四个字母组成 {a, b, c, d}, 我们要把它编码成电报发送出去(电报只有 2 种字符,即二进制{0, 1}), 要怎么编码才最短,才能保证传输效率尽可能高。

一种很直观的想法是,定义 {a=00, b=01, c=10, d=11} ,不过并不能保证这种编码转译出来的编码最短。

霍夫曼编码算法,正式为了解决这个问题。

算法步骤#

霍夫曼算法需要先统计所有字符出现的概率。对于一种语言来说,这个概率一般是固定的,例如汉语中, 字出现很多,而出现的比较少。

统计各个字母出现概率后,接下来思路就是针对出现概率高的字符分配比较短的编码,出现概率低的字符分配长编码。

假设刚才 {a,b,c,d} 出现概率分别为 {0.5, 0.25, 0.125, 0.125}. 我们构造下面的初始节点

graph TD a(a 0.5) b(b 0.25) c(c 0.125) d(d 0.125)

霍夫曼编码就是每次取概率最小的两个节点,合并为一个大节点, 循环往复,直到最后只剩下一个节点。

第一步,合并概率最低的 c/d 节点,形成一个新节点, 新节点的概率等于 c/d 的概率相加(0.25)。

graph BT a(a 0.5) b(b 0.25) c(c 0.125):::dimmed d(d 0.125):::dimmed cd(cd 0.25) classDef dimmed fill:#bbb c --> cd d --> cd

第二步,合并概率最低的 b/cd 节点

graph BT a(a 0.5) b(b 0.25):::dimmed c(c 0.125):::dimmed d(d 0.125):::dimmed cd(cd 0.25):::dimmed bcd(bcd 0.5) classDef dimmed fill:#bbb c --> cd d --> cd b --> bcd cd --> bcd

第三步,现在只剩下两个节点了,把他们合并成一个。

graph BT a(a 0.5):::dimmed b(b 0.25):::dimmed c(c 0.125):::dimmed d(d 0.125):::dimmed cd(cd 0.25):::dimmed bcd(bcd 0.25):::dimmed abcd(bcd 1.0) classDef dimmed fill:#bbb c --> cd d --> cd b --> bcd cd --> bcd a --> abcd bcd --> abcd

得到这棵树后,定义树的左节点是 0,右节点是 1. 就能得到霍夫曼编码

graph BT a(a 0):::dimmed b(b 0):::dimmed c(c 0):::dimmed d(d 1):::dimmed cd(cd 1):::dimmed bcd(bcd 1):::dimmed abcd(root) classDef dimmed fill:#bbb c --> cd d --> cd b --> bcd cd --> bcd a --> abcd bcd --> abcd

所以按照树从上到下的路径,得到:

a = 0
b = 10
c = 110
d = 111

我们可以计算一下这种语言平均编码长度,即每个字符概率乘以他们的编码长度 \(\sum(p_i*len(C_i))\) :

0.5*1 + 0.25*2 + 0.125 * 3 + 0.125 * 3
= 0.5 + 0.5 + 0.375 + 0.375
= 1.75

而我们最初提出的 {a=00, b=01, c=10, d=11} 平均需要 2 字符,霍夫曼编码平均需要 1.75 字符,结果是要好很多的。