文件压缩小项目haffman压缩

时间:2021-11-28 21:36:33

文件压缩的原理:

  文件压缩总体可以分为有损压缩和无损压缩两类,有损压缩是指对mp3等格式的文件,忽略一些无关紧要的信息,只保留一些关键的信息,但并不因此影响用户对于这些mp3格式文件的体验度,无损压缩是基于比特位的压缩,即都是通过某种特殊的编码方式将数据信息中存在的重复度、冗余度有效地降低,从而达到数据压缩的目的。比如,“中国”可以代替*。压缩的文件必须可以解压,这样才算达到了文件压缩的目的。

  haffman压缩:

1.定义一个字符数组vector<_char_info>(_char_info为一个结构体,存放字符的信息),大小为256,遍历整个文档,统计文档中字符出现的次数(以字符的ascll码为下标,每当字符出现一次,就将存储在该位置的数字加一);

2.根据统计得到的次数建立haffman树,haffman树采用孩子双亲表示法

  priority_queue实际是一个堆,默认情况下按照按照小于的方式建堆,即默认情况下为大堆,我们建立haffman需要先找到vector<_char_info>中的子符出现次数(权值)最小的,因此需要建立小堆,这样堆顶元素即为vector<_char_info>中的最小值。建立小堆,需要自定义priority_queue,将其设置成大于的比较方式,因此需要对()运算符进行重载,设置成大于的比较方式

3.haffman树建立好之后,根据 haffman获取文档中出现字符的haffman编码

  记录根节点的权值,存储的文件中所有字符的个数filesize

  获取方式:从haffman树的叶子节点开始,如果当前节点的父节点不为空,则一直往上遍历。得到的编码我们需要将其reverse一下,获得正确的编码,每reverse一下,需要将filzesize--,将获取的编码存储在_char_info结构体中的_char_Code字段,当filesize为0时,获取了所有字符的编码,进行下一步

  往压缩文件中写源文件的信息,我们将自己压缩的文件后缀命名为.hzp,将源文件的后缀,字符信息存储到压缩的文件中,注意,解压时必须要能区分文本信息与我们写入的辅助信息,因此需要将每个字符的信息以如下的方式存入.hzp文件中

  文件压缩小项目haffman压缩

   将辅助信息写入压缩文件之后,开始以将源文档中字符编码按照比特位的方式一个字节一个字节写入压缩文件,当源文件的文件在此到达文件末尾的时候写入完毕,此时还要注意是否将所有的比特位都写入压缩文件,如果没有,则需要特殊处理

   以上的步骤做完之后,需要解压文件

 4.解压文件:

  打开后缀为.hzp的文件,由于我们往.hzp文件中写信息的时候是以上图中的方式,因此,我们需要先读取解压后文件的后缀,存入一个字符串中str_suffix中,读取一行后接着读取需读取的辅助信息的行数,读到行数之后,将字符形式的行数转化为size_t类型,可以调用库函数atoi。将读取的信息存入_char_info结构体中(字符以及字符出现的次数),读完辅助信息之后根据辅助信息重建haffman树。

  haffman重建之后,根据压缩文件中的字符遍历haffman树,如果读到的字节中的比特位为1,则访问haffman树的右子树,为0则访问haffman树的左子树,当走到叶子节点的时候需要将该编码对应的字符写入到解压后的文件中(每次写一个字符一个字符),这时需要将访问haffman树的指针复位,重新指向根节点,让filesize--,当filesize为0时说明解压完毕。

 5.一下为编程代码:

  (1)头文件:

 #pragma once 

 #include<iostream>
