huffman压缩
基于GZIP文件压缩
文件压缩是在不丢失有用信息的前提下,缩减数据量以减少存储空间,提高传输、存储火绒处理效率,同时可以对数据加密保护(加密算法),增强数据在传输过程中的安全性
压缩的解压缩的分类有:
有损压缩,可能丢失一些信息,不能还原到与源文件一样的,适合图片压缩,压缩率会更好
无损压缩,压缩后的压缩文件可以被还原成与源文件完全相同的格式
LZ77原理:
发现在文件中的局部范围内,有些语句重复出现,对于重复出现的词语能否想办法让其变短,起到压缩的目的
压缩的过程:
对于文件中规定长度范围内重复出现的词语,可以使用<长度,距离>对方式进行替换
长度:重复文件所占字节
距离:后文中重复出现词语首字节,与前文中重复的词语首字节位置差
eg:
cfvgtbhabcdbtgyhuabcdhuijabcdbnjabcdfrv
cfvgtbhabcdbtgyhu(4,10)huij(5,18)bnj(4,26)frv(真正存储时,不会有括号和逗号)
若没有较长语句重复,但是在字节层面有一些重复
DDAB DDBC DBCD CDCC
一个字节占8个比特位,如果能够给每个字节找一个小于8比特位的编码来一环文件中的字节,以起到压缩的目的
eg:定长编码
A:00 B:01 C:10 D:11
11110001 11110110 11011011 101110110
huffman原理:
字节重复出现的次数不一样,能否让出现多的字节对应编码位数少一些,少的多一些
DDAB DDBC DBCD CDCC DDDD
eg:变长编码
A:1 B:3 C:5 D:11
A:100 B:101 C:11 D:0
所有字符对应的编码都在子节点的位置,因此不会发生某个字符编码是其他字符前缀
树的路径长度:树根到每个结点的路径长度之和
树的带权路径长度WPL:所有叶子结点的带权路径长度之和
WPL最小的二叉树称作最优二叉树或huffman树,即权值大的靠近根
解压缩:
形成编码文件后,可以使用unordered_map<string , char>根据key值也就是string查找到对应的char
也可以利用huffman树解压缩,从根节点走,左0右1,直到到达叶子结点,用这种方法就不用每次查表了,但是要在压缩文件里保存每个字符对应的次数
压缩文件格式:
txt(源文件后缀)
4(字符数量)
A:1
B:3
C:5
D:7
96 DF FC 00
如果直接解压,那么多出来的几位比特位也被解压了,所以需要考虑总文件的大小,控制真正读取的位数
代码实现:
HuffmanTree.h:
#pragma once
#include<vector>
#include<queue>
template<class W>
struct HuffmanTreeNode {
HuffmanTreeNode<W>* _left;
HuffmanTreeNode<W>* _right;
HuffmanTreeNode<W>* _parent;
W _weight;
HuffmanTreeNode(const W& weight = W())
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_weight(weight)
{}
};
template<class W>
struct CompareNode {
bool operator()(const HuffmanTreeNode<W>* n1, const HuffmanTreeNode<W>* n2){
return n1->_weight > n2->_weight;
}
};
template<class W>
class HuffmanTree {
typedef HuffmanTreeNode<W> Node;
public:
HuffmanTree()
:_root(nullptr)
{}
HuffmanTree(const std::vector<W>& vw, const W& valid) {
//添加一个无效的权值
//std::priority_queue < Node*, std::vector<Node*>, [](Node* left, Node* right) {
// return left->_weight > right->_weight;
//} > q;
std::priority_queue<Node*, std::vector<Node*>, CompareNode<W>> q;
for (auto& e: vw) {
if (valid != e) {
q.push(new Node(e));
}
}
if (q.empty()) {
return;
}
while (q.size() > 1) {
//取两个最小结点形成一个新的树
Node* left = q.top();
q.pop();
Node* right = q.top();
q.pop();
Node* parent = new Node(left->_weight + right->_weight);
parent->_left = left;
left->_parent = parent;
parent->_right = right;
right->_parent = parent;
//将新树的根放入森林
q.push(parent);
}
_root = q.top();
}
~HuffmanTree() {
Destroy(_root);
}
Node* GetRoot() {
return _root;
}
private:
void Destroy(Node*& root) {
if (root) {
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
}
Node* _root;
};
//void TestHuffmanTree() {
// std::vector<int> v = { 2,4,7,9 };
// HuffmanTree<int> hv(v);
//}
FileCompressHuffman.h:
#pragma once
#include<string>
#include<vector>
#include"HuffmanTree.h"
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
using std::string;
struct ByteInfo {
unsigned char _ch;
size_t _appearCount;
string _chCode;
ByteInfo(size_t appearCount = 0)
:_appearCount(appearCount)
{}
ByteInfo operator+(const ByteInfo& x)const {
return _appearCount + x._appearCount;
}
bool operator>(const ByteInfo& x)const {
return _appearCount > x._appearCount;
}
bool operator!=(const ByteInfo& x)const {
return _appearCount != x._appearCount;
}
};
class FileCompressHuffman {
public:
FileCompressHuffman();
void CompressFile(const string& filePath);
void UnCompressFile(const string& filePath);
private:
void GenerateHuffmanCode(HuffmanTreeNode<ByteInfo>* root);
void WriteHeadInfo(const string& filePath, FILE* fOut);
string GetFileSuffix(const string& filePath);
string GetFileName(const string& filePath);
string GetLine(FILE* fIn);
//ByteInfo _fileInfo[256];
std::vector<ByteInfo> _fileInfo;
};
FileCompressHuffmanTree.cpp:
#define _CRT_SECURE_NO_WARNINGS
#include"FileCompressHuffman.h"
FileCompressHuffman::FileCompressHuffman() {
_fileInfo.resize(256);
for (int i = 0; i < 256; ++i) {
_fileInfo[i]._ch = i;
}
}
void FileCompressHuffman::CompressFile(const string& filePath) {
//1.统计源文件中每个字符出现的次数
FILE* fIn = fopen(filePath.c_str(), "rb");
if (fIn == nullptr) {
cout << "待压缩文件打开失败" << endl;
return;
}
unsigned char rdBuff[1024];
while (true) {
size_t rdSize = fread(rdBuff, 1, 1024, fIn);
if (rdSize == 0) {
break;
}
for (size_t i = 0; i < rdSize; ++i) {
_fileInfo[rdBuff[i]]._appearCount++;
}
}
//2.用统计结果创建huffman树
//调用默认构造,使得appearCount等于0
HuffmanTree<ByteInfo> ht(_fileInfo, ByteInfo());
//3.获取huffman编码
GenerateHuffmanCode(ht.GetRoot());
//4.写方便解压的数据信息
string name = GetFileName(filePath);
name += "Compress.hz";
FILE* fout = fopen(name.c_str(), "wb");
WriteHeadInfo(filePath, fout);
//5.用获取到的编码对源文件改写
fseek(fIn, 0, SEEK_SET);//使文件指针从头开始读取
int bitCount = 0;
unsigned char bits = 0;
while (true) {
size_t rdSize = fread(rdBuff, 1, 1024, fIn);
if (rdSize == 0)
break;
for (size_t i = 0; i < rdSize; ++i) {
string& strCode = _fileInfo[rdBuff[i]]._chCode;
for (size_t j = 0; j < strCode.size(); ++j) {
bits <<= 1;
if (strCode[j] == '1') {
bits |= 1;
}
bitCount++;
if (bitCount == 8) {
fputc(bits, fout);
bits = 0;
bitCount = 0;
}
}
}
}
//最后一个的8个比特位可能没有满
if (bitCount > 0 && bitCount < 8) {
bits <<= (8 - bitCount);
fputc(bits, fout);
}
fclose(fIn);
fclose(fout);
}
void FileCompressHuffman::UnCompressFile(const string& filePath) {
if (GetFileSuffix(filePath) != "hz") {
cout << "压缩文件类型匹配失败" << endl;
return;
}
FILE* fIn = fopen(filePath.c_str(), "rb");
if (fIn == nullptr) {
cout << "压缩文件打开失败" << endl;
return;
}
//1.读取后缀
string UnCompressFileName = GetFileName(filePath) + "Un";
string suffixFile = GetLine(fIn);
UnCompressFileName += ".";
UnCompressFileName += suffixFile;
//2.读取频次信息总行数
size_t lineCount = atoi(GetLine(fIn).c_str());
for (size_t i = 0; i < lineCount; ++i) {
string str = GetLine(fIn);//A:1
//注意换行字符的处理
if (str == "") {
str += '\n';
str += GetLine(fIn);
}
//_fileInfo[str[0]]._ch = str[0];
//_fileInfo[str[0]]._appearCount = atoi(str.substr(2).c_str());
_fileInfo[str[0]]._appearCount = atoi(str.c_str()+2);//都可以
unsigned char ch = str[0];
_fileInfo[ch]._ch = ch;
_fileInfo[ch]._appearCount = atoi(str.substr(2).c_str());
}
//3.还原树
HuffmanTree<ByteInfo> ht(_fileInfo, ByteInfo());
//4.解压缩
HuffmanTreeNode<ByteInfo>* cur = ht.GetRoot();
FILE* fout = fopen(UnCompressFileName.c_str(), "wb");
size_t Size = 0;
unsigned char rdBuff[1024];
while (true) {
size_t rdSize = fread(rdBuff, 1, 1024, fIn);
if (rdSize == 0)
break;
for (size_t i = 0; i < rdSize; i++) {
char ch = rdBuff[i];
for (size_t j = 0; j < 8; j++) {
if (ch & 0x80)//如果结果为 0,则说明最高位是 0
//结果是1
cur = cur->_right;
else
cur = cur->_left;
ch <<= 1;
//找到叶子结点
if (cur->_left == nullptr && cur->_right == nullptr) {
fputc(cur->_weight._ch, fout);
cur = ht.GetRoot();
Size++;
if (Size == cur->_weight._appearCount)
break;
}
}
}
}
fclose(fout);
fclose(fIn);
}
string FileCompressHuffman::GetLine(FILE* fIn) {
string line;
char ch;
while ((ch = fgetc(fIn)) != EOF) {
//ch = fgetc(fIn);
if (ch != '\n')
line += ch;
else
break;
}
return line;
}
string FileCompressHuffman::GetFileName(const string& filePath) {
return filePath.substr(0, filePath.rfind('.'));
}
string FileCompressHuffman::GetFileSuffix(const string& filePath) {
return filePath.substr(filePath.rfind('.') + 1);
}
void FileCompressHuffman::WriteHeadInfo(const string& filePath, FILE* fOut) {
//1.获取原文件后缀
string headInfo;
headInfo += GetFileSuffix(filePath);
headInfo += '\n';
//2.获取频次信息
size_t LineCount = 0;
string chInfo;
for (auto e : _fileInfo) {
if (e._appearCount == 0)
continue;
chInfo += e._ch;
chInfo += ':';
chInfo += std::to_string(e._appearCount);
chInfo += '\n';
LineCount++;
}
headInfo += std::to_string(LineCount);
headInfo += '\n';
headInfo += chInfo;
fwrite(headInfo.c_str(), 1, headInfo.size(), fOut);
}
void FileCompressHuffman::GenerateHuffmanCode(HuffmanTreeNode<ByteInfo>* root) {
if (root == nullptr)
return;
GenerateHuffmanCode(root->_left);
GenerateHuffmanCode(root->_right);
//找叶子结点
if (root->_left == nullptr && root->_right == nullptr) {
string& chCode = _fileInfo[root->_weight._ch]._chCode;
HuffmanTreeNode<ByteInfo>* cur = root;
HuffmanTreeNode<ByteInfo>* parent = cur->_parent;
while (parent) {
if (parent->_left == cur) {
chCode += '0';
}
else {
chCode += '1';
}
cur = parent;
parent = cur->_parent;
}
reverse(chCode.begin(), chCode.end());
}
}
Text.cpp:
#define _CRT_SECURE_NO_WARNINGS
//#include"HuffmanTree.h"
#include"FileCompressHuffman.h"
void menu() {
cout << "************************************************" << endl;
cout << "************************************************" << endl;
cout << "******* 0.退出 *******************" << endl;
cout << "******* 1.huffman压缩 *******************" << endl;
cout << "******* 2.huffman解压 *******************" << endl;
cout << "************************************************" << endl;
cout << "************************************************" << endl;
}
int main() {
int input = 0;
bool isQuit = false;
FileCompressHuffman fc;
string fileName;
while (true) {
menu();
cin >> input;
switch (input)
{
case 0:
isQuit = true;
break;
case 1:
cout << "输入压缩文件名:" << endl;
cin >> fileName;
fc.CompressFile(fileName);
break;
case 2:
cout << "输入解压缩文件名:" << endl;
cin >> fileName;
fc.UnCompressFile(fileName);
break;
}
if (isQuit)
break;
}
return 0;
}
遇到的问题:
1.读不了换行后的数据
解决:在读取新一行数据时,判断如果取到的是空,说明此时是换行,那么手动加上'\n',然后再读一遍,取到 :次数,就可以继续正确读取了
2.读不了汉字
常见的汉字编码如 UTF - 8、GBK 等,通常使用多个字节来表示一个汉字。以 UTF - 8 为例,它是一种变长编码,一个汉字可能由 2 到 4 个字节组成,并且这些字节的最高位通常为 1,而char类型通常是 1 个字节,是有符号类型,其取值范围一般是 -128 到 127,也就是说这意味着char的最高位(第 7 位)被用作符号位,0 代表正数,1 代表负数
因为unsigned char是无符号类型,取值范围是 0 到 255。它没有符号位,所有 8 位都用于表示数值
解决:将char换成unsigned char
3.大文件压缩后只有一部分被解压缩了
打开、写入的是文本文件,检测有没有读到末尾,就是看是否到EOF
压缩文件本质是二进制类型的文件,一旦读到文本文件转换成的FF,就认为结束不往后走了
解决:将读取、写入的方式从文本文件改成二进制文件,r/w改成rb/wb
压缩率:
文本文件:
huffman 约等于37%,zip约等于75%
二进制文件:
huffman 约等于24%,zip约等于41%
多次压缩的结果可能会改变,也取决于字符种类的多少,字符种类的多少会影响哈夫曼树的结构和数据分布的复杂性。其中,平均下来要是每个字节的编码小于8位,那么文件会变小,要是多余8位,就会变大
扩展:
范式huffman树:
huffman树压缩完成后,必须在压缩文件中保存字节频率信息后,才可以解压缩,如果字节频率信息比较大,也会影响压缩率。且解压时需要通过不断遍历还原的huffman树,效率也会打折扣,因此在工程中一般很少使用huffman直接进行压缩,而是使用范式huffman树
范式huffman树,是在huffman树的基础之上,进行了一些强制性的约定,即:对于同一层节点中,所有的叶子节点都调整到左边,然后,对于同一层的叶子节点按照符号顺序从小到大调整,最后按照左0右1的方式分配编码。
使用范式huffman树,可以提高压缩效率,提高不了压缩率,就不用从叶子往根去推,编码可以算出来,同一层的编码位长一样,从左往右编码加一,下一层编码加一,再左移层数差位
怎么得到编码表?排序字节频率表,以编码位长为第一字段,编码大小为第二字段,不用保存频率,也就是说只需要保存256个字节,同时也不用走递归了
解压缩时,也只需要编码和编码位长,算出编码,得出解码表