一、前言
如果你学习数据结构,就一定会学到Huffman树,而Huffman编码实际上上就是zip压缩的核心部分,所以,如果已经学习了Huffman树,为何不尝试写一个压缩程序出来呢?
如果你没有学习Huffman树,那咱们就一起先学习一下Huffman树吧。
二、Huffman树压缩文件
定义:Huffman树,又称为最优二叉树,是加权路径长度最短的二叉树。
建立:
这样建立的树,保证所有数据成员都在叶子节点上,且数越小,离根节点越远,越大,离根节点越近,那么这样的特点应用于压缩中是很关键的,我们可以让出现次数少的字符编码长一些,次数多的字符编码短一些。接下来我们看看压缩的步骤吧~
1>统计要压缩的文件中字符出现的次数。
遍历一遍文件,将字符出现的次数统计在一个结构体数组里,数组里包含字符,字符出现的次数,对该字符的编码。
2>用得到的数组构建一个Huffman树。
因为每次要取最小值,所以这里考虑建立一个小堆。
3>得到Huffman编码
怎么得到呢?向右为1,向左为0,就是这么简单,我画图示意一下:
原本用一个char表示的字符,现在只占了几个位,这就是为什么能将文件压缩。
4>向压缩文件里写入Huffman编码。
写入的时候,满8个位写进去,如果最后不足8个位,先补齐,解压的时候要注意,解压到源文件字符数的时候停止即可。源文件的总字符数可以在第一次遍历统计出现的字符个数时统计,还有一种方法就是,仔细观察Huffman树就知道,它的根节点的大小,其实就是所有叶子节点相加的和。所以,根节点的大小就是源文件里所有字符出现的总次数。
至此,压缩就结束了。
但是,怎么解压缩呢?解压缩至少也得已知这样的一颗树才行啊,所以,我们在压缩完成后要建立一个配置文件。
5>建立配置文件
配置文件里要存储源文件字符及出现的次数。有了这样的配置文件,就可以再次构建Huffman树!
三、解压缩
1.根据配置文件得出出现的字符和对应出现次数
四、我遇到的问题
1>编译时不通过,一大堆的错误,我找了半天!最后发现是一个很简单的问题,我的Huffman树使用的是C++模板实现的,模板不能分离编译,而我在压缩时建立Huffman树是在另一个文件中进行的,所以编译不通过。
解决方法:.h后缀改成.hpp,重新包一下头文件ok。
2>文件的打开方式。这里打开文件一定要用二进制形式,"wb","rb".因为二进制打开和文本打开其实是有区别的。文本方式打开,会对‘\n’进行特殊处理,那如果这个字符本身就是'\n'.这就会出现问题,所以使用二进制打开,特点:不进行任何处理,是什么就是什么。
3>压缩后解压缩的图片打不开,经过我反复查找,终于发现是配置文件里对‘\0’的处理问题,我在写配置文件起初是用一个string把字符和它出现的次数连接起来放进去。比如:a,3 这样带来的问题是 \0,200 写的时候是以c字符串的形式写的,所以遇见'\0'就终止了,那么在解压缩的时候就会出问题。
解决方法:先把字符放进去,再把剩下的构建成string对象放进去。
4>压缩汉字出现问题 汉字字符范围是0-255
5>二次压缩效率很低,压缩一次之后字符出现的次数相差不是很大
模板不可以分离编译:
五、源码
1>Huffman树
//构建Huffman树
#pragma once
#include<iostream>
#include"Heap.h"
using namespace std;
template<class W>
struct HuffmanTreeNode
{
HuffmanTreeNode<W>* _left;
HuffmanTreeNode<W>* _right;
HuffmanTreeNode<W>* _parent;
//里面存的是个W 结构的魅力
W _w; // Ȩֵ
//三叉树 方便构建
HuffmanTreeNode(const W& w)
:_left(NULL)
,_right(NULL)
,_parent(NULL)
,_w(w)
{}
};
template<class W>
class HuffmanTree
{
public:
typedef HuffmanTreeNode<W> Node;
HuffmanTree()
:_root(NULL)
{}
HuffmanTree(W* w, size_t n, const W& invalid)
{
//适配器
struct NodeComapre
{
bool operator()(Node* l, Node* r)
{
return l->_w < r->_w;
}
};
Heap<Node*, NodeComapre> minHeap;
for (size_t i = 0; i < n; ++i)
{
//重载了!=
if (w[i] != invalid)
minHeap.Push(new Node(w[i])); // 一步顶两步
}
while (minHeap.Size() > 1)
{
Node* left = minHeap.Top();
minHeap.Pop();
Node* right = minHeap.Top();
minHeap.Pop();
//重载了+
Node* parent = new Node(left->_w + right->_w);
parent->_left = left;
parent->_right = right;
left->_parent = parent;
right->_parent = parent;
minHeap.Push(parent);
}
_root = minHeap.Top();
}
~HuffmanTree()
{
Destory(_root);
_root = NULL;
}
Node* GetRoot()
{
return _root;
}
protected:
void Destory(Node* root)
{
if (root == NULL)
return;
Destory(root->_left);
Destory(root->_right);
delete root;
}
private:
Node* _root;
};
2>压缩和解压缩
#pragma once
#pragma warning(disable:4996)
#include<stdio.h>
#include<cassert>
#include<Windows.h>
#include<string>
#include<iostream>
#include"HuffmanTree.hpp"
struct weight
{
long long _count; //出现次数
char _ch; //字符
string _code; //编码
weight(long long count=0)
:_count(count)
,_ch(0)
,_code("")
{}
weight operator+(const weight& w)
{
long long tmp = _count + w._count;
return weight(tmp);
}
bool operator<(const weight& w)
{
return _count < w._count;
}
bool operator!=(const weight& w)
{
return !(_count == w._count);
}
};
class HuffmanPress
{
public:
HuffmanPress()
{
for (int i = 0; i < 256; i++)
{
_infos[i]._ch = i;
}
}
bool FilePress(const char* filename)
{
//1.统计各个字符出现的次数
FILE* fout = fopen(filename, "rb");
assert(fout);
//ch定义成int 有可能超过127
char ch = fgetc(fout);
long long charcount = 0;
while (1)
{
if (feof(fout))
break;
_infos[(unsigned char)ch]._count++;
ch = fgetc(fout);
charcount++;
}
//2.构建HuffmanTree
weight invalid(0);
HuffmanTree<weight> hp(_infos, 256, invalid);
//GetRoot()返回的是Node*
//3.获取Huffman编码
HuffmanTreeNode<weight>* root = hp.GetRoot();
string code;
_GetCodeR(root, code);
//4.给压缩文件中写哈夫曼编码
string Compreesfile = filename;
Compreesfile += ".huffman";
FILE* fin = fopen(Compreesfile.c_str(), "wb");
assert(fin);
//统计完次数 最后需要是指针指向文件开头
fseek(fout, 0, SEEK_SET);
ch = fgetc(fout);
int value = 0;
int pos = 0;
while (!feof(fout))
{
string s = _infos[(unsigned char)ch]._code;
for (size_t i = 0; i < s.size(); i++)
{
value <<= 1;
if (s[i]=='1')
{
value |= 1;
}
if (++pos==8)
{
fputc(value, fin);
value = 0;
pos = 0;
}
}
ch = fgetc(fout);
}
if (pos)//最后编码不足一字节
{
value = value << (8 - pos);
fputc(value, fin);
}
//配置文件
string Configfile = filename;
Configfile += ".config";
FILE* fcon = fopen(Configfile.c_str(), "wb");
assert(fcon);
char countstr[20];//字符出现的次数
//把这些全部用字符保存
//(source,dstin,进制) 保留了高32位
itoa(charcount >> 32, countstr, 10);
fputs(countstr, fcon);
fputc('\n', fcon);
//与运算 和1相&是本身 保留了低32位 总结一下相关位运算
itoa(charcount & 0xffffffff, countstr,10);
fputs(countstr, fcon);
fputc('\n', fcon);
for (int i = 0; i < 256; i++)
{
string s;
if (_infos[i]!=invalid)
{
fputc(_infos[i]._ch, fcon);
fputc(',', fcon);
itoa(_infos[i]._count, countstr, 10);
s += countstr;
fputs(s.c_str(), fcon);
fputc('\n', fcon);
}
}
//这种方法会重复计算 比如有两个a 会记录两次
//while (ch!=EOF)
//{
// string s;
// if (_infos[ch]!=invalid)
// {
// fputc(ch, fcon);
// fputc(',', fcon);
// itoa(_infos[ch]._count, countstr, 10);
// s += countstr;
// fputs(s.c_str(), fcon);
// fputc('\n', fcon);//一行结束
// }
// ch = fgetc(fout);
//}
fclose(fout);
fclose(fin);
fclose(fcon);
return true;
}
bool FileUncompress(char* filename) //这里给的是压缩文件名
{
//文件操作
string Configfile = filename;
int tmp = Configfile.rfind('.');
Configfile = Configfile.substr(0, tmp);
string Uncompress = Configfile + ".uncompress";
FILE* fucon = fopen(Uncompress.c_str(), "wb");//反压缩文件
assert(fucon);
//这个是读
FILE* fin = fopen(filename, "rb");//哈夫曼文件(哈夫曼编码保留的文件)
assert(fin);
Configfile += ".config";
FILE* fcon = fopen(Configfile.c_str(), "rb");//配置文件 出现的字符及其对应的出现次数(重建哈夫曼树) 和总字符数
assert(fcon);
string s;
_ReadLine(fcon, s);
long long count = atoi(s.c_str());//总字数
s.clear();
_ReadLine(fcon, s);
count <<= 32;
count += atoi(s.c_str());
s.clear();
while (_ReadLine(fcon,s))
{
if (!s.empty())
{
char ch = s[0];
_infos[(unsigned char)ch]._count = atoi(s.substr(2).c_str());
s.clear();
}
//这里不懂
else
{
s += '\n';
}
}
//再次构建Huffman树
weight invalid(0);
HuffmanTree<weight> hp(_infos, 256, invalid);
HuffmanTreeNode<weight>* root = hp.GetRoot();
HuffmanTreeNode<weight>* cur = root;
char ch = fgetc(fin);
int pos = 8;//pos是8
while (1)
{
--pos;
if ((ch>>pos)&1)
{
cur = cur->_right;
}
else
{
cur = cur->_left;
}
if (cur->_left==NULL&&cur->_right==NULL)
{
fprintf(fucon,"%c", cur->_w._ch);
//fputc(cur->_w._ch, fucon);
cur = root;
count--;
}
if (pos==0)
{
ch = fgetc(fin);
pos = 8;
}
if (count==0)
{
break;
}
}
fclose(fin);
fclose(fcon);
fclose(fucon);
return true;
}
protected:
bool _ReadLine(FILE* filename, string& line)
{
assert(filename);
if (feof(filename))
return false;
///时刻注意这里是unsigned char
unsigned char ch = fgetc(filename);
while (ch != '\n')
{
line += ch;
ch = fgetc(filename);
if (feof(filename))
return false;
}
return true;
}
void _GetCodeR(HuffmanTreeNode<weight>* root,string code)
{
if (root==NULL)
return;
if (root->_left==NULL&&root->_right==NULL)
{
_infos[(unsigned char)root->_w._ch]._code = code;
}
_GetCodeR(root->_left,code + '0');
_GetCodeR(root->_right,code + '1');
}
private:
weight _infos[256];
};
void Test()
{
HuffmanPress h;
h.FilePress("test.txt");
h.FileUncompress("test.txt.huffman");
}
int main()
{
Test();
system("pause");
return 0;
}
Heap.h
#include<iostream>
using namespace std;
#include<vector>
template<class T>
struct Greater
{
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
template<class T>
struct Less
{
bool operator()(const T&x, const T& y)
{
return x < y;
}
};
template<class T, class Compare = Grater<T>>
class Heap
{
public:
Heap()
{}
Heap(const T* a, size_t n)
{
//vector(动态数组)
//vector reserve() 改变Capacity 配套用push_back 建议使用这个
//vector resize() 改变size大小 a[i] = array[i]
_v.reserve(n);
for (size_t i = 0; i < n; i++)
{
_v.push_back(a[i]);
}
//建堆
for (int i = ((int)_v.size() - 2) / 2; i >= 0; i--)
{
AdjustDown(i);
}
//向下调正算法给的第一个就是父节点 向上给的第一个是叶子节点
//向下调正算法 假设左右都是大堆 往下换 叶子节点不用算,从最后一个非叶子节点算起(倒着算)
}
void Push(const T& data)
{
_v.push_back(data);
//向上调正算法 只在push()这里用,把新填的叶子节不断向上换 直到符合大/小堆
AdjustUp(_v.size() - 1);
}
void Pop()
{
//1.先将堆顶元素与堆的最后一个元素交换
swap(_v[_v.size() - 1], _v[0]);
//2.删除最后一个节点
_v.pop_back();
//3.向下调整
AdjustDown(0);
}
T& Top()
{
return _v[0];
}
size_t Size()
{
return _v.size();
}
protected:
void AdjustDown(int root)
{
int parent = root;
int child = root * 2 + 1;
while (child<(int)_v.size())
{
if (child + 1 < (int)_v.size() && Compare()(_v[child + 1], _v[child]))
{
child++;
}
if (Compare()(_v[child], _v[parent]))
{
swap(_v[parent], _v[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void AdjustUp(int child)
{
int parent = (child - 1) / 2;
//注意这里是child>0 不是parent>0
while (child>0)
{
if (Compare()(_v[parent], _v[child]))
{
break;
}
else
{
swap(_v[child], _v[parent]);
child = parent;
parent = (child - 1) / 2;
}
}
}
private:
vector<T> _v;
};