#include<vector>
#include<string>
#include<assert.h>
#include"HaffmanTree.hpp"
using namespace std; typedef unsigned char UCH; struct Char_info{ //构造函数
Char_info(size_t char_count = )
:_char_count(char_count)
{} UCH _ch; //记录当前字符
long long _char_count; //记录当前字符在文件中的位置
string _str_code; //记录当前字符的haffman编码 Char_info operator+(const Char_info& temp)const{
return Char_info(temp._char_count + _char_count);
} bool operator>(const Char_info& temp)const{
return _char_count > temp._char_count;
} bool operator<(const Char_info& temp)const{
return _char_count < temp._char_count;
} bool operator!=(const Char_info& temp)const{
return _char_count != temp._char_count;
} bool operator==(const Char_info& temp)const{
return _char_count == temp._char_count;
} bool operator!()const{
return !_char_count;
} }; class FileCompressHaffMan{
public:
//构造函数
FileCompressHaffMan(); void CompressFile(const string& strPath); //文件压缩 void GetHaffmanCode(HaffmanTreeNode<Char_info>* pRoot); //获取haffman编码 void WriteHead(FILE* DestFile, const string& strPath); //写后缀,编码等信息 void UNCompressFile(const string& strPath); //解压文件 void GetLine(FILE* fIn,string& str);//读取一行数据 vector<Char_info> _char_info; };

 (2)构造函数:

 FileCompressHaffMan::FileCompressHaffMan(){
_char_info.resize();
for (size_t i = ; i < _char_info.size(); ++i){
_char_info[i]._ch = i;
}
}

   (3)压缩文件的函数:

 void FileCompressHaffMan::CompressFile(const string& strPath){

     //打开文件,记录当前字符的出现字数
if (strPath.empty()){
cout << "文件目录错误" << endl;
return;
} FILE* Source_File = fopen(strPath.c_str(), "rb"); //以二进制的形式打开文件 if (nullptr == Source_File){
cout << "打开文件失败" << endl;
return;
} //定义读取缓冲区,接收一次读取的数据 UCH* ReadBuff = new UCH[]; //定义记录的数组,用来统计文档中每个字符出现的次数 while (true){
size_t ReadSize = fread(ReadBuff, , , Source_File);
if ( == ReadSize){
//读取文件完成
break;
} //统计文档中字符出现的次数
for (size_t i = ; i < ReadSize; ++i){
_char_info[ReadBuff[i]]._char_count++;
}
} //将char_info的数据进行排序,取权值最小的,建立haffman树
HaffmanTree<Char_info> ht;
ht.CreateHaffmanTree(_char_info,_char_info[]); //获取haffman编码
GetHaffmanCode(ht.GetRoot());
//fseek(Source_File, 0, SEEK_SET); //将文件指针偏移到文件的开始位置 FILE* DestFile = fopen("finshFileCompress.hzp", "wb"); //打开压缩文件
assert(DestFile); WriteHead(DestFile, strPath); //往目标文件中写入后缀,字符编码等信息 //写压缩文件
//由于当前打开额源文件的文件指针已经到达文件末尾,所以需要将文件指针重置到开头
fseek(Source_File, , SEEK_SET);
char ch = ;
int bitcount = ;
while (true){
size_t Read_Size = fread(ReadBuff, , , Source_File);
if ( == Read_Size){
break;
//读取文件完毕,结束
} //以比特位的方式将字符编码写入到文件中
for (size_t i = ; i < Read_Size; ++i){
string& char_Code = _char_info[ReadBuff[i]]._str_code;
for (size_t j = ; j < char_Code.size(); ++j){
ch <<= ;
if ('' == char_Code[j]){
ch |= ;
}
bitcount++;
if ( == bitcount){
fputc(ch, DestFile);
//一个字节一个字节写入文件
bitcount = ;
}
}
}
}
//写完之后还有比特位剩余,则将剩余的比特位写入文件
if (bitcount > && bitcount < ){
ch <<= ( - bitcount); //等于号啊啊啊啊啊
fputc(ch, DestFile);
} delete[] ReadBuff;
fclose(DestFile); //关闭压缩文件
fclose(Source_File); //关闭源文件 }
 void FileCompressHaffMan::GetHaffmanCode(HaffmanTreeNode<Char_info>* pRoot)    //获取haffman编码
{
if (nullptr == pRoot){
return;//没写这个判断条件就会造成死循环
}
GetHaffmanCode(pRoot->_pLeft);
GetHaffmanCode(pRoot->_pRight); if ((pRoot->_pLeft == nullptr)&&(pRoot->_pRight == nullptr)){
HaffmanTreeNode<Char_info>* pCur = pRoot;
HaffmanTreeNode<Char_info>* pParent = pCur->_pParent; string& strCode = _char_info[pCur->_weight._ch]._str_code;
while (nullptr!=pParent){
if (pCur == pParent->_pLeft){
strCode += '';
}
else
{
strCode += '';
}
//pParent要往下走,不然会形成死循环
pCur = pParent;
pParent = pCur->_pParent;
}
reverse(strCode.begin(),strCode.end()); //翻转字符串,将haffman编码写入
}
} void FileCompressHaffMan::WriteHead(FILE* DestFile, const string& strPath){
string str_suffix = strPath.substr(strPath.rfind('.')); //截取文件的后缀写入目标文件中
str_suffix += '\n'; string str_charinfo;
char szCount[];
size_t linecount = ; for (size_t i = ; i < ; ++i){
if (_char_info[i]._char_count){
str_charinfo += _char_info[i]._ch;
str_charinfo += '/';
_itoa(_char_info[i]._char_count, szCount, );
str_charinfo += szCount;
str_charinfo += '\n';
linecount++;
}
}
_itoa(linecount, szCount, );
str_suffix += szCount;
str_suffix += '\n'; str_suffix += str_charinfo;
fwrite(str_suffix.c_str(), , str_suffix.size(), DestFile); }

   (4)创建haffman树(采用模板的方式)

 #pragma once
