Huffman树的应用-------实现文件压缩项目
Huffman树的相关定义:
WPL(带权路径长度) = PL*weight
PL (最小路径长度)= 完全二叉树的路径长度
路径(path):从树中一个结点到另一个结点之间的分支构成该俩点之间的路径。
路径长度:指路径上的分支条数。树的路径长度是从树的根结点到每一个结点的路径长度之和。
带权路径长度最小的二叉树是权值大的外结点(带有权值的叶子结点)离根结点最近的扩充二叉树,这就是Huffman树。
Huffman树又称为最优二叉树,是一类加权路径长度最短的二叉树,在编码设计、决策和算法设计等领域有着广泛应用。
用贪心算法去构建Huffman树,
贪心算法:
指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的选择,而是某种意义上的局部最优解,贪心算法不是对所有问题得到整体最优解。
注:在贪心算法中使用了一个最小堆,利用它从中选择根结点权值最小和次小的俩颗树。
如下图所示:
构建Huffman树的算法会用到最小堆,则实现堆的代码如下:
Heap.h
#pragma once
#include<assert.h>
#include<vector>
//仿函数
template <class T>
struct GreaterCompare
{
bool operator()(const T& l, const T& r)
{
return l > r;
}
};
template<class T>
struct LessCompare
{
bool operator()(const T& l, const T& r)
{
return l < r;
}
};
template <class T,class Compare = LessCompare<T>>
class Heap
{
public:
Heap()
{}
Heap(const T* arr, int len)
{
assert(arr);
assert(len > 0);
int i = 0;
//传入数据
for (; i < len; ++i)
{
_arr.push_back(arr[i]);
}
//建堆
i = _arr.size();
for (i = (i - 2) / 2; i >= 0;--i)
{
_AdjustDown(i);
}
}
int Size()
{
return _arr.size();
}
bool Empty()
{
return _arr.empty();
}
//插入数据
void Push(const T& x)
{
_arr.push_back(x);
_AdjustUp(_arr.size()-1);
}
//删除数据
void Pop()
{
if (!Empty())
{
swap(_arr[0], _arr[_arr.size() - 1]);
_arr.pop_back();
_AdjustDown(0);
}
}
T Top()
{
return _arr[0];
}
~Heap()
{}
protected:
//向下调整
void _AdjustDown(int root)
{
Compare com;
int child = root * 2 + 1;
while (child < (int)_arr.size())
{
if (child + 1 < (int)_arr.size() && com(_arr[child + 1], _arr[child]))
++child;
if (com(_arr[child] , _arr[root]))
{
swap(_arr[child], _arr[root]);
root = child;
child = 2 * root + 1;
}
else
{
break;
}
}
}
//向上调整
void _AdjustUp(int child)
{
Compare com;
int root = (child - 1) / 2;
while (child > 0)
{
if (com(_arr[child], _arr[root]))
{
swap(_arr[child], _arr[root]);
child = root;
root = (child - 1) / 2;
}
else
{
break;
}
}
}
private:
vector<T> _arr;
};
用贪心算法构建Huffman树的代码如下:
HuffmanTree.h
#pragma once
#include"Heap.h"
template<class T>
struct HuffmanTreeNode
{
HuffmanTreeNode<T>* _left; //左孩子
HuffmanTreeNode<T>* _right; //右孩子
T _weight; //数据(此处称为权值)
HuffmanTreeNode(T w = 0)
:_left(NULL)
, _right(NULL)
, _weight(w)
{}
};
//重写仿函数
template<class T>
struct HuffmanLessCompare
{
bool operator()(HuffmanTreeNode<T>* l, HuffmanTreeNode<T>* r)
{
return l->_weight < r->_weight;
}
};
template <class T, class Compare = HuffmanLessCompare<T>>
class HuffmanTree
{
typedef HuffmanTreeNode<T> Node;
public:
HuffmanTree()
:_root(NULL)
{}
//贪心算法实现哈弗曼树的建立
HuffmanTree(const T* arr, int len,const T invalid)
{
assert(arr);
assert(len > 0);
_root = _CreateHuffmanTree(arr, len, invalid);
}
Node* GetRoot()
{
return _root;
}
protected:
//压缩文件中加入invalid,当count = 0时不插入,所以文件压缩.h中引入了函数去重载!=
Node* _CreateHuffmanTree(const T* arr, int len,const T invalid)
{
Heap<Node*,Compare> hp;
for (int i = 0; i < len; ++i)
{
if (arr[i] != invalid)
{
hp.Push(new Node(arr[i]));
}
}
while (hp.Size () > 1)
{
Node* left =hp.Top();
hp.Pop();
Node* right =hp.Top();
hp.Pop();
//权值相加就是CharInfo相加,即是CharInfo中的次数相加,所以文件压缩.h中引入了函数去重载+
Node* root = new Node(left->_weight + right->_weight);
root->_left = left;
root->_right = right;
hp.Push(root);
}
return hp.Top();
}
private:
Node* _root;
};
Huffman树的应用:Huffman编码技术
Huffman编码技术是数据压缩的重要方法
Huffman编码如下图所示:
项目文件压缩的内容:
文件压缩:
1.在文本文件中,数据是以字符的ASCII码的形式存放,ASCII码的范围是0-255,所以文件压缩中用元素个数为256的数组作为底层数据结构,其中元素类型为CharInfo,包括字符,字符出现的次数,字符编码;
2.在编码前我们先要统计各个字符出现的次数;
3.根据统计的次数作为权值构建哈弗曼树;
4.递归遍历哈弗曼树生成每个字符对应的编码,编码从根结点到叶子结点;
5.将压缩编码写入压缩文件中;
6.编写配置文件保存各个字符出现的次数以便解压时重建哈夫曼树;
文件解压缩:
7.根据源文件找到配置文件;
8.将配置文件中的数据(各个字符及各个字符出现的次数)读到底层数据结构数组中;
9.根据读到的数据(数组)作为权值重新构建哈夫曼树;
10.读取压缩文件,遍历哈弗曼树,二者结合去还原源文件。
项目文件压缩的具体实现代码如下:
注:其中用到的构建Huffman树,及构建Huffman树算法中用到的最小堆的具体实现都在上面;
FileCompress.h
#pragma once
#include"HuffmanTree.h"
#include<string>
typedef unsigned long LongType;
struct CharInfo
{
unsigned char _ch; //字符
LongType _count; //字符出现的次数
string _code; //字符的编码
CharInfo(LongType count = 0)
:_ch(0)
,_count(count)
{}
bool operator!=(const CharInfo& c) const
{
return _count != c._count;
}
CharInfo operator+(const CharInfo& c)
{
CharInfo tmp;
tmp._count = _count + c._count;
return tmp;
}
bool operator<(const CharInfo& c)
{
return _count < c._count;
}
};
class FileCompress
{
public:
//所有字符就位
FileCompress()
{
for (int i = 0; i < 256; ++i)
{
_arr[i]._ch = i;
}
}
//文件压缩
void CompressFile(const char* fileName)
{
assert(fileName);
//1.统计文件中各个字符出现的次数
FILE* fout = fopen(fileName, "rb");//以只读的方式打开二进制文件
assert(fout);
unsigned char ch = fgetc(fout);
1.(1).统计有多少个字符,以便在解压缩时补0位的字符也多余出来
//LongType allCount = 0;
while (!feof(fout)) //(char)ch != EOF
{
//++allCount;
_arr[ch]._count++;
ch = fgetc(fout);
}
//2.(1).根据统计的次数作为权值构建哈弗曼树
CharInfo invalid(0);
HuffmanTree<CharInfo> hf(_arr, 256, invalid);
//2.(2).生成每个字符所对应的编码
string code;
_HuffmanCodeOfWeight(hf.GetRoot(),code);
//3.将压缩编码写入压缩文件中
string CompressFileName = fileName;
CompressFileName += ".compress";
FILE* input = fopen(CompressFileName.c_str(), "wb");
assert(input);
//3.(1).重定位流(数据流/文件)上的文件内部位置指针
fseek(fout, 0, SEEK_SET);
int eightBit = 0;
unsigned char bit = 0;
ch = fgetc(fout);
while (!feof(fout)) //(char)ch != EOF
{
string& code = _arr[(unsigned char)ch]._code;
for (int i = 0; i < (int)code.size(); ++i)
{
bit <<= 1;
if (code[i] == '1')
bit |= 1;
if (++eightBit == 8)
{
fputc(bit, input);
bit = 0;
eightBit = 0;
}
}
ch = fgetc(fout);
}
//3.(2).将最后一个字节不够8位的往高位移动,其余补0,并写入文件
if (eightBit)
{
bit <<= (8 - eightBit);
fputc(bit, input);
}
//4.编写配置文件保存字符出现的次数,以便解压时重新构建哈弗曼树
string configName = fileName;
configName += ".config";
FILE* configFile = fopen(configName.c_str(), "wb");//以写的方式打开二进制文件
assert(configFile);
//4.(1).配置文件中保存的字符的次数以字符串的形式存储
char character[20];
4.(2).写入总次数,以便解压缩最后一个补0的字节正常恢复
//_itoa(allCount, character, 10);
//fputs(character, configFile);
//fputc('\n', configFile);
//4.(3).写入各个字符及其出现的总次数,出现字符的格式:字符,次数\n
CharInfo _invalid(0);
for (int i = 0; i < 256; ++i)
{
if (_arr[i] != _invalid)
{
fputc(_arr[i]._ch, configFile);
fputc(',', configFile);
//_count是long long型,当大文件(字符次数出现次数超过整型)出现,
//需要分俩段写,先写高位,再写低位?
_itoa(_arr[i]._count, character, 10);
fputs(character, configFile);
fputc('\n', configFile);
}
}
fclose(fout);
fclose(input);
fclose(configFile);
}
//文件的解压缩
void UnCompress(char* fileName)
{
assert(fileName);
//1.根据原文件找到配置文件
string ConfigFileName = fileName;
ConfigFileName += ".config";
FILE* ConfigFileOutput = fopen(ConfigFileName.c_str(), "r");
string line;
2.(1).得出所有字符出现总次数
//ReadLine(ConfigFileOutput, line);
//LongType allCount = atoi(line.c_str());
//line.clear();
//2.(2).将配置文件中剩余的字符与各个字符的次数还原到数据结构CharInfo数组中
while (ReadLine(ConfigFileOutput, line))
{
if (!line.empty())
{
unsigned char ch = line[0];
_arr[ch]._count = atoi(line.substr(2).c_str());
line.clear();
}
else
{
//若统计到有个空行则为换行符
line += '\n';
}
}
//3.根据配置信息读到的次数作为权值重新构建哈弗曼树
CharInfo invalid(0);
HuffmanTree<CharInfo> hf(_arr, 256, invalid);
HuffmanTreeNode<CharInfo>* root = hf.GetRoot();
HuffmanTreeNode<CharInfo>* cur = root;
//4.读取压缩文件与哈弗曼树一起去还原原文件
string CompressFileName = fileName;
CompressFileName += ".compress";
FILE* Output = fopen(CompressFileName.c_str(), "rb");
assert(Output);
string UnCompressFileName = fileName;
UnCompressFileName += ".uncompress";
FILE* Input = fopen(UnCompressFileName.c_str(), "wb");
assert(Input);
//4.(0).Huffman树的根结点的权值就是就是所有字符出现次数的总和
LongType allCount = hf.GetRoot()->_weight._count;
int eightBit = 8;
unsigned char ch = fgetc(Output);
while (!feof(Output)) //(char)ch != EOF
{
//4.(1).遍历哈弗曼树,通过ch去判断走左树还是走右树
--eightBit;
if (ch & (1 << eightBit))
cur = cur->_right;
else
cur = cur->_left;
//4.(2).读到一个字符,写入解压缩文件中,
if (cur->_left == NULL && cur->_right == NULL)
{
fputc(cur->_weight._ch, Input);
cur = root;
//4.(3).用总次数去控制循环,防止补0位被解析为字符
if (--allCount == 0)
{
break;
}
}
//4.(4).8个位解析完,读取下一个字符
if (eightBit == 0)
{
ch = fgetc(Output);
eightBit = 8;
}
}
fclose(ConfigFileOutput);
fclose(Output);
fclose(Input);
}
~FileCompress()
{}
protected:
//2.(2).生成每个字符(叶子节点)所对应的编码
void _HuffmanCodeOfWeight(HuffmanTreeNode<CharInfo>* root, string& code)
{
if (root == NULL)
return;
//叶子结点为编码结点
if (root->_left == NULL && root->_right == NULL)
_arr[root->_weight._ch]._code = code;
_HuffmanCodeOfWeight(root->_left, code + '0');
_HuffmanCodeOfWeight(root->_right, code + '1');
}
bool ReadLine(FILE* configFile, string& line)
{
assert(configFile);
unsigned char ch = fgetc(configFile);
if ((char)ch == EOF)
return false;
while ((char)ch != EOF && ch != '\n')
{
line += ch;
ch = fgetc(configFile);
}
return true;
}
private:
CharInfo _arr[256];
};
项目的测试用例如下:
#include<iostream>
using namespace std;
#include<assert.h>
#include"FileCompress.h"
#include<windows.h>
void TestFileCompress()
{
FileCompress fc;
int begin = GetTickCount();
//fc.CompressFile("Input.txt");
fc.CompressFile("Input.BIG");
int end = GetTickCount();
cout <<"FileCompressCostTime: " <<end - begin << endl;
}
void TestFileUnCompress()
{
FileCompress fc;
int begin = GetTickCount();
//fc.UnCompress("Input.txt");
fc.UnCompress("Input.BIG");
int end = GetTickCount();
cout <<"FileUnCompressCostTime: "<< end - begin << endl;
}
int main()
{
TestFileCompress();
TestFileUnCompress();
return 0;
}
项目中遇到的问题:
0.注意一些函数调用中的参数为const char*,而自己定义了string类型变量,记得c++到c的转换函数,string.c_str();
1.注意文本的字符范围(ASCII码)是0-255,类型为unsigned char,否则文件压缩会出现问题;
2.文件解压缩过程中的补位0的问题,用字符出现的总次数去控制循环,防止补位0被解析为字符;
3.字符出现的总次数不需要存放在配置文件中,Huffman树的根结点的权值就是所有字符出现次数的总和;
4.最后一个问题让我调试了多次,就是EOF(EOF的16进制代码为0xFF(十进制为-1))是文本文件结束的标志,当把数据以二进制的形式存放到文件中时,就会有-1的值出现,因此不能采用EOF作为二进制文件的结束标志,为解决此问题,ASCII提供了一个feof函数,用来判断文件是否结束,feof既可以判断二进制文件,又可以判断文本文件。
注:函数feof的简介:
函数原型:int feof(FILE* stream);
功能:检测流上的文件结束符;
返回值:如果文件结束,则返回值非0值,否则返回0;
项目测试结果如下图所示: