Huffman压缩真正的C++实现

  关于Huffman编码的规则不多说了。
  首先谈一下用C++实现Huffman 压缩 的过程。
  (1)进行字符频数统计,读文件一般按字节读入,每个字节是8bit,可以开一个比2^8 = 256大一些的数组,如freq[260] = {0},然后每读入一个字符,对应的freq[index]++即可。
  (2)根据统计结果构造Huffman树。可以选择二叉树,也可以选择数组存储Huffman树。这里选择了二叉树。构造过程可以使用priority_queue辅助,每次pq.top()都可以取出权值(频数)最小的节点。每取出两个最小权值的节点,就new出一个新的节点,左右孩子分别指向它们。然后把这个新节点push进优先队列。
  (3)根据Huffman树构造Huffman编码表,可以使用map容器存储。
  (4)根据编码表将文件编码,并写入到输出文件。

  只有这些,想要实现Huffman压缩文件并不够,因为压缩的文件需要解压缩。这就要求压缩文件要有特定的格式,如果只是将Huffman编码写入到压缩文件,解压程序并不知道原Huffman树是什么样的,所以无法进行Huffman编码的decode。所以我们不仅要将Huffman编码写入到文件中,也需要将Huffman树也存入到压缩文件中,使得在解压的时候能够首先重建出Huffman树。
  如何存储Huffman树,一种方法是将原字符及频数写入到文件中。如

a 5000
b 20000
c 8000
d 4000
e 27000
......

  意思是字符a在文件中出现了5000次,b出现了20000次。省略号代表对源文件编码后的Huffman编码。根据字符及其频数,跟压缩程序一样,在解压过程中我们就可以重建Huffman树。
  第二种方法是直接将字符及其Huffman编码写入到文件中,如

a 1100
b 111
c 1101
d 0
e 10
......

  因为第二种方法比较直观,可以直接根据huffman编码就左0右1的“生成”一棵Huffman树来,所以选用了第二种方法。然而读者已经发现问题了,在读这些字符及编码的时候,并不知道什么时候结束啊?所以我们可以在头部插入一个节点数,来指明在适当的时候停止读取。即在第一行插入一个数字5,如

5
a 1100
b 111
c 1101
d 0
e 10
......

  如何存储Huffman树的问题解决了,那到底如何存储Huffman编码呢?肯定不是写入01010串啊,这样编码比源文件还长,还谈什么压缩。众所周知,程序在写文件的时候,都是按Byte写入的。Huffman编码是由0和1组成的,我们可以把Huffman编码每一位看成一个bit,所以就我们在写文件的时候,要将每8位bit组成一个Byte,然后写入到文件。深入分析一下,这就带来一个问题,如果源文件的整个Huffman编码的长度并不是8的倍数怎么办?有些同学说,用0或1补齐啊。对,当然要补齐,但是又会出现新的问题。
  举个例子,假设a,b,c,d,e 这5个字符的Huffman编码就如上所示。文件尾部字符为“abcd”,对应编码即为110011111010。共12bit,前8bit为11001111,后4bit为1010,如果我们将后4个bit以0补齐,10100000。那么在解码的时候,很可能就会解码成“abcddddd”,对,就是补齐位的每一个0都解码成字符d。因为我们不知道在尾部的8bit中,哪些是补齐的0,哪些是真正的Huffman编码。
  为了解决这个问题,我们可以在头部再插入一个“补位数”,如刚刚补了4位,我们就存储一个4在压缩文件中。如

5
a 1100
b 111
c 1101
d 0
e 10
4
......

  可是在实际操作过程中,因为不可能在内存中存储大量的Huffman编码,所以我们一般是边读取源文件,边进行编码的,只有到编码的最后才能知道有多少位需要补齐。所以就得先编码一遍,计算补位数,将补位数写入,再重新进行编码,这样过程感觉很繁琐。
  第二种方法是储存一个总字符数量。这样我们就能知道解码多少个字符之后就停止,这样后面补0补1度不影响了。比如上面的例子中,一共有100000个字符,我们就存储一个100000到压缩文件中。如
  

5
a 1100
b 111
c 1101
d 0
e 10
100000
......

  这就比第一种方法好一点。但是我们又可以分析一下,总字符数量是一个不确定的东西,总有可能溢出int、long、long long。所以总感觉很危险,当然有BigInt或者自己实现的大数类可以使用。但是我们可以再思考另一个方法,使用pseudo-eof,伪结束符。
  首先什么是结束符,比如我们在读文件的时候,使用输入流的eof()函数来判断是否读文件结束。这里的eof()实际上会返回一个-1,来说明读取的结束。所以所谓结束符,就是你在正常读取的时候一定不会读到的字符。
  那么我们在正常读文件的时候,在按Byte读取的情况下,什么值不会遇到?没错,比Byte还大的字符。可是比Byte还大,那还怎么用字符表示。这里我们一定不要被字符这个东西给限制了,所谓字符就是一个8bit的二进制流,9bit能不能表示一个字符?当然能,我比你大,还不能表示你?所以我们可以把读入的字符转换成int类型,用int类型来表示所有可能出现的字符,即包括pseudo-eof在内的十进制为0~256的所有字符。比如255的unsigned char是0xFF,转化成int就是0x000000FF,实际值是不变的。那么自然pseudo-eof的int值用16进制表示就为0x00000100
  需要注意的是,这个pseudo-eof也需要Huffman编码,这样我们解码时在遇到这个“字符”时就知道解码可以结束了。既然需要Huffman编码,我们就需要手动向频数数组中插入一个“频数为1,字符id为256”的字符,然后再对所有的字符(包括pseudo-eof)进行Huffman树的构建。并在写入Huffman编码的最后,插入pseudo-eof的Huffman编码。当然,不要忘了补齐8位,此时补0补1都没什么影响了,因为解码遇到pseudo-eof时就已经停止解码了。
  写到这里我又想到一个问题,既然第二个方法中有考虑到总字符数量溢出的问题,那么每个字符的频数会不会也会溢出呢?说不定会,就不能用int freq[20]这样的数组了,这里就先不考虑了……真正投入到工业生产环境中使用肯定是需要改进的。这里就当先介绍一下实现的基本思想就是了。

  附实现的代码及运行结果。
  

/**
 * huffman.h
 */
#ifndef _huffman_h
#define _huffman_h

#include <cstdlib>
#include <string>
#include <string.h>
#include <queue>
#include <map>
#include <fstream>

using namespace std;

#define MAX_SIZE 270
#define WRITE_BUFF_SIZE 10
#define PSEUDO_EOF 256

struct Huffman_node
{
    
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值