Huffman文件压缩是一种经典的无损压缩方式,刚做完Huffman图片压缩实验不久,写一篇文章总结一下。所用环境为VS2013.
原理
1、所有文件都可以转化为一个一个的字节,例如Java中有字节流,就是通过字节的方式读写文件,C++中也是如此,这也就提供了统计文件中字节权重的方式。
2、可以通过统计到的字节的权重构建Huffman树,从而得到Huffman编码。
3、每个字节是八位二进制,而得到的Huffman编码长度可能小于八位(这时的编码是字符串,可以用Huffman编码替代字符,然后对编码进行位操作,每八位编码转换为一个字符,这样就压缩了空间。
举个例子:
有如下内容的文本:
aaaaaaabbbbbccdddd
a有7个,b有5个,c有2个,d有4个,通过Huffman编码可以得到如下编码:
a 0
b 10
c 110
d 111
用编码代替原文件中的内容:
00000001010101010110110111111111111
长度为35,没八位转换为一个字符,可以得到5个字符(不够补0),而原文有18个字符,这样就达到了压缩的目的。
实现
原理理解起来应该不太难,但要写起来需要注意到的地方挺多的。
首先就是如何统计字符的权重:
void CountWeight(const char* pFilename)
{
FILE *in = fopen(pFilename, "rb"); // rb是以二进制的方式打开
unsigned char ch = fgetc(in);
while (!feof(in))
{
weight[ch]++;
count1++;
ch = fgetc(in);
}
fclose(in);
}
注意字符的类型为unsigned char,还要注意,不要在while循环中输出读到的东西,如果文件有几百kb的话,你会怀疑程序是不是停不下来,然后各种换IO流,不要问我为什么知道。。。
接下来就是建立Huffman树了:
void Huffman(HuffmanTree& HT, int weight[], int n)
{
if (n < 1)
return;
int m = 2 * n - 1; // 结点个数
HT = (HuffmanTree)malloc((m + 1)*sizeof(HTNode));
int i;
HT[0].weight = 0;
HT[0].lchild = 0;
HT[0].rchild = 0;
HT[0].parent = 0;
for (i = 1; i <= n; i++) // 初始化叶子结点
{
HT[i].weight = weight[i-1];
HT[i].lchild = 0;
HT[i].rchild = 0;
HT[i].parent = 0;
}
for (i = n+1; i <= m; i++) // 初始化父结点
{
HT[i].weight = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
HT[i].parent = 0;
}
for (i = n + 1; i <= m; ++i){ // 建树
int s1, s2;
Select(HT, s1,s2,i-1);
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
}
然后是编码:
void HuffmanCoding(HuffmanTree &HT, HuffmanCode &HC, int n)
{
HC = (HuffmanCode)malloc((n + 1)*sizeof(char*));
char *cd = (char*)malloc(n*sizeof(char));
cd[n - 1] = '\0';
for (int i = 1; i <= n; ++i){
int start = n - 1;
for (int c = i, f = HT[i].parent; f != 0; c = f, f = HT[f].parent){
if (HT[f].lchild == c){
cd[--start] = '0';
}
else {
cd[--start] = '1';
}
}
HC[i] = (char*)malloc((n - start) * sizeof(char));
strcpy(HC[i], &cd[start]);
}
}
这些都做完后就能得到Huffman编码了,然后用编码表示文件中的字符,用一个字符串来存储,这个字符串很长很长。
然后就是进行位操作了:
result_out.open(s, std::ios::out | std::ios::app); // 压缩后的文件
int t = 0;
for (int i = 0; i < pBinStr.length(); i++)
{
char b = 0x00;
for (int j = 0; j < 8 && i < pBinStr.length(); j++,i++)
{
b = b << 1;
if (pBinStr.at(i) == '1')
b = b | 0x01;
}
i--;
result_out << (char)b;
t++;
}
然后就可以看看压缩效果了,测试的时候需要注意,并不是所有的图片压缩效果都很好,bmp格式的图片压缩效果应该是比较好的,如果不好,用画图工具打开,将图片另存为256位图,或者找一张色彩单调的图片。如果是其他格式的文件,压缩效果几乎为0,甚至会变大一点点,这都是正常现象,原因在于这些文件中每个字符的权重差别不大,导致编码长度都为8左右,导致压缩效果很不好。