HuffmanTree
定义
哈弗曼树是一种优化的二叉树,称为最优二叉树,是加权路径长度最小的二叉树。所谓权值在这里指的是节点中的数据。本文的哈弗曼树用数组提供数据,例如:arr[]={1,2,3,4,5,6}创建的哈弗曼树见下图:
图中蓝色的节点值是数组arr提供,红色节点值是两个孩子节点值相加得到。
名词解释
节点的权值:权就相当于重要程度,通过一个具体的数字来表示
路径:在树中从一个节点到另一个节点的分支。
路径长度:一条路径上的分支数量。
树的路径长度:从树的根节点到每个节点的路径长度之和。
树的带权路径长度:树中各个叶子节点的路径长度*该叶子节点的权的和。
性质
1、根节点值是所有的叶子节点值相加得到
2、创建哈弗曼树的节点值全部在叶子节点上,而且只在叶子节点出现
3、哈夫曼树的加权路径长度是最小的,这个是因为权值大的节点离根节点近,权值小的节点离根节点远。
创建HuffmanTree
1、取出数组中最小的两个值,组成最小的一个分支,用两者的和作为两者的父亲节点并在原数组中替换两者。
2、依次循环直到数组所有符合要求的值被使用。
使用示例:arr[] = {1,2,3,4,5,6};
节点定义
哈弗曼树节点采用二叉链结构,包含关键值_val;指向左节点的指针_left;指向右节点的指针_right。
template <class T>
struct HuffmanTreeNode{
T _val;
HuffmanTreeNode<T>* _left;
HuffmanTreeNode<T>* _right;
HuffmanTreeNode(const T& val)
:_val(val)
,_left(NULL)
,_right(NULL)
{}
};
构造函数
构造函数提供了两个,一个是空的构造函数,防止因为没有默认构造函数产生错误。另一个是带参的构造函数。
参数解释
arr是一个数组名,用来提供创建哈弗曼树的数值来源;size是数组的大小,这个大小在构建最小堆的时候会用到;invalid指的是非法值,如果数组中有一些特定的值,不希望插入到哈弗曼树中,用这个值来判断。一般来说,所有的非法值是相同的。
构建的思想
构建哈夫曼树的方式在上面已经提到,但是如何用代码来实现呢?这里可以借用最小堆来实现。如果不太熟悉最小堆可以看这个传送门。每次取最小堆的堆顶元素第一次是作为左节点,第二次作为右节点。同时将两者出堆,接着用两者的关键值创建一个父亲节点入堆。利用循环就可以创建出来哈弗曼树了。至于为什么要用最小堆,根据前述的方法,我们需要每次取数组中的最小的两个值来构建新的节点,用最小堆的话可以分两次取堆顶数据,然后让创建的新的节点再入堆。最小堆会自动调整,不需要人为参与修改。而且堆的存储机制就是动态增长的数组,天然符合这里利用数组传参的要求。其中这里传入给最小堆的数据类型是节点指针,为什么是节点指针很好理解。如果是节点本身,那么我们就没有办法将之前创建的内容链接在一起,见下图:
为什么不用节点的引用呢?引用的话不太好,如果是引用,可能出现节点结构太大的情况,这样开销有点大,不划算。这里还需要解释一下,需要自己创建一个比较仿函数,因为Node是一个自定义类型,在创建最小堆的时候是没有办法知道比较方式的,仿函数如下:(其中为了书写方便,在HuffmanTree类中定义了节点Node)
typedef HuffmanTreeNode<T> Node;
struct NodeCompare{
//const Node* left, const Node* right
//wrong
bool operator()(Node* left, Node* right){
return left->_val < right->_val;
}
};
这里需要注意的是,仿函数的参数不能够定义为const对象。需要修改。
构建的代码如下:注意循环结束的条件是最小堆中只有一个元素,因为此时已经构建完成。
HuffmanTree()
:_root(NULL)
{}
HuffmanTree(T* arr, size_t size, const T& invalid){
//注意这里使用的是Node*
Heap<Node*, NodeCompare> minheap;
for(size_t i = 0; i < size; ++i){
if(arr[i] != invalid){
//Node*
minheap.Push(new Node(arr[i]));
}
}
//minheap.Size()>1
while(minheap.Size() > 1){
Node* left = minheap.Top();
minheap.Pop();
Node* right = minheap.Top();
minheap.Pop();
Node* parent = new Node(left->_val + right->_val);
parent->_left = left;
parent->_right = right;
minheap.Push(parent);
}
_root = minheap.Top();
}
析构函数
析构函数通过递归实现,内部调用Destroy实现。
//destructor
~HuffmanTree(){
Destroy(_root);
_root = NULL;
}
void Destroy(Node* root){
if(root){
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
}
获取根节点函数
//GetRoot
Node* GetRoot(){
return _root;
}
补充
因为没有实现拷贝构造和赋值运算符的重载,所以将他们都声明为私有成员变量。防止错误使用导致未知错误
文件压缩及解压缩
有了哈弗曼树,我们就可以利用哈弗曼树创建哈夫曼编码,通过哈夫曼编码可以实现文件压缩的功能。
哈夫曼编码
所谓哈夫曼编码是在哈弗曼树的基础上,定义向左的路径为零,向右的路径为一。这样定义以后,每一个叶子节点都会有一个唯一的编码值。而重要的是,在哈弗曼树中,所有叶子节点就是我们用来构建哈弗曼树的原值。使用示例:arr[] = {1,2,3,4,5,6};如下图:
需要注意的是,哈夫曼编码不是唯一的,就算是同样的原值,因为插入的顺序不一样会导致不同的编码,但是毋庸置疑的是,每一个叶子节点一定会有唯一的编码值。
压缩思路
1、扫描文件内容,统计文件中字符出现的次数。
2、利用文件出现次数构建哈弗曼树。
3、创建哈夫曼编码。
4、将创建哈弗曼树的字符及其对应出现次数写入压缩文件中,用于解压使用。
5、将哈夫曼编码替换原子符写入压缩文件。
压缩原理
举例:文件test.txt中内容:aaaaaabbbbbccccdddeef
其中字符出现的次数统计:a->6;b->5;c->4;d->3;e->2;f->1。利用次数构建的哈弗曼树和编码如下:
图中黑色字体是出现的字符,蓝色数字是对应出现的次数,红色字体是构建时两字符出现次数相加之和。为什么通过哈夫曼编码可以压缩文件?我们通过编码将原字符替换,只有当编码的长度超过一个字节的时候(也就是八个比特位)才会比替换之前的字符大小要大。但是出现次数多的字符的编码都十分的短,出现次数少的字符编码才会超过一个字节。这样抵消之下,肯定是相比于压缩之前文件大小要小。上例子中,写入压缩文件的压缩编码如下:
压缩与解压代码分析
结构体分析
结构体CharInfo是用来存储字符、字符出现的次数、字符的编码三者的结构类型。其中将_count定义为long long类型,防止字符出现的次数超过了整型表示范围。_code用string类型;通过这个结构体可以将三者紧紧地联系在一起。其中对该结构体重载了!=、+、< 这三个运算符。在代码中都会用到。
typedef long long LongType;
struct CharInfo{
char _ch;
LongType _count;
string _code;
//for invalid
bool operator!=(const CharInfo& info){
return _count != info._count;
}
//for minheap.parent
CharInfo operator+(const CharInfo& info){
CharInfo tmp;
tmp._count = _count + info._count;
return tmp;
}
//for compare
bool operator<(const CharInfo& info){
return _count < info._count;
}
};
构造函数
本文使用的文件压缩类类名为FileCompress。类的私有成员是一个CharInfo类型的数组。其大小是256,因为字符包括汉字一共只有256个。所以直接指定数组大小就好。
CharInfo _infos[256];
构造函数直接将数组的下标赋值给结构体CharInfo的_ch成员,因为字符型数据就是通过整型转换过去的。这样就相当于给数组的每一个元素都指定好了对应字符。例如:_infos[97]对应字符’a’。
FileCompress(){
for(size_t i = 0; i < 256; ++i){
_infos[i]._ch = i;
_infos[i]._count = 0;
_infos[i]._code = " ";
}
}
压缩函数
压缩函数分为五步,步骤在代码中详细的分析,这里分别说一下每个步骤注意的点:
1、统计。统计时,要注意用二进制方式打开文件,用二进制模式打开的文件不会对任何字符有转义的动作,文本模式下,window操作系统会将’\n’和’\r’进行编码转换。而且写入的时候,也需要用二进制方式写入,否则会出现压缩没有问题,但是解压的时候提前结束的情况。
2、构建哈弗曼树。定义非法值,其字符个数为零个。
3、构建哈夫曼编码调用函数。
4、写入构建哈弗曼树的字符和对出现次数。调用fopen函数,要将string类转换为char*格式;创建一个没有string类的临时结构体,因为构建哈弗曼树只需要ch和count;将构件用的源写入之后,再写入一个标志位,用字符个数为-1表示,用于跟替换的编码相互隔开。
5、用编码将字符替换。在进行这一步之前,将文本指针指向文章开头。因为统计字数的时候,文本指针已经指向了文末。替换的步骤是:从源文件中获取一个字符,通过该字符获得之前已经编好的哈夫曼编码,获取编码的每一个位,如果编码为’1’,将value当前位置为1;反之,置为0。依次进行,当该字符的编码全部用完,获取下一个字符,一直到value的八个位都替换完成, 将value写入压缩文件中。为什么要到一个字节才写入,这个是因为函数能写入的最小的单位是字节。
void Compress(const char* file){
//fopen的打开方式,用二进制方式打开
FILE* fout = fopen(file, "rb");
assert(fout);
//1、统计文章字符出现的次数
//fgetc的返回值是整型
int ch = fgetc(fout);
while(ch != EOF){
++ _infos[(unsigned char)ch]._count;
ch = fgetc(fout);
}
//2、构建哈夫曼树
CharInfo invilad;
invilad._count = 0;
HuffmanTree<CharInfo> tree(_infos, 256, invilad);
//code不要初始化,不然会出现编码错误
string code;
//3、构建哈夫曼编码
GenerateHuffmanTreeCode(tree.GetRoot(), code);
//4、写入构建哈弗曼树的字符和对出现次数
string compressfile = file;
compressfile += ".huffman";
FILE* fin = fopen(compressfile.c_str(), "wb");
assert(fin);
for(int i = 0; i < 256; ++i){
if(_infos[i]._count != 0){
TmpCharInfo info;
info._ch = _infos[i]._ch;
info._count = _infos[i]._count;
fwrite(&info, sizeof(info), 1, fin);
}
}
//设置判断位,让其跟压缩内容分割开来
TmpCharInfo info;
info._count = -1;
fwrite(&info, sizeof(info), 1, fin);
//5、用编码将字符替换。
fseek(fout, 0, SEEK_SET);
ch = fgetc(fout);
//value是用来写入到压缩文件中的一个字节
char value = 0;
//pos两个作用,一个是当前替换的位置,另一个是用来判断是不是已经满八个比特位
size_t pos = 0;
while(ch != EOF){
string& code = _infos[(unsigned char)ch]._code;
for(size_t i = 0; i < code.size(); ++i){
if(code[i] == '1'){
value |= (1 << pos);
}
else if(code[i] == '0'){
value &= ~(1 << pos);
}
else{
assert(false);
}
++ pos;
if(pos == 8){
fputc(value, fin);
value = pos = 0;
}
}
ch = fgetc(fout);
}
//特殊处理未满八个位也将剩下的字符全部写入。
//正是因为这一步,才会有说上图中用来填充的五个零的出现。
if(pos > 0){
fputc(value, fin);
}
fclose(fout);
fclose(fin);
}
构建哈夫曼编码函数
这个函数利用递归实现,如果当前走向哈弗曼树的左子树code+0,如果是右子树code+1。如果当前节点是叶子节点那么修改对应字符的_code为code的值。因为这里用的code是值传递,当递归函数返回上一层的时候,当前层的修改不会被带回去。
如上图中的根节点6,它的code是空字符,第一层递归的时候,进入节点3,此时的code是值传递,会进行一次值拷贝,节点3的code是“0”,再下一层,进入节点1,同样是值拷贝,节点1的code是“00”。此时节点1的叶子节点,写入字符’1’的_code为字符串”00”。这个就是它的编码。然后退回到节点3,它的code依然是“0”。然后再进入到节点2,它的code是“01”,叶子节点,写入字符’2’的_code是字符串”01”。这个是他的编码。
//GenerateHuffmanTreeCode
void GenerateHuffmanTreeCode(const HTreeNode* root, string code){
if(root == NULL)
return;
if(root->_left == NULL && root->_right == NULL){
//change _infos
_infos[(unsigned char)(root->_val._ch)]._code = code;
return;
}
GenerateHuffmanTreeCode(root->_left, code+'0');
GenerateHuffmanTreeCode(root->_right, code+'1');
}
解压函数
解压函数的思路:
1、从压缩文件中读取构建哈弗曼树的字符和次数
2、构建哈夫曼树
3、从压缩文件中读取编码,结合哈弗曼树将编码翻译为字符写入解压文件中。
//unCompress
void UnCompress(const char* file){
//获取压缩文件名字,同时设定解压文件的文件名
string uncompressfile = file;
size_t pos = uncompressfile.rfind('.');
if(pos == string::npos){
perror("pos wrong");
exit(1);
}
uncompressfile.erase(pos);
uncompressfile += ".unhuffman";
//解压
FILE* fin = fopen(uncompressfile.c_str(), "wb");
assert(fin);
//从压缩文件中获取构建哈弗曼树的字符和对应的出现的次数。
FILE* fout = fopen(file, "rb");
assert(fout);
TmpCharInfo info;
fread(&info, sizeof(info), 1, fout);
while(info._count != -1){
_infos[(unsigned char)info._ch]._ch = info._ch;
_infos[(unsigned char)info._ch]._count = info._count;
fread(&info, sizeof(info), 1, fout);
}
//创建哈弗曼树,用来翻译编码使用
CharInfo invalid;
invalid._count = 0;
HuffmanTree<CharInfo> tree(_infos, 256, invalid);
HTreeNode* root = tree.GetRoot();
HTreeNode* cur = root;
LongType num = root->_val._count;
int ch = fgetc(fout);
//解决文件中只有一个字符的情况
if(root->_left == NULL){
while(num--){
fputc(root->_val._ch, fin);
}
}
else{
while(ch != EOF){
for(size_t i = 0; i < 8; ++i){
//按照编码规则,遇到1向右走,遇到0向左走。
if(((unsigned char)ch & (1 << i)) == 0){
cur = cur->_left;
}
else{
cur = cur->_right;
}
//叶子节点写入翻译字符
if(cur->_left == NULL && cur->_right == NULL){
//限制翻译的字符的个数,防止将补充的字符也翻译。
if(num-- == 0){
break;
}
fputc(cur->_val._ch, fin);
//指向root,解压下一个字符
cur = root;
}
}
//当前读取的一个字节已经用完,继续解压下一个字节
ch = fgetc(fout);
}
}
fclose(fin);
fclose(fout);
}
注意:
1、读取编码转换字符的方法是:获取到的编码一定是一个字节的,但是其中可能包含了多个编码,此时结合哈弗曼树,如果编码遇到的是’1’,在哈弗曼树中向右子树中走,如果是’0’,在哈夫曼中向左子树走。一直遇到叶子节点的时候,表示此时已经翻译成功一个字符,将该字符写入解压文件中。同时将指向节点的指针重新指向根节点,此时需要解压下一个字符。
2、如果原文件中只有一个字符,此时是构不成哈弗曼树的,需要特殊处理这种情况。
3、需要控制翻译的字符的个数。因为压缩文件中最后几个位的字符可能是当初为了写入一个字节而填充的,这些并不是源文件中含有的字符。因此这些字符不能被翻译。根据哈弗曼树的性质可以知道,根节点的值就是所有叶子节点的关键值之和。所以可以获取根节点的关键值,每翻译一个字符,就将该关键值减一,当关键值为零的时候表示原文件中所有的字符都已经被翻译出来了,此时不管是否还能读取到字符,都应该直接跳出写入字符的循环,防止写入多余的无用值。
代码
//Heap.h
Heap
//Huffman.h
#pragma once
#include "Heap.h"
template <class T>
struct HuffmanTreeNode{
T _val;
HuffmanTreeNode<T>* _left;
HuffmanTreeNode<T>* _right;
HuffmanTreeNode(const T& val)
:_val(val)
,_left(NULL)
,_right(NULL)
{}
};
template <class T>
class HuffmanTree{
typedef HuffmanTreeNode<T> Node;
struct NodeCompare{
//const Node* left, const Node* right
//wrong
bool operator()(Node* left, Node* right){
return left->_val < right->_val;
}
};
public:
//constructor
HuffmanTree()
:_root(NULL)
{}
HuffmanTree(T* arr, size_t size, const T& invalid){
Heap<Node*, NodeCompare> minheap;
for(size_t i = 0; i < size; ++i){
if(arr[i] != invalid){
//Node*
minheap.Push(new Node(arr[i]));
}
}
//minheap.Size()>1
while(minheap.Size() > 1){
Node* left = minheap.Top();
minheap.Pop();
Node* right = minheap.Top();
minheap.Pop();
Node* parent = new Node(left->_val + right->_val);
parent->_left = left;
parent->_right = right;
minheap.Push(parent);
}
_root = minheap.Top();
}
//destructor
~HuffmanTree(){
Destroy(_root);
_root = NULL;
}
void Destroy(Node* root){
if(root){
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
}
//GetRoot
Node* GetRoot(){
return _root;
}
private:
HuffmanTree(const HuffmanTree<T>& huf);
Node*& operator=(const HuffmanTree<T>& huf);
Node* _root;
};
void TestHuffmanTree(){
int arr[] = {0,1,2,3,4,5,6,7,8,9};
size_t Size = sizeof(arr)/sizeof(arr[0]);
HuffmanTree<int> hp(arr, Size, arr[0]);
cout << hp.GetRoot()->_val << endl;
}
//Compress.h
#pragma once
#include <string>
#include <cstdio>
#include <cstring>
#include <assert.h>
#include "HuffmanTree.h"
typedef long long LongType;
struct CharInfo{
char _ch;
LongType _count;
string _code;
//for invalid
bool operator!=(const CharInfo& info){
return _count != info._count;
}
//for minheap.parent
CharInfo operator+(const CharInfo& info){
CharInfo tmp;
tmp._count = _count + info._count;
return tmp;
}
//for compare
bool operator<(const CharInfo& info){
return _count < info._count;
}
};
class FileCompress{
typedef HuffmanTreeNode<CharInfo> HTreeNode;
struct TmpCharInfo{
unsigned char _ch;
LongType _count;
};
public:
//constructor
FileCompress(){
for(size_t i = 0; i < 256; ++i){
_infos[i]._ch = i;
_infos[i]._count = 0;
_infos[i]._code = " ";
}
}
//Compress
void Compress(const char* file){
FILE* fout = fopen(file, "rb");
assert(fout);
int ch = fgetc(fout);
while(ch != EOF){
++ _infos[(unsigned char)ch]._count;
ch = fgetc(fout);
}
//create HuffmanTree
CharInfo invilad;
invilad._count = 0;
HuffmanTree<CharInfo> tree(_infos, 256, invilad);
string code;
GenerateHuffmanTreeCode(tree.GetRoot(), code);
//write
string compressfile = file;
compressfile += ".huffman";
FILE* fin = fopen(compressfile.c_str(), "wb");
assert(fin);
//write source
for(int i = 0; i < 256; ++i){
if(_infos[i]._count != 0){
TmpCharInfo info;
info._ch = _infos[i]._ch;
info._count = _infos[i]._count;
fwrite(&info, sizeof(info), 1, fin);
}
}
//flag of the end of source
TmpCharInfo info;
info._count = -1;
fwrite(&info, sizeof(info), 1, fin);
//write code
fseek(fout, 0, SEEK_SET);
ch = fgetc(fout);
char value = 0;
size_t pos = 0;
while(ch != EOF){
string& code = _infos[(unsigned char)ch]._code;
for(size_t i = 0; i < code.size(); ++i){
if(code[i] == '1'){
value |= (1 << pos);
}
else if(code[i] == '0'){
value &= ~(1 << pos);
}
else{
assert(false);
}
++ pos;
if(pos == 8){
fputc(value, fin);
value = pos = 0;
}
}
ch = fgetc(fout);
}
if(pos > 0){
fputc(value, fin);
}
fclose(fout);
fclose(fin);
}
//GenerateHuffmanTreeCode
void GenerateHuffmanTreeCode(const HTreeNode* root, string code){
if(root == NULL)
return;
if(root->_left == NULL && root->_right == NULL){
//change _infos
_infos[(unsigned char)(root->_val._ch)]._code = code;
return;
}
GenerateHuffmanTreeCode(root->_left, code+'0');
GenerateHuffmanTreeCode(root->_right, code+'1');
}
//unCompress
void UnCompress(const char* file){
//change the name of file
string uncompressfile = file;
size_t pos = uncompressfile.rfind('.');
if(pos == string::npos){
perror("pos wrong");
exit(1);
}
uncompressfile.erase(pos);
uncompressfile += ".unhuffman";
//uncompress
//open fin
FILE* fin = fopen(uncompressfile.c_str(), "wb");
assert(fin);
//read to get ch and count
FILE* fout = fopen(file, "rb");
assert(fout);
TmpCharInfo info;
fread(&info, sizeof(info), 1, fout);
while(info._count != -1){
_infos[(unsigned char)info._ch]._ch = info._ch;
_infos[(unsigned char)info._ch]._count = info._count;
fread(&info, sizeof(info), 1, fout);
}
//create HuffmanTree
CharInfo invalid;
invalid._count = 0;
HuffmanTree<CharInfo> tree(_infos, 256, invalid);
HTreeNode* root = tree.GetRoot();
HTreeNode* cur = root;
LongType num = root->_val._count;
int ch = fgetc(fout);
//olny root
if(root->_left == NULL){
while(num--){
fputc(root->_val._ch, fin);
}
}
//more than root
else{
while(ch != EOF){
for(size_t i = 0; i < 8; ++i){
if(((unsigned char)ch & (1 << i)) == 0){
cur = cur->_left;
}
else{
cur = cur->_right;
}
if(cur->_left == NULL && cur->_right == NULL){
if(num-- == 0){
break;
}
fputc(cur->_val._ch, fin);
cur = root;
}
}
ch = fgetc(fout);
}
}
fclose(fin);
fclose(fout);
}
protected:
CharInfo _infos[256];
};
void TestFileCompress(){
FileCompress fc;
FileCompress fcu;
fc.Compress("file");
fcu.UnCompress("file.huffman");
}