定义
我们现在来学习一种能够大幅压缩自然语言文件空间(以及许多其他类型文件)的数据压缩技术。
它的主要思想是放弃文本文件的普通保存方式:不在使用7位或8位二进制数表示每一个字符,而是用较少的比特表示出现频率高的字符,用较多的比特表示出现频率低的字符。
简而言之,不在用ASCII编码表示,而是用较短的前缀码表示。
前缀码
什么是前缀码?如果所有字符编码都不会成为其他字符编码的前缀,符合这种规则的叫前缀码。
比如,如果A的编码为0,R的编码为00,那么A的编码是R的前缀,这不属于前缀码。那么前缀码的例子是什么样的呢?如下:
所有的前缀码的解码方式都和它一样,是唯一的。因此前缀码被广泛应用于实际生产中。注意,像7位ASCII编码编码这样的定长编码也是前缀码。
霍夫曼单词查找树
表示前缀码的一种简便方法就是使用单词查找树。
任意含有M个空链接的单词查找树都没M个字符定义了一种前缀码方法:我们将空链接替换为指向叶子结点(含有两个空链接的结点)的链接,每个叶子结点都含有一个需要编码的字符。这样,没个字符的编码都是从根结点到该结点的路径表示的比特字符串,其中左链接表示0,右链接表示1.
如果构造这样一棵树?
首先,树的结点包含left和right,和一个字符频率变量freq,以及字符ch。以下为构造一颗霍夫曼单词查找树的过程:
- 将需要被编码的字符放在叶子结点中并在每个结点中维护了一个名为freq的实例变量来表示以它为根结点的子树种的所有字符出现的频率。
- 创建一片由许多只有一个结点(即叶子结点)的树所组成的森林。每棵树都表示输入流的一个字符,每个结点中的freq表示它在输入流中的出现频率。
- 找到两个频率最小的结点,然后创建一个以二者为子结点的新结点(新结点的频率为它的两个子结点的频率之和)。
- 不断重复第3过程,最终所有的结点会被合并为一颗单独的单词查找树。
特点:
- 树的叶子结点包含freq和字符。
- 频率高的离根结点最近,频率低的在树的底层。
- 根结点频率值等于输入中的字符数量。
- 该树表示的编码压缩比其他树更多,是一种最优的前缀码
压缩
对于任意单词查找树,都能产生一张将树中的字符和比特字符串(用由0和1组成的String字符串表示)相对应的编译表。其实就是字符和它的比特字符串的符号表。在这里我们用st[]数字表示。在构造符号表时buildCode()递归遍历整棵树,并为每个结点维护一条从根结点到它的路径所对应的二进制字符串(左链接表示0,右链接表示1)。到达一个叶子结点后,就将结点的编码设为该二进制字符串。如下图的编译表:
然后,压缩就很简单了,只需要在其中找到输入字符所对应的编码即可。
上图字符串ABRACADABRA!的编码为:首先是0(A的编码),然后是111(B的编码),然后是110(R的编码),最后得到完整编码为0111110010110100011111001010.
解压
首先readTrie()将霍夫曼单词查找树编码为的比特流构造为霍夫曼查找树。然后读取霍夫曼压缩码,根据该编码从根结点向下移动(读取一个比特,为0移动到左结点,为1移动到右结点)。当遇到叶子结点后,输出该结点的字符并重新回到根结点。
例如压缩ABRACADABRA!后的编码为:0111110010110100011111001010,其单词查找树的比特流为:01010000010010100010001001000011010000110101010010101000010。
首先由树的比特流构造霍夫曼查找树(得如下树),然后解码编码。第一个为0,所以移动到左子结点,输出A;回到根,然后连续三个1,即向右移动3次,输出B;回到根,然后两个1,一个0,即向右移动两次,向左移动一次,输出R。如此重复,最后得到ABRACADABRA!
实现代码
/**
* 霍夫曼压缩
* @author nicholas.tang
*
*/
public class Huffman