1951年,Huffman在MIT信息论的同学需要选择是完成学期报告还是期末考试。导师Robert M. Fano给他们的学期报告的题目是,寻找最有效的二进制编码。由于无法证明哪个已有编码是最有效的,Huffman放弃对已有编码的研究,转向新的探索,最终发现了基于有序频率二叉树编码的想法,并很快证明了这个方法是最有效的。由于这个算法,学生终于青出于蓝,超过了他那曾经和信息论创立者香农共同研究过类似编码的导师。Huffman使用自底向上的方法构建二叉树,避免了次优算法Shannon-Fano编码的最大弊端──自顶向下构建树。
以上是我从百度百科上的摘抄。我们大致可以从中了解赫夫曼编码的历史以及其根本作用——有效地压缩数据。需要多说一句的是,对于Huffman编码的中文译名有大概三种:“哈夫曼”、“霍夫曼”、“赫夫曼”。其实都是同一个人,同一种编码了。本文中,我采用《算法导论》中的译名——赫夫曼。
定长编码与变长编码
好了,言归正传。先看看如果需要对字符型的数据文件编码,在赫夫曼编码产生之前是怎样做的。假设现在有一个10万字符的数据文件,它只出现了6个不同的字符:a,b,c,d,e,f,他们出现的频率,经统计,如下表所示:
a | b | c | d | e | f | |
---|---|---|---|---|---|---|
频率(千次) | 45 | 13 | 12 | 16 | 9 | 5 |
定长编码 | 000 | 001 | 010 | 011 | 100 | 101 |
变长编码 | 0 | 101 | 100 | 111 | 1101 | 1100 |
既然是有6个字符,那么,根据二进制编码的规律,3bit的二进制串就可以把这6个字符完全表示出来,因为3bit的二进制串最多可以表示8个互不相同的项目。表示出来的编码形式就是表中“定长编码”那一行所示。
这么一来,通过计算得知,我们按照这种方法编码文件,一共生成了300000个bit. 再来看看表格最后一行的变长编码,它的基本逻辑是这样:对字符采取不定长的编码方式,其中,对于字符频率越高的字符,用相对较短的编码;而对于字符频率越低的字符,则采用较长的编码。
这样,即便变长编码时,有些字符的编码长度超过了定长编码的情况(比如这里e和f的编码长度都是4,大于3),但总体来说,还是相当节省空间的。比如,表格中的例子,经过计算,变长编码的文件大小是224000bit,比起定长编码,大约节省了1/4.
好了,接下来,进入主题,这种高效的变长编码,也就是赫夫曼编码是如何实现的?
赫夫曼树的构造
首先,补充一个概念——前缀码。它指的是这样一种编码方式:没有任何字符是其他字符的前缀。所以,从语义上理解,好像叫“非前缀码”要更贴切一点,但是这个名字已经是业内标准,也只能这么叫了。赫夫曼编码正是这样一种前缀码。也因为它的这种性质,我们在对赫夫曼编码的文件解码时,不会对首个字符产生歧义,以此类推,就能顺利解码了。
这里,说解码的原因是告诉大家,不用担心这种变长编码的解码问题。
至于如何构造赫夫曼编码,关键就在于构造赫夫曼树。赫夫曼树,是一棵满的二叉树(所谓“满”是指它的任何一个非叶节点都有两个孩子),它采用的是一种从底置顶,自下而上的构建方式。具体如下:
(1) 拿上面表格中的数据为例,我们现将所有字符根据其频率按升序排序:我在这里干脆写成了Python中字典的形式,方便观察。
{'f': 5, 'e': 9, 'c': 12, 'b': 13, 'd': 16, 'a': 45}
(2) 为这个排好序的字典的每个条目,生成一个树节点,树节点类这样构造:
class HuffmanTreeNode