Huffman树的构建,
利用huffman编码对源文件进行压缩和解压缩
(文章结尾有源码链接)
通过LZ77变形思想对源数据进行语句的重复压缩之后,语句层面的重复性已经解决,但并不是已经达到最佳,字节层面可能也有大量重复。
比如:“BCDCDDBDDCADCBDC”
一个字节占8个比特位,那如果能对所有字节找到小于8个比特位的编码,然后用找到的编码对源文件中对应字节重新进行改写,也可以让源文件更小。那如何找编码呢?
静态等长编码:每个字符的编码长度都相等。
A:00 B:01 C:10 D:11
01101110 11110111 11100011 10011110 //压缩完成后的结果只占4个字节,压缩率还是比较高的。
动态不等长编码:每个字符的编码根据具体的字符情况来确定
A:00 B:01 C:10 D:11
01101110 11110111 11100011 10011110//压缩完成后的结果只占4个字节,压缩率还是比较高的。
动态不等长编码:每个字符的编码根据具体的字符情况来确定
A:100 B:101 C:11 D:0
10111011 00101001 11000111 01011 //压缩完成后最后一个字节没有用完,还剩余3个比特位,显然动态不等长编码比等长编码压缩率能好点。
如何获得不等长编码?
解决方式:huffman编码(通过建立huffman树,获得相应字符的编码)
#huffman树
##什么是huffman树?
从二叉树的根节点到二叉树中所有叶节点的路径长度与相应权值的乘积之和为该二叉树的带权路径长度WPL。即带权路径最小的二叉树成为Huffman树。
##huffman树构建
1.由给定的n个权值{w1,w2,w3,…,wn}构造n棵只有根节点的二叉树森林F={T1,T2,T3,…,Tn};每个二叉树Ti只有一个带权值wi的根节点,左右孩子均为空。
2.重复以下步骤,直到F中只剩下一棵树为止
在F中选取两颗根节点权值最小的二叉树,作为左右子树构造一棵新的二叉树,新二叉树根节点的权值为其左右子树根节点的权值之和。
在F中删除这两颗二叉树
把新的二叉树加入到F中
HuffmanTree.hpp(构建Huffman树)
#pragma once
using namespace std;
//#include
#include "FileCompressHuff.h"
#include
#include
template<class W>
struct HuffManTreeNode
{
HuffManTreeNode(const W& weight = W())//内置类型:0 自定义类型:无参构造函数 全缺省构造函数
: _pLeft(nullptr)
, _pRight(nullptr)
, _pParent(nullptr)
,_weight(weight)
{}
HuffManTreeNode<W>*_pLeft;
HuffManTreeNode<W>* _pRight;
HuffManTreeNode<W>* _pParent;
W _weight;//节点的权值
};
template<class W>
class Less
{
typedef HuffManTreeNode<W> Node;
public:
bool operator()(const Node* pLeft, const Node* pRight)
{
return pLeft->_weight > pRight->_weight;
}
};
template<class W>
class HuffManTree
{
typedef HuffManTreeNode<W> Node;
public:
HuffManTree()
:_pRoot(nullptr)
{}
HuffManTree(const vector<W>& vWeight,const W& invalid)//有效的权值和无效(字符出现0次)的权值
{
CreateHuffManTree(vWeight,invalid);
}
~HuffManTree()
{
_DestroyTree(_pRoot);
}
Node* GetRoot()
{
return _pRoot;
}
void CreateHuffManTree(const vector<W>& vWeight, const W& invalid)
{
//1.构建森林
std::priority_queue<Node*,vector<Node *>,Less<W>> q;
for (auto e : vWeight)
{
if (e == invalid)
continue;
q.push(new Node(e));
}
//2.
while (q.size() > 1)
{
Node* pLeft = q.top();
q.pop();
Node* pRight = q.top();
q.pop();
Node* pParent = new Node(pLeft->_weight + pRight->_weight);
pParent->_pLeft = pLeft;
pParent->_pRight = pRight;
pLeft->_pParent = pParent;
pRight->_pParent = pParent;
q.push(pParent);
}
_pRoot = q.top();
}
private:
void _DestroyTree(Node*& pRoot)
{
if (pRoot)
{
_DestroyTree(pRoot->_pLeft);
_DestroyTree(pRoot->_pRight);
delete pRoot;
pRoot = nullptr;
}
}
private:
Node* _pRoot;
};
#利用huffman编码对源文件进行压缩
1.统计源文件中每个字符出现的次数
2.以字符出现的次数为权值创建huffman树
3.通过huffman树获取每个字符对应的huffman编码
读取源文件,对源文件中的每个字符使用获取的huffman编码进行改写,将改写结果写道压缩文件中,直到文件结束。
FileCoppressHuff.h (基于huffman思想的的文件压缩)
#pragma once
#include
#include
#include "HuffmanTree.hpp"
struct charInfo
{
unsigned char _ch; //具体的字符
size_t _count; //字符出现的次数
std::string _strCode; //字符的编码
charInfo(size_t count=0)
:_count(count)
{}
charInfo operator+(const charInfo& c)
{
return charInfo(_count + c._count);
}
bool operator>(const charInfo& c)const
{
return _count > c._count;
}
bool operator==(const charInfo& c)const
{
return _count == c._count;
}
};
class FileCompressHuff
{
public:
FileCompressHuff();
void CompressFile(const std::string& path);
void UNCompressFile(const std::string& path);
private:
void GenerateHuffManCode(HuffManTreeNode<charInfo> *pRoot);//生成编码
void WriteHead(FILE* fOut, const string& filePostFix);//增加文件信息
string GetFilePostFix(const string& fileName);//获取文件后缀
void ReadLine(FILE* fIn, string &strInfo);
private:
std::vector<charInfo> _fileInfo;
};
FileCoppressHuff.cpp
#include "FileCompressHuff.h"
#include "HuffmanTree.hpp"
#include
FileCompressHuff::FileCompressHuff()//构造函数--完成初始化
{
_fileInfo.resize(256);
for (int i = 0; i < 256; i++)
{
_fileInfo[i]._ch = i;
_fileInfo[i]._count = 0;
}
}
void FileCompressHuff::CompressFile(const std::string& path)
{
//1.统计源文件中每个字符出现的次数
FILE* fIn = fopen(path.c_str(), "rb");
if (nullptr == fIn)
{
assert(false);
return;
}
char* pReadBuff = new char[1024];
int rdSize = 0;
while (true)
{
rdSize = fread(pReadBuff, 1, 1024, fIn);
if (0 == rdSize)
break;
for (int i = 0; i < rdSize; i++)
{
_fileInfo[(unsigned char)pReadBuff[i]]._count++;
}
}
//2.以字符出现的次数为权值,创建HuffManTree
HuffManTree<charInfo> t(_fileInfo,charInfo(0));
//3.获取每个字符的编码
GenerateHuffManCode(t.GetRoot());
//4.用获取的字符编码重新改写编码
FILE* fOut = fopen("2.txt", "wb");
if (nullptr == fOut)
{
assert(false);
return;
}
WriteHead(fOut, path);//解压缩的信息
//+解压缩数据
fseek(fIn,0,SEEK_SET);//修改文件指针指向起始位置
char ch = 0;
size_t bitcount = 0;//统计读到的比特位个数
while (true)
{
rdSize = fread(pReadBuff, 1, 1024, fIn);
if (0 == rdSize)
break;
//根据字节的编码对读取到的内容进行重写
for (int i = 0; i < rdSize; i++)
{
string strCode=_fileInfo[(unsigned char)pReadBuff[i]]._strCode;
//A:"110" B:"101"
for (size_t j = 0; j< strCode.size(); j++)
{
ch <<= 1;
if ('1' == strCode[j])
ch |= 1;
bitcount++;
if (8 == bitcount)
{
fputc(ch, fOut);//读取一个字节(8个比特位)
bitcount = 0;
ch = 0;
}
}
}
}
//最后一个可能不够8个比特位
if (bitcount < 8)
{
ch <<= (8 - bitcount);//剩下的比特位数值移到高位
fputc(ch, fOut);
}
delete[] pReadBuff;
fclose(fIn);
fclose(fOut);
}
//获取每个字符的编码
//获取每个字符的编码
void FileCompressHuff::GenerateHuffManCode(HuffManTreeNode<charInfo> *pRoot)//根到叶子的路径
{
if (nullptr == pRoot)
return;
GenerateHuffManCode(pRoot->_pLeft);
GenerateHuffManCode(pRoot->_pRight);
if (nullptr == pRoot->_pLeft&&nullptr == pRoot->_pRight)//递归pRoot就是叶子节点
{
string& strCode = _fileInfo[pRoot->_weight._ch]._strCode;
HuffManTreeNode<charInfo>* pCur = pRoot;
HuffManTreeNode<charInfo>* pParent = pCur->_pParent;
while (pParent)
{
if (pCur == pParent->_pLeft)//左子树是0 右子树1
{
strCode += '0';
}
else
{
strCode += '1';
}
pCur = pParent;
pParent = pCur->_pParent;
}
reverse(strCode.begin(), strCode.end());
//_fileInfo[pRoot->_weight._ch]._strCode= strCode;//_fileInfo里面的每一个元素都是一个结构体
}
}
##压缩文件格式
源文件后缀+
字符次数出现的总行数+
字符以及字符出现次数(每个字符放置一行)+
压缩数据
.txt
4
A:1
B:3
C:5
D:7
?荴
压缩文件只保存压缩之后的数据可以吗?
答案是不行的,因为在解压缩时,没有办法进行解压缩。
比如:10111011 00101001 11000111 01011,只有压缩数据是没有办法进行解压缩的,因此压缩文件除了要保存压缩数据,还必须保存解压缩需要用到的信息。
void FileCompressHuff::WriteHead(FILE* fOut, const string& fileName)//文件压缩信息
{
assert(fOut);
//写文件后缀
string strHead;
strHead += GetFilePostFix(fileName);
strHead += '\n';
//写行数
size_t lineCount = 0;
string strChCount;//字符的次数
char szValue[32] = { 0 };
for (int i = 0; i < 256; i++)
{
charInfo& charInfo = _fileInfo[i];
if (_fileInfo[i]._count)//出现字符的次数不为0
{
lineCount++;
strChCount += _fileInfo[i]._ch;
strChCount += ':';
//char * itoa ( int value, char * str, int base );
_itoa(charInfo._count, szValue, 10);//字符转整形
strChCount += szValue;
strChCount += '\n';
}
}
_itoa(lineCount, szValue, 10);
strHead += szValue;//+行数
strHead += '\n';
//写字符信息
strHead += strChCount;//+每个字符的信息
fwrite(strHead.c_str(),1,strHead.size(),fOut);
}
//2.txt F:\123\2.txt
string FileCompressHuff::GetFilePostFix(const string& fileName)
{
return fileName.substr(fileName.rfind('.'));
}
//读取字符出现的总行数
void FileCompressHuff::ReadLine(FILE* fIn, string &strInfo)
{
assert(fIn);
while (!feof(fIn))//没有到文件的末尾
{
char ch = fgetc(fIn);
if (ch == '\n')
break;
strInfo += ch;
}
}
##解压缩
1.从压缩文件中获取源文件的后缀
2.从压缩文件中获取字符次数的总行数
3.获取每个字符出现的次数
4.重建huffman树
5.解压缩从压缩数据中一个一个字节的获取压缩数据,每获取到一个字节的压缩数据,从根节点开始,按照该字节的二进制比特位信息遍历huffman树,该比特位是0,取当前节点的左孩子,是1取当前节点的右孩子,直到遍历到叶子节点位置,该字符就被解析成功。继续此过程,直到所有的数据解析完毕。
void FileCompressHuff::UNCompressFile(const std::string& path)
{
FILE* fIn = fopen(path.c_str(), "rb");
if (nullptr == fIn)
{
assert(false);
return;
}
//文件后缀
string strFilePostFix;
ReadLine(fIn, strFilePostFix);
//字符信息的总行数
string strCount;
ReadLine(fIn, strCount);
int lineCount = atoi(strCount.c_str());
//字符信息
for (int i = 0; i < lineCount; i++)
{
string strchCount;
ReadLine(fIn, strchCount);
//如果读取到的是\n
if (strchCount.empty())
{
strchCount += '\n';
ReadLine(fIn, strchCount);
}
//A:1
_fileInfo[(unsigned char)strchCount[0]]._count = atoi(strchCount.c_str() + 2);
}
//还原哈夫曼树
HuffManTree<charInfo> t;
t.CreateHuffManTree(_fileInfo, charInfo(0));
FILE* fOut = fopen("3.txt","wb");
assert(fOut);
//解压缩
unsigned char* pReadBuff = new unsigned char[1024];
HuffManTreeNode<charInfo>* pCur = t.GetRoot();
size_t fileSize = pCur->_weight._count;//文件组的大小就是字节组中字符的个数
size_t unCount = 0;//解压缩的个数
char ch = 0;
while (true)
{
size_t rdSize = fread(pReadBuff, 1, 1024, fIn);
if (0 == rdSize)
break;
for (size_t i = 0; i < rdSize; i++)
{
//只需将一个字节中8个比特位单独处理
ch = pReadBuff[i];
for (int pos = 0; pos < 8; pos++)
{
if (ch & 0x80)//检测高位
pCur = pCur->_pRight;
else
pCur = pCur->_pLeft;
ch <<= 1;
if (nullptr == pCur->_pLeft&&nullptr == pCur->_pRight)
{
fputc(pCur->_weight._ch, fOut);
unCount++;
if (unCount == fileSize)
break;
pCur = t.GetRoot();//每次写一个字符,然后指向根节点
}
}
}
}
//压缩数据
delete[] pReadBuff;
fclose(fIn);
fclose(fOut);
}
源码:https://github.com/uniquefairty/C-Code/tree/master/HuffmanTreeZip