什么是HuffMan压缩:
简单来说就是利用Huffman树生成Huffman编码,对文件重复出现的字符进行记录,以减少出现次数。从而达到压缩文件的目的。
为什么HuffMan就能实现文件压缩呢?
数据在硬盘中的存储是有格式的,比如说字符就是char类型的,占了8个比特位,但是实际上有些字符可能根本就用不了8个比特位,因此就造成了空间的浪费。而huffman就是根据字符出现的次数重新建立存储规则,减少这些空间浪费。
什么是HuffMan树?
定义: Huffman树,又称最优二叉树,是加权路径长度最短的二叉树。
带权路径长度 = 节点的权值 * 当前节点距离根节点的路径长度
如何构建HuffMan树?
生成HuffMan编码
HuffMan树中左子树路径标记为0,右子树路径标记为1.从根节点到叶子结点的编码就是该字符的Huffman编码。
HuffMan文件压缩原理:
1.统计待压缩文件中每个字符出现的次数
- 构建结构体,存放信息:字符,次数,编码
- 遍历源文件,将每个字符的次数写入对应的结构体信息中
2.将字符出现的次数作为权值构建Huffman树
- 普通的二叉树只能通过双亲节点找到子节点,但是在获取编码的时候需要通过叶子结点往根节点走,所有构建二叉树时,要有子节点指向双亲节点
3.通过Huffman树获取每个字符所对应的编码
- 通过叶子结点找根节点的编码是逆序的,所以要使用reverse
4.向压缩文件中写入信息
- 压缩文件中只保存压缩的文件内容是不够的,还要有一些记录源文件内容的信息
- 第一行:存放源文件的后缀,因为在解压缩文件的时候需要后缀
- 第二行:存放源文件字符,次数 的总行数
- 第三行:存放字符和字符出现的次数–为了解压缩重建Huffman树
- 剩余内容是压缩编码
例如:源文件名为“1.txt”,存放“ABBBCCCCCDDDDDDD”
在压缩文件中的模型为:
解压缩:
1.获取后缀
2.获取字符以及对应的次数
3.重建Huffman树
4.解压压缩数据
- 从压缩文件中读取一个字节ch
- 从根节点开始,按照ch的8个比特位信息(代表字符的编码)从高到低遍历Huffman树:
a.该比特位是0,取当前节点的左孩子,否则取右孩子
b.一直遍历,直到遍历到叶子节点位置,该字符就被解析成功,讲解压出来的字符写入文件
c.如果在遍历Huffman过程中,8个比特位已经比较完毕还没有到达叶子节点,就从第四步开始执行
d.重复以上过程,直到所有的数据解析完毕
问题:
1.压缩汉字时候程序会崩溃,但是压缩字母程序就能正常运行?
- 原因:创建存放信息的数据是char 类型的,类型大小是1字节(0~255),但是汉字都是占两个字节的,对应的char类型就会因为超过255而导致数组的下边变成负数,数组下边越界访问导致程序崩溃。
- 解决方法:将存放信息的数据类型改成unsigned char 类型
2.解压大文件,只能解压缩一部分内容?
- 原因:一般情况下,文件指针碰到EOF就表示到文件结尾了,因为EOF是 -1,也就是FF,所以只解压了一部分(解压到第一个FF就停止了)。
- 解决方法:采用 feof()函数,多加一个判断即可,feof()函数就是判断文件末尾,而不仅仅是碰见EOF停止。
3.为什么压缩照片的时候会失败?
- 原因:是因为 ‘\0’ 的问题,因为如果刚开始把(字节,次数)先写入 buf 中,再由 buf 通过 fwrite 函数写入文件中,一定会出现问题(可能出现 0 字节)。因为 fwrite 的第一个参数要求C格式的字符串,把 buf 转化为C格式的字符串,如果遇见 ‘\0’,就会停止,所以就会崩溃。
- 解决方法:不要将字节写入 buf 中,而是通过 fputc 直接把字节写入文件中,然后再写入 buf 中,再将buf写入文件即可。
文件压缩的图解过程:
源码:
main.c
#include "HuffmanTree.hpp"
#include "FileCompressHuffman.h"
int main()
{
//TestHuffman();
FileCompressHuffmanM test;
//test.CompressFile("文件压缩(原文件).png");
test.UnCompressFile("文件压缩.hzp");
return 0;
}
huffmanTree.hpp
#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
template<class W>
struct HuffmanTreeNode //定义哈弗曼树节点的类型
{
HuffmanTreeNode(const W& weight)
:_pLeft(nullptr)
, _pRight(nullptr)
, _pParent(nullptr)
, _weight(weight)
{}
HuffmanTreeNode<W>* _pLeft;
HuffmanTreeNode<W>* _pRight;
//用孩子双亲形式表示二叉树,方便从叶子结点找到根节点,从而对字符进行编码
HuffmanTreeNode<W>* _pParent;
W _weight; //节点权值
};
template<class W>
struct Compare //用仿函数比较,变小堆
{
typedef HuffmanTreeNode<W>* PNode;
bool operator()(const PNode pLeft, const PNode pRight)
{
return pLeft->_weight > pRight->_weight;
}
};
template<class W>
class HuffmanTree
{
typedef HuffmanTreeNode<W> Node;
typedef Node* PNode;
public:
HuffmanTree()//构造函数,初始状态下为空树
: _pRoot(nullptr)
{}
void CreatHuffmanTree(const std::vector<W>& v, const W& invalid) //根据权值创建树,模板引用 vector在标准的命名空间中定义的
{
if (v.empty()) //v是存放权值的数组
return;
//用所给的权值创建二叉树森林
//std::priority_queue<PNode> q; 使用仿函数实现 小堆 比较器
std::priority_queue<PNode,std::vector<PNode>,Compare<W>> q;//优先级队列 保存树,把地址放进去就好了,,但是默认是大堆,而我们需要的是小堆
for (size_t i = 0; i < v.size(); ++i)
{
if (v[i] != invalid) //过滤出现0次的字符,通过创建哈弗曼树多加上一个参数 来实现
q.push(new Node(v[i])); //用权值创建节点
}
while (q.size() > 1) //当树不止一个时,把作为左右孩子
{
PNode pLeft = q.top();
q.pop();
PNode pRight = q.top();
q.pop();
PNode pParent = new Node(pLeft->_weight + pRight->_weight);
pParent->_pLeft = pLeft;
pLeft->_pParent = pParent;
pParent->_pRight = pRight;
pRight->_pParent = pParent;
q.push(pParent); //把两个子树生成的树放回队列中
}
_pRoot = q.top(); //哈夫曼树创建成功
}
PNode GetRoot()
{
return _pRoot;
}
~HuffmanTree()
{
_Destroy(_pRoot); //销毁 哈夫曼树
}
private:
void _Destroy(PNode& pRoot) //后序遍历进行销毁二叉树
{
if (pRoot)
{
_Destroy(pRoot->_pLeft);
_Destroy(pRoot->_pRight);
delete pRoot;
pRoot = nullptr;
}
}
private:
PNode _pRoot;
};
//void TestHuffman()
//{
// std::vector<int> v{ 3,1,7,5 };
// HuffmanTree<int> ht;
// ht.CreatHuffmanTree(v);
//}
FileCompressHuffman.h
#pragma once
#include<string>
#include<iostream>
#include<vector>
#include "HuffmanTree.hpp"
using namespace std;
typedef unsigned char UCH;
//哈夫曼树中的字符信息,包括字符代表的字母,次数,编码
struct CharInfo
{
//用构造函数初始化哈夫曼树中的字符
CharInfo(size_t charCount = 0)
: _charCount(charCount)
{}
//权值在结构体中,而结构体不能直接相加,所以要重载+
CharInfo operator+(const CharInfo& info) //字符出现的次数
{
return CharInfo(_charCount + info._charCount);
}
bool operator>(const CharInfo& info)
{
return _charCount > info._charCount;
}
bool operator!=(const CharInfo& info)const
{
return _charCount != info._charCount;
}
bool operator==(const CharInfo& info)const
{
return _charCount == info._charCount;
}
UCH _ch;
size_t _charCount;
std::string _strCode;
};
class FileCompressHuffmanM
{
public:
FileCompressHuffmanM();
void CompressFile(const std::string& strFilePath); //const 保证原文件不会被修改
void UnCompressFile(const std::string& strFilePath);//不加命名空间会如何?
void WriteHead(FILE* fOut, const std::string& strFilePath);//头部信息:用来保存压缩文件的信息,包括后缀,字符出现的次数
private:
void GetHuffmanCode(HuffmanTreeNode<CharInfo>* pRoot);
void GetLine(FILE* fIn, std::string& strContent);
std::vector<CharInfo> _charInfo; //把编码保存在数组中,这个数组是个结构体类型的,字符信息
};
FileCompressHuffmanM.cpp
#pragma once
#include "FileCompressHuffman.h"
#include"HuffmanTree.hpp"
#include <iostream>
#include <assert.h>
using namespace std;
FileCompressHuffmanM::FileCompressHuffmanM()
{
_charInfo.resize(256);
for (size_t i = 0; i < 256; ++i)
{
_charInfo[i]._ch = i; //i是ch 的ASCII码
}
}
void FileCompressHuffmanM::CompressFile(const std::string& strFilePath)
{
string MiddleName = strFilePath.substr(0, strFilePath.find('.'));// MiddleName是生成的压缩文件的名称
//1.获取原文件中每个字符出现的次数
FILE* fIn = fopen(strFilePath.c_str(), "rb"); //string 类 转换成char*类型
if (fIn == nullptr)
{
cout << "文件打开失败" << endl;
return;
}
UCH *pReadBuff = new UCH[1024]; //每次读取1024字节,即1M
//long long CharCount[256] = { 0 }; //符号有可能出现很多次,所以出现次数用long long 类型表示
//vector<CharInfo> charInfo(256);
//初始化_ch
while (1)
{
size_t rdSize = fread(pReadBuff, 1, 1024, fIn);//返回读到的元素个数
if (0 == rdSize)
break;
for (size_t i = 0; i < rdSize; ++i)
{
//CharCount[pReadBuff[i]]++; //pReadBuff[i]表示第i个字节读到的内容,对应的ascii码作为数组CharCount的下标
_charInfo[pReadBuff[i]]._charCount++;
}
}
//2.以每个字符出现的次数作为权值构建哈夫曼树
HuffmanTree<CharInfo> ht; //类名 类型 实例化对象
ht.CreatHuffmanTree(_charInfo,CharInfo(0));
//3.根据哈弗曼树获取每个字符的编码
GetHuffmanCode(ht.GetRoot());
//4.根据每个字符的编码重新改写原文件
//先打开一个文件,写入压缩信息
MiddleName += ".hzp";
FILE* fOut = fopen(MiddleName.c_str(), "wb");
assert(fOut);
WriteHead(fOut, strFilePath);
UCH ch = 0;
char bitCount = 0;
fseek(fIn, 0, SEEK_SET);
while (true)
{
size_t rdSize = fread(pReadBuff, 1, 1024, fIn); //把buff中的编码写入压缩文件
if (0 == rdSize)
break;
for (size_t i = 0; i < rdSize; ++i)
{
string& strCode = _charInfo[pReadBuff[i]]._strCode;
// ch: 0000 0000
// A:100
// B:101
for (size_t j = 0; j < strCode.size(); ++j) //j小于 对应的字符的字符编码的长度
{
ch <<=1;
if (strCode[j] == '1') //ch初始值是0, ch代表一个字节,不是代表一个比特位,把这八个比特位填满就可以以往压缩文件中写入字节了
ch |= 1;
bitCount++;
if (8 == bitCount) //每满一个字节,就往压缩文件中写入一个字节
{
fputc(ch, fOut);
bitCount = 0;
}
}
}
}
//如果8个bit没有写满,还要把剩余的内容写入压缩文件
if (bitCount >0 && bitCount < 8) //大于0的原因是 ,如果8个比特位恰好写满,在上一步中会置0
{
ch <<= 8 - bitCount;
fputc(ch , fOut);
}
delete[] pReadBuff;
fclose(fIn);
fclose(fOut);
}
void FileCompressHuffmanM::UnCompressFile(const std::string& strFilePath)
{
//获得解压缩文件的名称
string ResultName = strFilePath.substr(0,strFilePath.find('.'));
//检测压缩文件的后缀格式
string strPostFix = strFilePath.substr(strFilePath.rfind('.')); //有两个参数,第二个默认结尾
if (".hzp" != strPostFix )
{
cout << "压缩文件的格式有问题" << endl;
return;
}
//获取解压缩的信息
FILE* fIn = fopen(strFilePath.c_str(), "rb"); //fIn 是指向压缩信息 hzp文件 的指针
if (fIn == nullptr)
{
cout << "压缩文件打开失败" << endl;
return;
}
//获取原文件的后缀----读完压缩信息的第一行就行了,第一行保存的就是后缀
strPostFix = "";
GetLine(fIn, strPostFix);
//获取总行数
string strContent;
GetLine(fIn, strContent); //获取在遇到\n前的第一个内容,即行数,此时fIn 指向第一个\n
size_t lineCount = atoi(strContent.c_str());
//字符信息
for (size_t i = 0; i < lineCount; ++i)
{
strContent = "";
GetLine(fIn, strContent); ?????? fIn此时不是指向的是\n么?
if (strContent.empty()) //读取到换行符,如果不处理会直接退出,没有读取到后面的压缩内容
{
strContent += '\n';
GetLine(fIn, strContent);
}
_charInfo[(UCH)strContent[0]]._charCount = atoi(strContent.c_str()+2); //获取到次数 获取的字符串是 A,1 字符A对应的是获取的字符串的下标为0 的元素 占据了两个字节,要偏移过去
} //strCont 中保存的是字符信息 如果是汉字 也有可能出现负数的情况,所以强转成无符号类型
//还原哈夫曼树
HuffmanTree<CharInfo> ht;
ht.CreatHuffmanTree(_charInfo, CharInfo(0));
//解压缩
//string strUNComFile = NameOfCompressFile;
string strUNComFile = ResultName;
strUNComFile += strPostFix; //名字+后缀
FILE* fOut = fopen(strUNComFile.c_str(), "wb");
assert(fOut);
char* pReadBuff = new char[1024];
HuffmanTreeNode<CharInfo>* pCur = ht.GetRoot();
char pos = 7;
size_t SizeOfFile = pCur->_weight._charCount;
while (true)
{
size_t rdSize = fread(pReadBuff, 1, 1024, fIn); //从fIn文件中读取
if (0 == rdSize)
{
break;
}
for (size_t i = 0; i < rdSize; ++i)
{
pos = 7;
for (size_t j = 0; j < 8; ++j) //处理当前的一个字节
{
//if ((pReadBuff[i] & (1 << pos)) == 1)//1朝右走
if (pReadBuff[i] & (1 << pos))//1朝右走
pCur = pCur->_pRight;
else
pCur = pCur->_pLeft; //0朝左走
if (nullptr == pCur->_pLeft && nullptr == pCur->_pRight)
{
fputc(pCur->_weight._ch, fOut); //把这个字符写到fOut文件中
pCur = ht.GetRoot();
SizeOfFile--;
if (0 == SizeOfFile)
{
break;
}
}
pos--;
}
}
}
delete[] pReadBuff;
fclose(fIn);
fclose(fOut);
}
void FileCompressHuffmanM::GetHuffmanCode(HuffmanTreeNode<CharInfo>* pRoot)
{
if (pRoot == nullptr)
{
return;
}
GetHuffmanCode(pRoot->_pLeft);
GetHuffmanCode(pRoot->_pRight);
if ((pRoot->_pLeft == nullptr) && (pRoot->_pRight == nullptr))
{
//找到叶子结点 ,保存叶子节点,并追寻双亲节点
HuffmanTreeNode<CharInfo>* pCur = pRoot; //以字符信息作为模板类型,pCur类型是个结构体
HuffmanTreeNode<CharInfo>* pParent = pCur->_pParent;
string& strCode = _charInfo[pCur->_weight._ch]._strCode; //?????数组是结构体类型,把编码保存在结构体的_strCode中
while (pParent) //以字符的ASCII作为数组的下标: [叶子结点 中的权值 中的字符]
{
if (pCur == pParent->_pLeft)
{
strCode += '0';
}
else
{
strCode += '1';
}
pCur = pParent;
pParent = pCur->_pParent;
}
reverse(strCode.begin(), strCode.end());
}
}
void FileCompressHuffmanM::WriteHead(FILE* fOut, const std::string& strFilePath) //写入解压缩需要的信息
{
string strHeadInfo;
strHeadInfo = strFilePath.substr(strFilePath.rfind('.'));
strHeadInfo += '\n';
string strCharInfo;
char szCount[32];
size_t lineCount = 0;
for (size_t i = 0; i < 256; ++i)
{
if (_charInfo[i]._charCount)
{
strCharInfo += _charInfo[i]._ch; //把字符 和 次数放入 头信息中
strCharInfo += ',';
_itoa(_charInfo[i]._charCount, szCount, 10);
strCharInfo += szCount;
strCharInfo += '\n';
lineCount++;
}
}
_itoa(lineCount, szCount, 10); //整型数据要转换成字符串
strHeadInfo += szCount;
strHeadInfo += '\n';
strHeadInfo += strCharInfo;
fwrite(strHeadInfo.c_str(), 1, strHeadInfo.size(), fOut);
}
void FileCompressHuffmanM::GetLine(FILE* fIn, std::string& strContent)
{
while (!feof(fIn)) //指针没有在文件末尾,就读取一行
{
UCH ch = fgetc(fIn);
if ('\n' == ch)
{
return;
}
strContent += ch;
}
}