#include<vector>
#include<string>
#include<queue>
using namespace std; //以模板的形式写haffman树 //haffman树的结点信息
template<class T>
struct HaffmanTreeNode{ typedef HaffmanTreeNode<T> HFNode; T _weight; //haffman树节点的权值,存放文档中字符出现的次数
HFNode* _pLeft; //haffman树的左孩子
HFNode* _pRight; //haffman树的右孩子
HFNode* _pParent; //haffman树的结点的双亲 //构造函数,初始化haffman树节点的权值以及指针
HaffmanTreeNode(const T& weight)
:_weight(weight)
, _pLeft(nullptr)
, _pRight(nullptr)
, _pParent(nullptr)
{}
}; //自定义priority_queue的排序方式,建立小堆(默认比较方式是>,即默认情况下建立大堆)
template<class T>
struct compare{
typedef HaffmanTreeNode<T>* pHFNode;
bool operator()(const pHFNode& pLeft,const pHFNode& pRight){
return pLeft->_weight > pRight->_weight;
}
}; //haffman树
template<class T>
class HaffmanTree{
public:
typedef HaffmanTreeNode<T> HFNode;
typedef HFNode* pHFNode; HaffmanTree()
:_pRoot(nullptr)
{}
~HaffmanTree(){
_delete(_pRoot);
} //v用来存放Char_info结构体
void CreateHaffmanTree(const vector<T>& v,const T& invalid){
if (v.empty()){
return;
//v中为空,则说明信息不存在
} //通过priority_queue建立小堆,对v中的数据进行排序
priority_queue<pHFNode, vector<pHFNode>, compare<T>> q;
//入队
for (size_t i = ; i < v.size(); ++i){
if (v[i] != invalid){
q.push(new HFNode(v[i]));
} } //如果q中的个数大于1.则说明haffman树环没有创建成功
while (q.size()>){
pHFNode pLeft = q.top();
q.pop();
pHFNode pRight = q.top();
q.pop(); pHFNode pParent = new HFNode(pLeft->_weight + pRight->_weight);
pParent->_pLeft = pLeft;
pParent->_pRight = pRight;
pLeft->_pParent = pParent;
pRight->_pParent = pParent;
//将新的子树插入优先级队列
q.push(pParent);
}
_pRoot = q.top(); //根节点赋值给_pRoot
} //获取haffman树的根节点
pHFNode GetRoot(){
return _pRoot;
} //销毁haffman树
void _delete(HaffmanTreeNode<T>* pRoot){
if (pRoot == nullptr){
return;
}
_delete(pRoot->_pLeft);
_delete(pRoot->_pRight); delete pRoot;
} private:
pHFNode _pRoot; //haffman树的根节点
};

    (6)解压文件

 void FileCompressHaffMan::GetLine(FILE* fIn, string& str)//读取一行数据
{
//feof用来检测文件指针是否到达文件末尾
while (!feof(fIn)){
UCH ch = fgetc(fIn);
if (ch == '\n'){
return;
}
str += ch;
}
} void FileCompressHaffMan::UNCompressFile(const string& strPath){
string& str_suffix = strPath.substr(strPath.rfind('.'));
if (".hzp" != str_suffix){
cout << "文件格式不匹配" << endl;
return;
}
FILE* fIn = fopen(strPath.c_str(), "rb");
if (nullptr == fIn){
return;
//打开文件失败
} string strfix = ""; //获取文件的后缀
GetLine(fIn, strfix); string strChar = "";
GetLine(fIn, strChar);
size_t linecount = atoi(strChar.c_str());
for (size_t i = ; i < linecount; ++i){
strChar = "";
GetLine(fIn, strChar);
if (strChar.empty()){
strChar += '\n';
GetLine(fIn, strChar);
}
_char_info[(UCH)strChar[]]._char_count = atoi(strChar.c_str() + );
//加2的原因是要把字符以及分隔符跳过去
} //创建haffman树
HaffmanTree<Char_info> ht;
ht.CreateHaffmanTree(_char_info, _char_info[]); string UNCompress = "finshFileCompress";
UNCompress += strfix; //解压后文件的后缀 FILE* fOut = fopen(UNCompress.c_str(), "wb");
assert(fOut); char* pReadBuff = new char[]; HaffmanTreeNode<Char_info>* pCur = ht.GetRoot(); char pos = ;
long long filesize = pCur->_weight._char_count; while (true){
size_t ReadSize = fread(pReadBuff, , , fIn);
if (ReadSize == ){
break;//文件读取完毕
} for (size_t i = ; i < ReadSize; ++i){
pos = ;
char ch = pReadBuff[i];
for (size_t j = ; j < ; ++j){
//测试代码
//cout << pReadBuff[j] << endl;
//cout << (1 << pos) << endl;
//cout << (pReadBuff[j] & (1 << pos)) << endl;
if (ch&(<<pos)){
pCur = pCur->_pRight;
}
else{
pCur = pCur->_pLeft;
} if (nullptr == pCur->_pLeft&&nullptr == pCur->_pRight){
fputc(pCur->_weight._ch, fOut);
pCur = ht.GetRoot();
filesize--;
if ( == filesize){
break;//解压缩完成
}
}
pos--;
}
}
}
delete[] pReadBuff;
fclose(fIn);
fclose(fOut);
}

注意事项:

  1.在写代码时注意语法错误,ch|1没“=”的这种错误编译器是无法检测出来的

  2.创建haffman树的时候需要将那些权值不为0的字符信息存入haffman树,所以需要键入判断条件

文件压缩小项目haffman压缩文件压缩小项目haffman压缩

文件压缩小项目haffman压缩

  3.在读写文件时需要以二进制的方式打开文件,否则如果以文本的方式打开文件,当文档中遇到-1时,会认为文档结束,因此会造成文档不能完全压缩,只压缩一部分。如果以二进制的方式打开文件,则会避免这种问题

  4.解压文件的时候需要处理换行,如果读到一行,读到的字符串为空串,说明读到了换行,此时需要我们判断,如果字符串为空,则需要将换行加入字符串写入文件,并且再读一次,否则当文档中遇到换行的时候会解析出错

  5.重建haffman树之后,读完叶子节点之后需要将pCur重置,归位

          文件压缩小项目haffman压缩

  6.由于中文所占字节数比普通的英文字符多,因此不能用char,char类型存不下中文字符,因此需要用unsigned char存放

            文件压缩小项目haffman压缩