哈夫曼树(霍夫曼树)又称为最优树.
- 1、路径和路径长度
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。
- 2、结点的权及带权路径长度
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
- 3、树的带权路径长度
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
- 有了上面的概念,那么下面再给出huffman树的概念:
假设有n个权值{w1,w2,w3........,wn},试构造一棵有n个叶子结点的二叉树,每个叶子结点带权为wi,则其中带权路径长度WPL最小的二叉树称作最优二叉树或赫夫曼树。那么下面是构造一棵赫夫曼树的方法:
- 假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
下面是huffman.h的实现代码:
#pragma once
#include<queue>
template<typename T>struct Nod{
T _w;
struct Nod *_left;
struct Nod *_right;
Nod(T const& w) :_w(w), _left(NULL), _right(NULL){}
};
template<typename T> class Huffman{
public:
typedef Nod<T> Node;
template<typename Nod>struct Greater{ //仿函数,比较结点里面的值域的大小
bool operator()(Nod const& l, Nod const& r)
{
return l->_w > r->_w;
}
};
Huffman() = default;//默认构造函数
Huffman(T* w,size_t N,T invalue)//传一个数组名和数组的长度进来
{
//priority_queue<Node> pq0;//优先级队列默认大根堆 less的意思是孩子小于双亲
priority_queue<Node*, vector<Node*>, Greater<Node*> > pq;//小根堆,greater的意思是孩子大于双亲
for (size_t i = 0; i < N; ++i){
if (w[i] != invalue)
pq.push(new Node(w[i]));//建立小根堆,压进去是Node*
}
_root = CreatTree(pq);
}
Node* GetRoot()
{
return _root;
}
private:
Node *_root;
Huffman(Huffman<T> const&);//防拷贝
Huffman<T>& operator=(Huffman<T> const&);//防复制
Node* CreatTree(priority_queue<Node*, vector<Node*>, Greater<Node*>> &pq)
{
while (pq.size() > 1){
Node *left = pq.top(); pq.pop();
Node *right = pq.top(); pq.pop();
Node *parent = new Node(left->_w + right->_w); pq.push(parent);
parent->_left = left;parent->_right = right;
}
return pq.top();
}
};
---------------------------------------------我是分界线-------------------------------------------------------------------------
- 构建完了huffman树,开始进行文件压缩。文件压缩可以分为五个步骤:
- 1. 统计字符出现的次数
- 2. 构建HuffmanTree
- 3. 生成哈夫曼编码 (Huffman Code)
- 4. 压缩 (compress)
- 5. 解压缩 (uncompress)
- 下面是文件具体的压缩过程:
Input.txt
内容:aaaabbbcccd
1. 统计字母出现的次数:
a —— 4
b —— 3
c —— 2
d —— 1
2. 构建huffmanTree:
3. 生成huffman code(编码):
a -> 0 (* 出现次数多的字符(数值大)–路径短–编码短)
b -> 11
c -> 101
d -> 100
4. 压缩(按位存储):
文件 :Input.txt.huffman
内容 : 00001111 11101101 100(00000) —-按位存储
第三个字节后面的五个0是补位
(原文件占用10个字节,压缩后占用3个字节)
5. huffman解压缩:
还原文件:Input.txt.unhuffman
- 文件压缩的完整实现代码:
包括huffman.h,compressfile.h和test.cpp。
huffman.h:(如上代码)
compressfile.h
#pragma once
#include"huffman.h"
#include<string>
class FileCompress{
public:
struct _charinfos{//内置类型
char _ch;
size_t _count;
std::string _code;
bool operator!=(_charinfos const& charin)//如果结构体中_count不相等,那么结构体不相等
{
return _count != charin._count;
}
bool operator>(_charinfos const& charin)
{
return _count > charin._count;
}
_charinfos operator+(_charinfos const& charin)
{
_charinfos tmp;
tmp._count = _count + charin._count;
return tmp;
}
};
typedef Nod<_charinfos> Node;
struct _tmpinfos{
char _ch;
size_t _count;
};
FileCompress()//默认构造函数
{
for (size_t i = 0; i < 256; ++i){
charinf[i]._ch = i;
charinf[i]._count = 0;
}
}
void Compress(const char* file)//huffman.txt
{
//1.打开一个文件,对其字符进行统计出现的频率
FILE *fsrc = fopen(file, "rb");
char ch = fgetc(fsrc);
while (!feof(fsrc)){
++charinf[(unsigned char)ch]._count;
ch = fgetc(fsrc);
}
//2.构建一棵赫夫曼树并得到huaffman code
_charinfos invalue; invalue._count = 0;
Huffman<_charinfos> h(charinf, 256, invalue);//数组向指针的转化
auto root = h.GetRoot(),cur = root; string str;
Traverse(cur, str);//遍历赫夫曼树,得到huffman code
//3.压缩文件,把1.中统计的字符放到压缩文件中去
string filedst(file);
filedst += ".huffman";
FILE *fdst = fopen(filedst.c_str(), "wb");//打开目标文件
_tmpinfos tmpinf;
for (size_t i = 0; i < 256; ++i){//为了尽可能方便且少的将信息压入目标文件
if (charinf[i]._count){
tmpinf._ch = i;tmpinf._count = charinf[i]._count;
fwrite(&tmpinf, sizeof(_tmpinfos), 1, fdst);//将有用的信息写入目标文件
}
}tmpinf._count = 0;
fwrite(&tmpinf, sizeof(_tmpinfos), 1, fdst);//多写入一个信息,用作信息结束标志
rewind(fsrc);//将stream指向的流的文件定位符设置在文件的开始位置,等价于fseek(fsrc,0,SEEK_SET)
char ch1 = fgetc(fsrc), ch2 = 0; size_t pos = 0;
while (!feof(fsrc)){
string code = charinf[(unsigned char)ch1]._code;
for (size_t j = 0; j < code.size(); ++j){
ch2 |= code[j] - '0' << pos++;
if (pos == 8){
//int test55 = ch2; cout << test55 << " ";//验证信息,查看到底压入了什么数据
fputc(ch2, fdst);//向目标文件中写入一个每个位都被重置了的字符
ch2 = 0; pos = 0;
}
}
ch1 = fgetc(fsrc);
}
//还要考虑当原文件读到最后一字符个时,ch2的8个位没有被全部重置的情况
if (pos){ /*int test55 = ch2; cout << test55 << " ";*/
fputc(ch2, fdst);
}
fclose(fsrc);
fclose(fdst);
}
void Uncompress(const char* file)//huffmanhzq.txt
{
//4.根据压缩文件重构赫夫曼树
FILE *fout = fopen(file, "rb");
_tmpinfos tmpinf;
fread(&tmpinf, sizeof(_tmpinfos), 1, fout);
while (tmpinf._count){//根据压缩文件,对其字符进行统计出现的频率
charinf[(unsigned char)tmpinf._ch]._ch = tmpinf._ch;
charinf[(unsigned char)tmpinf._ch]._count = tmpinf._count;
fread(&tmpinf, sizeof(_tmpinfos), 1, fout);
}
_charinfos invalue; invalue._count = 0;
Huffman<_charinfos> h(charinf, 256, invalue);//重构赫夫曼树
//5.解压缩文件(root->_w._count记录了原文件中所有字符出现的次数)
string filedst(file);
filedst.erase(filedst.rfind('.')); filedst += ".unhuffman";
FILE *fin = fopen(filedst.c_str(), "wb");//以写的方式打开文件
auto root = h.GetRoot(), cur = root;
char ch = fgetc(fout); size_t n = root->_w._count;
while (!feof(fout)){
//int test55 = ch; cout << test55 << " ";//验证信息,查看到底拿到了什么数据
for (size_t i = 0; i < 8; ++i){
if (ch & 1 << i){ cur = cur->_right; }
else{ cur = cur->_left; }
if (!cur->_left && !cur->_right){
fputc(cur->_w._ch, fin);
--n;//因为n记录了原文件所有字符出现的次数,所以当n==0时表示已经翻译完
cur = root;
}
if (!n)break;
}
ch = fgetc(fout);
}
fclose(fout);
fclose(fin);
}
private:
_charinfos charinf[256];
void Traverse(Node* const& root, string str)//遍历赫夫曼树,得到huffman code
{
if (!root->_left && !root->_right){//走到叶子结点将huffman code信息写入并返回
charinf[(unsigned char)root->_w._ch]._code = str;
return;
}
Traverse(root->_left, str + '0');
Traverse(root->_right, str + '1');
}
};
test.cpp:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include<CoreWindow.h>
#include"FileCompress.h"
void test_compressfile()
{
FileCompress fc;
fc.Compress("input.txt"); //从input.txt到input.txt.huffman
fc.Uncompress("input.txt.huffman"); //从input.txt.huffman到input.txt.unhuffman
//FileCompress fc;
//fc.Compress("hehe.jpg"); //从hehe.jpg到hehe.jpg.huffman
//fc.Uncompress("hehe.jpg.huffman");//从hehe.jpg.huffman到hehe.jpg.unhuffman
}
int main()
{
test_compressfile();
system("pause");
return 0;
}
最后讲一下在进行文件压缩的时候可能会遇到的问题:(ps:别人总结的很好,下面是摘抄笔记)
1.压缩带中文的文件,程序就会崩溃。
最后发现数组越界的问题. \
因为char它的范围是-128~127,程序中使用char类型为数组下标(0~127),所以字符没有问题,但是汉字是占两个字节的,所以会出现越界的问题,解决的方法就是char类型强转为unsigned char,它的范围为0~255.
2.解压缩文件生成后会丢失很多内容.
1. 文件的打开方式.
这里打开文件一定要用二进制形式,"wb","rb".因为二进制打开和文本打开其实是有区别的。
1.文本方式打开:会对‘\n’进行特殊处理,那如果这个字符本身就是'\n'.这就会出现问题。
2.二进制方式打开:不进行任何处理,是什么就是什么。
2. 文件结束符的问题。
刚开始用的文件结束标志是EOF在ASII中它的编码是-1,随后我们用了 二进制方式打开文件,而二进制文件是会出现-1的,所以提前结束了,所以我们用二进制读取并且不能用EOF来判断结束。
C中有一个函数叫做feof(),它可以检查流上文件的结束符,如果文件结束,则返回非0值,否则返回0
下面引用百科的解释:
feof(fp)有两个返回值:如果遇到文件结束,函数feof(fp)的值为非零值,否则为0。
EOF是文本文件结束的标志。在文本文件中,数据是以字符的ASCⅡ代码值的形式存放,普通字符的ASCⅡ代码的范围是32到127(十进制),EOF的16进制代码为0x1A(十进制为26),因此可以用EOF作为文件结束标志。[1]
当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ASCI C提供一个feof函数,用来判断文件是否结束。feof函数既可用以判断二进制文件又可用以判断文本文件。