摘要
收到疫情的影响,今年包括考研的时间线都往后顺延,因此也多了好多准备的时间。为了较好的掌握c++的语法,以及考虑到Huffman算法的易实现性,我便产生了写huffman压缩的想法。待到真正上手写代码的时候,发现其设计的细节特别多,一度也让我有了放弃的念头!好在都被我一一解决,哈哈!还是挺高兴的。
这个小项目其实说难也不难,但前提是得要熟悉三方面的知识。第一就是最基本的Huffman编码;第二是对数据的移位操作——对二进制数的理解;第三还得熟悉c++(注:或者其他语言)对文件字节流的操作。总的来说思路还是比较清晰的。
Huffman算法
Huffman编码法被称为最优编码法,所获得的编码为无重复前缀码,其编码的依据是根据被编码的字符在整个原文中出现的频率,频率越高,代表该字符的编码长度越短,因此通过编码可以减少文件的大小。
Huffman树的生成过程
- 首先统计出各个字符出现的频率作为权值,然后每个字符对应创建一个节点,节点保存字符值、权值、编码。
- 取出所有节点中权值最小的两个节点(利用堆或者优先队列),将其权值相加后作为新节点的权值,并将新节点作为这两个节点的双亲节点。将该节点重新放入所有节点中。
- 重复 2 步骤,直到剩下一个节点,则所有节点构成一个Huffman树,该节点为树根
1 | template <class T> |
编码过程(Huffman树的遍历)
编码的生成我是利用递归遍历的方法实现的,当然你也可以用BFS。Huffman的规则是通往左子树的路径为0,右子树的路径为1。下面是代码:
1 | //前序遍历huffman树,并生成编码,将节点信息存入到对象中 |
解码过程:
从树根开始,根据Huffman编码0向左,1向右终点便是字符所在节点。但是这种解码方式有个前提就是需要根据文件头信息(编码的时候自己写入),重建Huffman树。
1 | //解析数据 |
文件后缀和编码信息
为了将压缩后的数据解码,我们需要将一些文件的信息,比如文件的大小、文件的后缀名以及编码信息写入压缩文件。我的文件头信息结构如下表:
总字节数 | 文件后缀字节数 | 后缀名 | 编码个数 | 字符 | 出现频次 |
---|---|---|---|---|---|
long long | short | 后缀长度 | short | unsigned char | long long |
总字节数用于后面判断文件结束,但可能因为文件过大,字节数超过long long所能存储的最大数。目前我觉得可以用unsigned long long来存储,或者利用单位换算,降低其数量级(但可能会涉及到精度问题)。由于后缀名具有不确定性,读取时并不确定所占的字节数,所以用2个字节存入后缀名所占字节数。
1 | //写入文件后缀和编码信息 |
文件数据压缩
文件压缩的细节点很多,比如关于c++的ifstream这个东西当时就搞的我很头疼。原来我是用fin.get(in_char);来读取的,但后来绝的每次读取一个字节效率实在太低。因此换用缓冲流来接收。还有关于不够八位的补零处理。虽然我补了零但我觉得直接写入应该也没什么问题,因为我是根据处理的字节总数来判断是否读取结束的。
1 | //写入压缩数据 |
压缩率
关于压缩率,我测试了txt
文件,jpg
文件,和MP3
文件除了txt
偶尔达到50%,其他均高达90%+,毕竟图片文件和mp3
文件都是压缩后的产物,压缩率不高也很正常。
相关文章
关于二进制的介绍和c++文件操作相关,详见