一:八卦
在《算法为什么这么难?》这篇博客里,刘未鹏讲了一个八卦:
根据wikipedia的介绍,霍夫曼同学(当年还在读Ph.D,所以的确是“同学”,而这个问题是坑爹的导师Robert M. Fano给他们作为大作业的,Fano自己和Shannon合作给出了一个suboptimal的编码方案,为得不到optimal的方案而寝食难安,情急之下便死马当活马医扔给他的学生们了)当年为这个问题憔悴了一个学期,最后就快到deadline的时候“忽然”想到二叉树这个等价模型,然后在这个模型下三下五除二就搞定了一篇流芳千古的论文,超越了其导师。
霍夫曼使用自底向上的方法构建二叉树,避免了次优算法Shannon-Fano编码的最大弊端──自顶向下构建树。1952年,霍夫曼在麻省理工攻读博士时发表了《一种构建极小多余编码的方法》(A Method for the Construction of Minimum-Redundancy Codes)一文,它一般就叫霍夫曼算法。
二:分析
在英文中,e的出現機率最高,而z的出現概率則最低。當利用霍夫曼編碼對一篇英文進行壓縮時,e極有可能用一個位元來表示,而z則可能花去25個位元(不是26)。用普通的表示方法時,每個英文字母均占用一個字節(byte),即8個位元 。二者相比,e使用了一般編碼的1/8的長度,z則使用了3倍多。倘若我們能實現對於英文中各個字母出現概率的較準確的估算,就可以大幅度提高無損壓縮的比例。
为了方便讲述,我们说编码前的是字母流,编码后的是比特流。然后某字母对应的几个比特就是它的编码,学名叫前缀码。这个编码需要满足两个条件:
1. 出现概率大的字母编码短,概率小的字母编码长。
2. 编码后的比特流可以唯一还原成字母流。
比如按照上面的表:0,101,100,111,1101,1100是a,b,c,d,e,f的前缀码,那么比特流001011101就可以毫无歧义的还原成0.0.101.1101,就是aabe。
怎么满足这两个条件呢?霍夫曼设计了一种方式:二叉树,因为二叉树的两个分叉可以代表0和1,而且树又是无环的,不会产生歧义。
用树来表示的话,霍夫曼编码的cost公式可以这么表达:cost=Σ freq(i) * depth(i),i是所有的叶子节点。
但是如果我们假设所有的父节点的频率都是两个子节点的和的话,那么,cost公式也可以表达为:除了根节点外的所有节点的频率和。
这样,假设所有字母的概率值组成一个集合Q,其中Fi,Fj分别是两个最小的概率,那么cost(Q)=Fi+Fj+cost(Q'),Q'就是Q去掉Fi和Fj并加入概率为(Fi+Fj)的新节点后组成的新集合。
上图中,红框中的节点组成的集合就是Q’。
三:代码
n=|C|
Q=C
for i=1 to n-1
allocate a new tree node z
z.left=x=EXTRACT-MIN(Q)
z.right=y=EXTRACT-MIN(Q)
z.freq=x.freq+y.freq
INSERT(Q,z)
return EXTRACT-MIN(Q)
其中的EXTRACT-MIN(Q)就是从队列Q中取最小数,可以用最小堆。
通过每次都取最小的,最终得到了一个可用的解,这就是贪心算法。
假如Q是由二叉堆实现的,那么,建堆使用的时间是O(n),每次调整堆使用的时间是O(lgn),共调整了n-1次。
总之时间复杂为O(nlgn)。