哈夫曼编码,是一种数据压缩算法,通常用于无损数据压缩。该算法是由 David A. Huffman在麻省理工学院就读理学博士(Doctor of Science)的时候发明的,这位大佬在1952年发表了相关的一篇论文A Method for the Construction of Minimum-Redundancy Codes,有兴趣的朋友可以看看。
最近,我完成了一个银行客户安全管理系统的小组项目,该系统可以对文件进行压缩/解压,加密/解密,在加密和压缩的文件中进行搜索功能,以及根据数据库文件进行排序功能。我实现的部分是哈夫曼编码的压缩(Compression),解压(Decompression)算法,和搜索(Search)算法。现在在这里分享一下自己的学习心得。.
可变长度编码和前缀编码
在提出哈夫曼编码之前,我们先了解一下什么是可变长度编码和前缀编码。
当一个字符在文本中出现的频率比其他字符更频繁的时候,我们可以通过一种算法,这种算法可以使该字符以更少的位数(bit)表示相同的一段文本,这种方式即为可变长度编码(variable-length encoding)。比如,有些字符可以只用一一个二进制数(0或1)表示,有些字符用两个二进制数表示(01或10)。
假设有一个字符串"aaabbc",其中字符’a’,‘b’,'c’出现的频率依次是3,2,1,我们根据频率先随机给这四个字符定义四种编码,如下所示:
在编码后,这个字符串可表示为0001010010 (0 |0 |0 |10 |10 |010),看似对文本进行了压缩,当你对其解码的时候,会出现模棱两可的情形,如下所示:
0|0|0|10|10|010
aaabbc
0|0|0|10|10|0|10
aaabbab
为什么会出现歧义呢?因为该编码没有满足前缀编码原则(prefix rule),即一个字符的编码不能是另一个字符编码的前缀,根据上表,由于字符a的编码0是字符c的编码的前缀,所以在解码过程中会产生歧义。当遇到010编码的时候,就可能解码成字符ab(0|10)或者c(010)。
哈夫曼编码
哈夫曼编码(Huffman-Coding)是一种同时满足可变长度编码和前缀编码原则的算法。它可以通过连接具有不同权重的不同节点来构造哈夫曼树(最优二叉树),其中权重最小的节点远离根节点,权重最大的节点更靠近根节点。权值是根据字符在文本中出现的频率来决定。
算法步骤
哈夫曼树的构造采用自下而上的方法。
- 初始化所有叶子节点,每个叶子结点代表一个字符,其权重表示每个字符在文本中出现的频率。
- 每次选择权值最低的两个节点组成一个新节点,新节点的权重等于左右两颗子树的权重之和。
- 将这个新节点与其他叶节点进行比较,选择权重最小的两个节点(包括该新节点但是不包括其孩子结点),再构造成一个新节点。
- 重复步骤3,直至得到一颗完整的哈夫曼树。
举例
假设字符’a’,‘b’,‘c’,‘d’,'e’在文本中出现的频率依次为6,9,7,3,5
初始化所有叶节点如下:
选取两个最小权重的节点,生成一个新的节点,这里最小权重分别是3和5,因此生成一个权重为8的新节点。
接着继续选择两个最小权重的节点,由于3和5已经生成了新节点,所以不需要再拿3和5与其他节点进行比较。比较8,6,9,7,可知6和7最小,因此生成一个权重为13的新节点:
再拿剩下的节点8,13,9进行比较,发现8和9最小,因此生成一个权重为17的新节点:
最后将13和17生成一个权重为30的新节点,构成一颗完整的哈夫曼树如下,
根据哈夫曼编码”左0右1“的规则,依次表示路径和字符如下:
根据生成的哈夫曼树,我们可以通过根节点到叶子节点的路径得到每个字符的哈夫曼编码:
字符 |
编码 |
a |
00 |
b |
11 |
c |
01 |
d |
100 |
e |
101 |
因此,原始的字符串就可以根据这个哈夫曼编码字典一一生成对应的编码,获得压缩后的编码。
代码运行
完整项目代码可参考我的github,其中文件project_dev1.c和project_dev2.c分别为哈夫曼编码的压缩和解压算法。
运行环境:
Linux系统,C90语言规范。
命令行指令:
make:执行makefile文件里面所有源代码的编译指令。
make clean:清除所有所有编译文件。
make cleanf:清除所有项目运行中产生的中间文件,比如压缩或者加密后的文件。
./main.out: 执行主程序
已给定的数据库文件:”Customer.txt",数据库文件内容如下所示:
运行步骤:
- 命令行输入make编译所有源代码。
- 命令行输入./main.out运行主程序。
- 在用户界面选择"1. I want to compress the file"
- 根据提示选择"1. I Efficient Compression",由于选项二是行程长度压缩算法(Run Length Encoding),相对而言没有哈夫曼编码压缩效率高(尤其在应对重复字符时)。接着输入你要解压的文件,这里可以压缩示例文件"Customer.txt"。或者用户自己创建文件进行压缩。
- 解压时终端会输出根据哈夫曼树生成的哈夫曼编码以及压缩前后的二进制数,如下所示:
- 运行完后,你可以在文件夹目录下看到两个导出的文件,其中"Huffman_Codes.txt"是压缩后的哈夫曼编码,"HFT_Model"是哈夫曼树模型,这个模型在解压缩的时候用得上。
- 回到用户界面,选择"3. I want to decompress the file",然后输入你要解压缩的文件名"Huffman_Codes.txt",如下所示:
- 该程序会将解压后的文件内容输入到终端上:
并且导出解压缩后的文件"HFT_Decompression.txt"到目录中:
- 回到用户界面,你可以使用其他功能或者直接输入-1退出程序。然后依次输入make clean 和 make cleanf清楚编译文件和程序运行中产生的中间文件"Huffman_Codes.txt",“HFT_Model"和"HFT_Decompression.txt”。