写在前面
Huffman压缩原理其实挺好理解的,我用java很快就写好了。然后用c++写,一开始我是这么想的:c++偏底层,应该对二进制串文件的读写会更简单吧。
不涉及到文件读写的部分确实很快就做好了,然后就被文件读写折磨。
各种深夜痛哭... ...
但还是值得的,学习了更多底层的知识。我对Huffman压缩基本掌握了。(本来想说完全掌握的,但,呵,生活。微笑)
写这篇博客花了我很长时间,我完全尽力了。我尽可能详细地写了三部分,分别是Huffman原理、坑和c++代码实现。我举了一些例子,并且经过实际动手验证,还自行绘制了几幅图帮助理解。
欢迎指错和讨论交流,也欢迎提问质疑,尽管能力有限,但我尽力解答。
目录
一、Huffman压缩
1、文件在计算机中存储形式
2、Huffman压缩算法原理
3、例子解析
二、需要特别注意的坑
1、windows的'\r\n'问题,c/c++可以用二进制方式读取来解决
2、文件读取末尾问题
三、Huffman的实现(上代码,本文c++版本,如果要java版本请私聊我)
编码篇
1、统计频率
2、建立Huffman树
3、获取Huffman编码表
4、编码
译码篇
1、获取Huffman译码表
2、译码
正文
一、Huffman压缩
1、文件在计算机中存储形式
在开始实现之前,我们要了解下计算机底层的编解码。
计算机只认0与1,一切文件最终的存储形式都是0、1串。文本、图片、视频等文件都是通过一定的协议进行编解码,而 这些协议及其转化由各种各样的软件(音频软件、画图软件、记事本等)实现。 下图是一张png图片文件在计算机中存储形式, 可以通过Binary Viewer 软件进行查看。
文件的存储有定长存储和不定长存储,定长存储就是int、char、byte等类型会以固定的位数b进行存储。举个例子,我们新建txt文件,写入“4 中”然后敲回车换行,再写入“16”,保存。现在我们用Binary Viewer查看它在计算机中的存储。
因为我们是用文本文件进行存储,所以记事本的编解码方式是将每一个字符对应的ASCII码写入文本文件,如果是0-127的只有8位,如果是中文这样的拓展字符则有16位。
我们看图吧,第一个字节(8位)“00110100”转化为10进制为52, 查ASCII表为‘4’,接下来的第二个字节“00100000”代表空格,接下来的两个字节是‘中’,然后依次是‘\r’,‘\n’,‘1’,6’。这里特别强调下,Windows系统用‘\r\n’表示换行,而Liunx用‘\n’,Mac用‘\r’。为什么会有如此差异呢?感兴趣的自行百度,这个跟早期打印机有关,现在只是一个规则,并没有实际含义,但要特别特别注意 。我写Huffman的时候被这个坑了,具体我们后面再说。我强烈建议写压缩的时候用上查看二进制串的工具(比如Binary Viewer),有利于理解和debug。
2、Huffman压缩算法原理
我们上边提到了计算机的定长存储,其实我们也看到了存储‘4’这样的数字,计算机用了8位,那么我们能不能减少二进制串呢,从而来实现压缩?有很多压缩算法,他们主要是用新一套的编解码表来实现。而每次所用的编码表都是不同的,是依据压缩的文件来决定的。
那Huffman是怎么做的呢?它先通过对要压缩的文件进行统计频率,比如在“65da as 美65a”中a-3(表示a出现3次),d-1,s-1,6-2,5-2,美-1,空格-2。Huffman采用不定长进行存储,频率高的对应的编码长度较短,频率低的对应的编码长度较长。但我们压缩后是要能解压的,假如有这样的一组编码“00101110”,Huffman压缩算法每次读取一位,直至找到在Huffman编码表中找到,然后去除这一串,接着重复以上操作,直至编码读取完毕。要实现这样的结果,我们要怎么创立Huffman编码表呢?以下是具体做法:(边看例子辅助理解)
(1)先对压缩文件的字符进行频率统计,以“字符--频率”的形式存入某容器m
(2) 在容器m中取出两个频率最小对应的字符,作为二叉树的两个叶子节点,并将频率和作为它们的根节点,同时将新结点存入容器m,将旧的两个结点踢出容器m。(容器m可以是优先队列)
(3)重复(2),直到最后容器m中只有一个元素。
(4) 将形成的二叉树的左节点标0,右节点标1。把从最上面的根节点到最下面的叶子节点途中遇到的0,1序列串起来,就得到了各个符号的编码。
3、例子解析
例子:有一串“cdbedfaabca”,进行Huffman编码和解码。
编码: (1)频率统计 f:1 e:1 d:2 c:2 b:2 a:5
(2) f与 e作为叶子结点,其根节点为_2 。 此时,新的频率表为:_2 d:2 c:2 b:2 a:5
d