简述:利用哈夫曼编码进行文件的压缩和解压缩。
开发环境:windows,VS2013,C++
项目特点:
压缩文件:读取文件中的字符,将其转化为哈弗曼编码,再通过位转化为压缩文件。
解压缩文件:从配置文件中读取字符及对应字符的出现次数建立哈夫曼树,得到解压缩文件中的字符。
写配置文件,可使压缩和解压缩通过读取配置文件进行。总字符数为树的根结点的权重,不需要写入配置文件。
对于大文件的处理,读取文件用二进制读取,接口会读成-1(EOF),结束的标志为FEOF。
先建立哈弗曼树,关于哈夫曼树:
Huffman树,又称为最优二叉树,是加权路径长度最短的二叉树。
【贪心算法】是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解。使用贪心算法构建Huffman树
下面看一个简单的描述
为了方便解压缩,我们需要编写配置文件
配置文件的格式为(字符,字符出现的次数,字符的huffman编码)
下面看下代码的实现
“test.cpp”
#define _CRT_SECURE_NO_WARNINGS 1
#include "FileCompress.h"
#include<windows.h>
int main()
{
int start = GetTickCount();
HuffmanFileCompress fc;
fc.CompressFile("test.txt");
HuffmanFileCompress fc2;
fc2.UnCompressFile("test.txt.Compress");
int end = GetTickCount();
cout<<"压缩文件耗时:"<<end - start<<endl;
system("pause");
return 0;
}
“Heap.h”
#pragma once
#include <vector>
#include <assert.h>
template<class T>
struct Less
{
bool operator()(const T& left,const T& Right)
{
return left < Right;
}
};
template<class T>
struct Greater
{
bool operator()(const T& left,const T& Right)
{
return left > Right;
}
};
template<class T,class Compare = Greater<T>>//缺省值给大堆
class Heap
{
public:
Heap()
{}
Heap(const T* arr,const size_t size)
{
for (int i = 0;i < size;i++)
{
_arr.push_back(arr[i]);
}
//建堆
for (int i = (_arr.size() - 2) / 2;i >= 0;i--)
{
//寻找非叶子结点,向下调整;
AdjustDown(i);
}
}
void Push(const T& data)
{
_arr.push_back(data);
AdjustUp(_arr.size()-1);
}
void Pop()
{
assert(_arr.empty() != true);
swap(_arr[0],_arr[_arr.size()-1]);
_arr.pop_back();
AdjustDown(0);
}
const T& Top()
{
return _arr[0];
}
bool Empty()
{
return _arr.empty();
}
int Size()
{
return _arr.size();
}
void Cout()
{
for (int i = 0; i < _arr.size();i++)
{
cout<<_arr[i]<<" ";
}
cout<<endl;
}
private:
void AdjustDown(int parent)
{
int child = 2 * parent + 1;//左孩子
Compare com;
while (child < _arr.size())
{
if (((child+1) < _arr.size())&&(com(_arr[child + 1],_arr[child])))
{
++child;
}
if (com(_arr[child],_arr[parent]))
{
swap(_arr[child],_arr[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void AdjustUp(int child)
{
int parent = (child - 1) / 2;
Compare com;
while (child > 0)
{
if (com(_arr[child],_arr[parent]))
{
swap(_arr[child],_arr[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
private:
vector<T> _arr;
};
“Huffman.h”
#pragma once
#include <iostream>
using namespace std;
#include "Heap.h"
#include <assert.h>
template<class T>
struct HuffmanTreeNode
{
T _weight;//权重
HuffmanTreeNode<T>* _left;//指向左子树的指针
HuffmanTreeNode<T>* _right;//指向右子树的指针
HuffmanTreeNode(const T& weight = T())
:_weight(weight)
,_left(NULL)
,_right(NULL)
{}
};//存放HuffmanTree节点的数据结构
template<class T>
class HuffmanTree
{
typedef HuffmanTreeNode<T> Node;
public:
//构造函数
HuffmanTree()
:_root(NULL)
{}
//构造函数
//构建huffmanTree
HuffmanTree(T* arr, size_t size, const T& invalid = T())
{
//每次都选取最小的两个节点,用最小堆最为合适
struct Less
{
bool operator()(Node* left, Node* right)
{
assert(left);
assert(right);
return left->_weight < right->_weight;
}
};
//将这组数据建立成最小堆,堆中的每个元素的类型都是Node*,这是为了保存后面的父节点
Heap<Node*, Less> MinHeap;//小堆
for (size_t i = 0;i < size;i++)
{
//如果字符出现的次数不为0,那么就将他加入最小堆中
if (arr[i]._count != invalid._count)
{
Node* node = new Node(arr[i]);
MinHeap.Push(node);
}
}
//运用Huffman算法,从堆中取出两个最小的节点构建新的父节点,并在小堆中将这两个节点删除,再把新构建的父节点插入小堆中继续重复此步骤
Node* frist = NULL;
Node* second = NULL;
Node* parent = NULL;
while (MinHeap.Size() > 1)
{
//选取两个最小的节点
frist = MinHeap.Top();
MinHeap.Pop();
second = MinHeap.Top();
MinHeap.Pop();
//利用两个小节点构建父节点
parent = new Node(frist->_weight + second->_weight);
//指针指向要明确
parent->_left = frist;
parent->_right = second;
MinHeap.Push(parent);
}
//小堆里面的最后一个节点就是HuffmanTree的根节点
_root = MinHeap.Top();
}
//析构函数
~HuffmanTree()
{
if (_root != NULL)
{
//销毁函数
_Destory(_root);
}
}
public:
//获取huffmanTree
Node* GetRoot()
{
return _root;
}
protected:
void _Destory(Node* root)
{
if (NULL == root)
{
return;
}
_Destory(root->_left);
_Destory(root->_right);
delete root;
root = NULL;
}
private:
Node* _root;
};//构件huffmanTree的数据结构
“FileCompress.h”
#pragma once
#include <iostream>
using namespace std;
#include<string>
#include "Huffman.h"
typedef long long LongType;
struct CharInfo
{
unsigned char _ch;//保存字符
LongType _count;//保存字符出现的次数
string _code;//保存Huffman编码
CharInfo(const LongType& count = 0)
:_count(count)
{}
CharInfo operator+(CharInfo& ch)
{
return CharInfo(_count + ch._count);
}
bool operator<(CharInfo& ch)
{
return _count < ch._count;
}
};
class HuffmanFileCompress
{
typedef HuffmanTreeNode<CharInfo> Node;
public:
HuffmanFileCompress()
{
//初始化每个位置的_data值
for (int i = 0;i < 256;i++)
{
_infos[i]._ch = i;
}
}
public:
//压缩文件
void CompressFile(const char* filename)
{
FILE* fread = fopen(filename, "rb");//已只读形式打开文件
if (NULL == fread)
{
cout<<"The File Open Fail"<<endl;
exit(0);
}
//统计文件中字符出现的次数
int ch = fgetc(fread);
while (ch != EOF)//一直读到文件末尾
{
_infos[ch]._count++;
ch = fgetc(fread);
}
//构建HuffmanTree
CharInfo invalid;
HuffmanTree<CharInfo> hufftree(_infos, 256, invalid);
Node* root = hufftree.GetRoot();
//获取huffman编码
string code;
_GetHuffmanCode(root, code);
//将文件指针移到文件头
fseek(fread, 0, SEEK_SET);
string write(filename);//write = "filename"
write += ".Compress";//压缩文件的名字
//创建一个压缩文件,存放压缩文件的信息
FILE* fwite = fopen(write.c_str(), "wb");
ch = fgetc(fread);
unsigned char data = 0;//压缩数据以二进制的形式存储在文件中
int pos = 7;//控制bit位的移动次数
while (ch != EOF)//读到文件结尾
{
const char* ptr = _infos[ch]._code.c_str();
//遍历保存结点编码
while (*ptr)
{
if (pos >= 0)
{
data = data | ((*ptr - '0')<< pos);
--pos;
}
if (pos < 0)
{
fputc(data, fwite);
pos = 7;
data = 0;
}
ptr++;
}
ch = fgetc(fread);
}
//最后一个字符不管写没写满都要放进去
fputc(data, fwite);
//写配置文件,用于解压缩
_WriteConfig(filename);
fclose(fread);
fclose(fwite);
cout<<"压缩成功"<<endl;
}
//解压缩文件
void UnCompressFile(const char* filename)
{
//文件必须存在,要不然解压什么
assert(filename);
string write(filename);
//去掉压缩文件的后缀,加上配置文件的后缀,然后读取配置文件
unsigned int index = write.rfind('.', write.size());
write = write.substr(0, index);
string writeconfig = write;
writeconfig += ".config";
//读取配置文件,将配置文件里面的信息添加到RInfo数组中。
CharInfo RInfo[256];
_ReadConfigFile(writeconfig.c_str(), RInfo);
//解压缩文件
write += ".UnCompress";
FILE* fwrite = fopen(write.c_str(), "wb");
CharInfo invalid;
HuffmanTree<CharInfo> hft(RInfo, 256, invalid);
Node* root = hft.GetRoot();
if (NULL == root)
{
return ;
}
Node* cur = root;
LongType count = (root->_weight)._count;
//开始解压缩
FILE* fread = fopen(filename, "rb");
unsigned char ch = fgetc(fread);
//用字符的总数来控制循环条件
int pos = 8;
while (count)
{
--pos;
unsigned char val = 1;
//需要对压缩文件一个字节一个字节的访问
if (ch & (val << pos))
{
cur = cur->_right;
}
else
{
cur = cur->_left;
}
//读到叶子结点说明已经找到一个字符
if (cur->_left == NULL && cur->_right == NULL)
{
//如果读到叶子结点,那么就要把相应的字符写进解压缩文件中
fputc(cur->_weight._ch, fwrite);
//每次都要将cur重新设置为根节点
cur = root;
if (--count == 0)
{
break;
}
}
if (pos == 0)
{
pos = 8;
ch = fgetc(fread);
}
}
fclose(fread);
fclose(fwrite);
cout<<"解压缩成功"<<endl;
}
private:
//读取配置文件
void _ReadConfigFile(const char* configfilename, CharInfo* info)
{
FILE* fread = fopen(configfilename, "rb");
if (NULL == fread)
{
cout<<"Read File Fault"<<endl;
exit(0);
}
int ch = fgetc(fread);
while (ch != EOF)//一直读取到文件结尾
{
//字符,字符出现的次数,字符编码
info[ch]._ch = ch;
unsigned char index = ch;//记录当前的下标,一遍后面使用
//因为这里是一个“,”,所以要将他读取并读取下一个字符
ch = fgetc(fread);
ch = fgetc(fread);
string count;
while (ch != ',')
{
count.push_back(ch);
ch = fgetc(fread);
}
info[index]._count = atoi(count.c_str());//将获取的字符出现的次数存入。
ch = fgetc(fread);
//将字符编码依次存入
while (ch != '\n')
{
info[index]._code.push_back(ch);
ch = fgetc(fread);
}
ch = fgetc(fread);//读取'\n'字符的下一个字符
}
}
//写配置文件
void _WriteConfig(const char* filename)
{
//压缩文件的信息保存在“.config”后缀的文件当中
string write(filename);
write += ".config";
FILE* fwite = fopen(write.c_str(), "wb");//只写形式打开文件
for (int i = 0;i < 256;i++)
{
if (_infos[i]._count)
{
//存放形式为(字符,字符出现的次数, 字符编码)
fputc(_infos[i]._ch, fwite);
fputc(',', fwite);
//将字符出现的次数以十进制字符的形式存入字符数组中
char arr[126];
_itoa(_infos[i]._count, arr, 10);
fputs(arr, fwite);
fputc(',', fwite);
fputs(_infos[i]._code.c_str(), fwite);
fputc('\n', fwite);
}
}
//关闭文件实际上就是保存文件
fclose(fwite);
}
//后序遍历哈夫曼树,我们只需要访问到叶子结点
//获取huffman编码
void _GetHuffmanCode(Node* root, string code)
{
if (NULL == root)
{
return;
}
//后序遍历,左右根
//编码左0右1
_GetHuffmanCode(root->_left, code + '0');
_GetHuffmanCode(root->_right, code + '1');
if (root->_left == NULL && root->_right == NULL)
{
_infos[root->_weight._ch]._code = code;
}
}
private:
CharInfo _infos[256];//创建一个CharInfo类型的数组,利用哈希性质,每个位置的_data值都是对应的字符
};
经过测试,该压缩程序确实可以压缩音频,图片,文本等等,能够压缩并且解压会原来的一模一样。但是压缩后只有文本的大小变小了,其他类型的文件大小却并没有变小。
压缩文本的比例大概在70%-80%之间
压缩“.pdf”文件大小并没有减小,图片同样没有减小