本篇文章主要包含4个方面:
1.哈夫曼算法实现压缩的原理
2.具体压缩及解压过程思路阐述
3.项目中遇到的问题
4.项目扩展
一、原理简述:
huffman算法实现文件压缩的主要原理是通过huffman编码来重新表示字符,使得出现频率高的字符编码短,出现少的字符编码长。当用编码表示原文件时,总体的bit位时相对减少的。但当大部分字符出现的频率都差不多时,huffman压缩的压缩效率会很低。
二、具体压缩及解压思路:
压缩:
1. 统计字符出现的次数
由于所有的文件在电脑中都是以二进制的形式存储的,打开文本图片音乐视频的工具就是一种解码的过程,打开不同的文件则依据的是不同的解码规则。例如,图片是由一个个像素构成的,像素呢又是由二进制组成,所以同一样可以使用ASCII码保存信息。
我们使用一个容量为256个元素的数组来统计字符出现的次数,通过结构体将次数、字符、huffman编码相对应。
2.构建哈夫曼树
采用哈夫曼算法,将一组集合中权值最小的两棵树拿出来,以他们的权值之和作为父节点插入到这个集合中,不断重复,直到集合中只有一棵树。这棵树就是为哈夫曼树,再次我们采用最小堆来寻找这两个最小的数。在此我们以字符出现次数作为权值构建哈夫曼树,这样一来出现次数越多的字符就越接近根结点。
3.得到哈夫曼编码
有了哈夫曼树,我们通过从根结点出发走到各个叶子节点,向左走为0,向右走为1,就得到每个字符的哈夫曼编码。因此我们如果有哈夫曼树就可以将哈夫曼编码翻译为具体字符。
4.压缩
先写入配置信息,由于在解压时是没有原文件的,因为我们的压缩文件里存储的是哈夫曼编码,要接压必须先构建哈夫曼树,从而才能将哈夫曼编码通过发福曼树解释为字符。哈夫曼树是根据字符及次数构建的,所以我们存入字符和对应次数即可。
重新读取源文件,逐个将字符转换为对应的哈夫曼编码存放到压缩文件中。
解压缩(原理和压缩相似):
1.打开压缩文件并读取配置信息;
2.根据配置信息,建立哈夫曼树;
3.解压缩
在压缩文件中逐个字符读取,解析该字符的每一位,才用贪心法,只要遇到一个叶子节点就还原对应的字符,并将该字符存放到解压缩的文件里面。此时应注意,压缩文件的最后几位可能是我们补上去的,通过哈夫曼树性质可知,根结点的权值就是所有字符出现的总次数,我们可以借总次数来控制解压。
三、在项目中遇到的问题
1.解压时解压不完全
当用文本方式读取压缩文件时,由于是以哈夫曼编码(二进制数)存储,用EOF(宏,定义为-1)判断文件结尾时有可能提前遇到文件结束标志,因为二进制文件中-1是可以实现的。所以应采用二进制形式打开并使用 feof() 函数判断文件结束。
如果以文本方式读取,要把 ‘\r’(回车)、‘\n’(换行)两个字符转换为一个字符\n,而二进制形式则不需要处理。
2.二次压缩效率低
因为压缩之后,配置信息中字符出现的次数都相差不大,体现不出来哈夫曼的特性,所以再压缩的话效率会非常底。
3.汉字压缩时出现问题
由于汉字是用多个字符表示的,这些字符的范围是0-255,所以应该用 unsigned char 声明读取的字符。
四、项目扩展
实现了对文件夹的压缩,对文件夹实际上就是对文件夹中的内容进行压缩,所以找到一个子文件后就一直向里找,直到对找到的文件进行压缩。
MyHeap.h
#pragma once
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;
//堆排序
//仿函数 定义排列的方法,实现排列代码的复用
template<class T>
struct UpOrder
{
bool operator()(const T i, const T j)
{
return i < j;
}
};
template<class T>
struct DownOrder
{
bool operator()(const T i, const T j)
{
return i > j;
}
};
//堆排列,默认升序排列方法,即排列成小堆
template<class T, class Compare = DownOrder<T>>
class Heap
{
public:
Heap()
{}
Heap(T*arr, int size)
{
//将数组中的顺序插入顺序表中
_a.reserve(size);
for (int i = 0; i < size; i++)
{
_a.push_back(arr[i]);
}
//建堆
for (int i = (size - 2) / 2; i >= 0; i--)
{
AdJustDown(i, size);
}
}
void Sort(T* arr, int size)
{
assert(arr);
int i = (size - 2) >> 1;//找到倒数第一个非叶节点
for (; i >= 0; i--)//从倒数第一个非叶节点开始往上排序
{
AdJustDown(i, size);
}
}
void Push(const T &a)
{
_a.push_back(a);
AdJustUp(_a.size()-1);
}
size_t Size()
{
return _a.size();
}
void Printf()
{
for (size_t i = 0; i < _a.size(); i++)
{
cout << _a[i] << " ";
}
cout << endl;
}
const T& Top()
{
return _a[0];
}
void Pop()//将堆顶元素和最后一个元素交换,删除最后一个,再调整顺序
{
assert(!_a.empty());
swap(_a[0], _a[_a.size() - 1]);
_a.pop_back();
if (_a.size() > 1)
{
AdJustDown(0, _a.size());
}
}
protected:
void AdJustDown(int root, int size)
{
assert(!_a.empty());
int parent = root; //用parent指针接收要排序的根结点
int child = parent * 2 + 1; //child表示该parent的左孩子结点
while (child < size)
{
//如果右孩子存在并且左孩子的值大于右孩子,则让child指针指向右孩子
if (((child + 1) < size) && Compare()(_a[child], _a[child + 1]))
{
child++;
}
if (Compare()(_a[parent], _a[child])) //若parent大则交换父子结点的值
{
swap(_a[parent], _a[child]);
parent = child; //继续往下比较
child = 2 * parent + 1;
}
else
{
break;
}
}
}
void AdJustUp(int child)
{
assert(!_a.empty());
while (child>0)
{
int parent = (child-1)>>1;//找到倒数第一个非叶节点
if (Compare()(_a[parent], _a[child]))
{
swap(_a[parent], _a[child]);
child = parent;
}
else
{
break;
}
}
}
private:
vector<T> _a;
};
FileCompress.h
#pragma once
#include<string>
#include<algorithm> //算法头文件,可直接使用reverse函数
#include<io.h>
#include<direct.h>
#include "MyHufmanTree.h"
typedef long long LongType;
//定义结构体存放字符信息
struct FileInfo
{
FileInfo(LongType appearCount = 0)
:_appearCount(appearCount)
{}
FileInfo operator+(const FileInfo &info)const
{
return FileInfo(_appearCount + info._appearCount);
}
bool operator != (const FileInfo &info)const
{
return _appearCount != info._appearCount;
}
bool operator == (const FileInfo &info)const
{
return _appearCount == info._appearCount;
}
bool operator<(const FileInfo &info)const
{
return _appearCount < info._appearCount;
}
bool operator>(const FileInfo &info)const
{
return _appearCount > info._appearCount;
}
unsigned char _ch; //字符
LongType _appearCount;//字符出现的次数
string _strCode; //字符对应的哈夫曼编码,定义为字符串
};
class HuffCompressFile
{
public:
struct _HuffmanInfo
{
unsigned char _ch;
LongType _count;
};
const string Compressfile(string filename) //传入要压缩的文件名,返回已压缩的文件名
{
vector<string>file;
string path = filename.c_str();//c_str()函数返回一个指向该字符串的指针常量
getFiles(path, file);//把各级文件夹信息保存到file数组中
if (file.empty())//如果为空,则表示是一个文件,直接压缩
{
return _Compress(filename);
}
else //文件夹
{
//首先创建一个新的文件夹
string newpath = path; //新的压缩后文件夹的路径名
newpath += ".huf";
_mkdir(newpath.c_str());
for (int i = 0; i < (int)file.size(); i++)
{
_Compress(file[i], newpath);
}
return newpath; //返回新建的压缩文件夹的名字
}
}
const string _UnCompressfile(string filename)
{
vector<string>file;
string path = filename.c_str();//c_str()函数返回一个指向该字符串的指针常量
getFiles(path, file);//把各级文件夹信息保存到file数组中
if (file.empty())//如果为空,则表示是一个文件进行压缩
{
return _UnCompress(filename);
}
else //文件夹
{
//首先创建一个新的文件夹
string newpath = filename; //新文件夹
for (int i = (int)filename.size() - 1; i >= 0; i--)
{
if (filename[i] == '.')
{
newpath.resize(i);
break;
}
}
newpath += ".uhuf";
_mkdir(newpath.c_str()); //创建一个新的解压缩文件
for (int i = 0; i < (int)file.size(); i++)
{
_UnCompress(file[i], newpath);
}
return newpath; //返回新建的解压缩文件夹的名字
}
}
protected:
//初始化所有字符
void HuffFileCompress()
{
for (size_t index = 0; index < 256; ++index)
{
_fileInfo[index]._ch = index;
_fileInfo[index]._appearCount = 0;
}
}
const string _Compress(const string filename, const string path = string())//
{
HuffFileCompress();//初始化结点
//1.获取源文件中每个字符出现的次数
FILE* fp = fopen(filename.c_str(), "rb");
assert(fp);
unsigned char ch = fgetc(fp);
assert(ch);
while (!feof(fp))
{
_fileInfo[ch]._appearCount++;
ch = fgetc(fp);
}
//2.根据字符出现的次数构建哈夫曼树,树中得体现出字符和次数
FileInfo invalid;
invalid._appearCount = 0;//出现0次的字符不用来创建哈夫曼树
HuffmanTree<FileInfo> hf(_fileInfo, 256, invalid);
//3.通过哈夫曼树获得每个字符的哈夫曼编码,遍历到叶子结点即可得到该结点哈夫曼编码
//由于需要从树顶向根部遍历,所以需要parent指针,即树节点是三岔链
HuffmanStrCode(hf.Top());
//4.压缩(遍历文档,字符转换为strcode) 压缩文件后缀定为.huf
FILE* fIn = NULL;
string CompressFileName = filename;
CompressFileName += ".huf";
if (path.empty()) //为空表示是单个文件
{
//第二次fOut打开 filename.huf
fIn = fopen(CompressFileName.c_str(), "wb");
assert(fIn);
}
else //不为空表示是文件夹
{
//得到要创建的路径
string FileName; //得到文件名
int i = filename.size() - 1;
for (; i >= 0; i--)
{
if (filename[i] == '\\')
break;
FileName += filename[i];
}
reverse(FileName.begin(), FileName.end());
string newpath = path;
newpath += '\\';
newpath += FileName;
newpath += ".huf";
fIn = fopen(newpath.c_str(), "wb"); //打开压缩文件
assert(fIn);
}
//写配置信息(字符出现的次数),解压时根据配置信息方能正确解压
_HuffmanInfo info;
for (size_t i = 0; i < 256; ++i)
{
if (_fileInfo[i]._appearCount)//出现过的以结构体的形式直接写进去
{
info._ch = _fileInfo[i]._ch;
info._count = _fileInfo[i]._appearCount;
size_t size = fwrite(&info, sizeof(_HuffmanInfo), 1, fIn);
assert(size = sizeof(_HuffmanInfo));
}
}
//如何知道写了多少个info进去? 区分配置信息和压缩信息
info._count = 0;//已写的info count肯定不为0,用来隔离配置信息和压缩信息
fwrite(&info, sizeof(_HuffmanInfo), 1, fIn);
unsigned char value = 0;
int count = 0;
fseek(fp, 0, SEEK_SET); //fp指向文件的开始
ch = fgetc(fp); //逐个字符读取
assert(ch);
while (!feof(fp))
{
string &code = _fileInfo[ch]._strCode;
for (size_t i = 0; i < code.size(); i++)
{
value <<= 1;
if (code[i] == '1')
{
value |= 1;
}
else
{
value |= 0;
}
++count;
if (count == 8)//满8位写入一次
{
fputc(value, fIn);
value = 0;
count = 0;
}
}
ch = fgetc(fp);
}
if (count != 0) //
{
value <<= (8 - count);
fputc(value, fIn);
}
fclose(fIn);
fclose(fp);
return CompressFileName;
}
const string _UnCompress(const string filename,const string path = string())
{
//得到解压缩之后的文件的名字
string name;
name = filename;
int i = 0;
string posfix;
for (i = (int)filename.size() - 1; i >= 0; --i) //找到后缀出现的位置
{
posfix.push_back(filename[i]);
if (filename[i] == '.')
break;
}
reverse(posfix.begin(), posfix.end());//让posfix保存要解压文件的后缀
if (posfix != ".huf") //如果要解压的不是huffman压缩的则不能解压
{
return string();
}
//1.改变文件后缀
name.resize(i);
string UnCompressFileName = name; //得到压缩文件名
UnCompressFileName += ".unhuf";
FILE *fInput = fopen(filename.c_str(), "rb");
assert(fInput);
FILE *fOut = NULL; //解压缩文件
if (path.empty()) //如果为空,表示是单个文件解压
{
//打开解压缩文件
fOut = fopen(UnCompressFileName.c_str(), "wb");
if (fOut == NULL)
{
fclose(fInput);
exit(EXIT_FAILURE);
}
}
else //文件夹进行解压缩
{
string FileName; //先得到压缩的文件名
for (int i = (int)name.size() - 1; i >= 0; i--)
{
if (name[i] == '\\')
{
break;
}
FileName += name[i];
}
reverse(FileName.begin(), FileName.end());
string newpath = path;
newpath += "\\";
newpath += FileName;
newpath += ".uhuf";
//打开解压缩文件
fOut = fopen(newpath.c_str(), "wb");
if (fOut == NULL)
{
fclose(fInput);
exit(EXIT_FAILURE);
}
}
//2.重建哈夫曼树(根结点是所有叶子结点的和)
//先读取字符次数信息,再重建哈弗曼树
FileInfo UnComInfo[256];
_HuffmanInfo info;
while (1)
{
size_t size = fread(&info, sizeof(_HuffmanInfo), 1, fInput);
assert(size = sizeof(_HuffmanInfo));
if (info._count > 0)
{
UnComInfo[(unsigned char)info._ch]._ch = info._ch;//可能出现汉字,所以用unsigned char
UnComInfo[(unsigned char)info._ch]._appearCount = info._count;
}
else
{
break;
}
}
FileInfo invalid;
invalid._appearCount = 0;//出现0次的字符不用来创建哈夫曼树
HuffmanTree<FileInfo> tree(UnComInfo, 256, invalid);
HuffmanTreeNode<FileInfo>* root = tree.Top();
LongType charCount = root->_w._appearCount; //所有结点出现的次数,即有效字符出现的个数
//3.解压缩
FILE* fIn = fopen(UnCompressFileName.c_str(), "wb");
assert(fIn);
//获取8位后和1000 0000 按位与
unsigned char value = fgetc(fInput); //每次读取一个字节,一个字节八个位
HuffmanTreeNode<FileInfo>*cur = root;
while (!feof(fIn))
{
for (int tmp = 7; tmp >= 0; --tmp) //获取压缩文件存放的二进制编码
{
if (value &(1 << tmp)) //逐个位判断为1或0,从而决定叶节点寻找的方向
cur = cur->_right;
else
cur = cur->_left;
if (cur->_left == NULL && cur->_right == NULL)
{
fputc(cur->_w._ch, fIn);
cur = root;
if (--charCount == 0)//有效字符已经读完
{
goto end;
}
}
}
value = fgetc(fInput);
}
end:
fclose(fIn);
fclose(fInput);
return UnCompressFileName;
}
void HuffmanStrCode(HuffmanTreeNode<FileInfo>*head)
{
if (head)
{
HuffmanStrCode(head->_left);
HuffmanStrCode(head->_right);
if (head->_left == NULL && head->_right == NULL) //找到叶子结点
{
//从叶子结点到根结点遍历
HuffmanTreeNode<FileInfo> *cur = head;
HuffmanTreeNode<FileInfo> *parent = head->_parent;
string& code = _fileInfo[head->_w._ch]._strCode;
while (parent)
{
if (parent->_left == cur)
code.push_back('0');
else
code.push_back('1');
cur = parent;
parent = parent->_parent;
}
reverse(code.begin(), code.end());
}
}
}
void getFiles(string path, vector<string>& files)
{
//文件句柄
long hFile = 0;
//文件信息
struct _finddata_t fileinfo;//该结构体是用来存储文件的各种信息
string p;
//string &assign(const char *s);用c类型字符串s赋值,append 指追加
if ((hFile = _findfirst(p.assign(path).append("\\*").c_str(), &fileinfo)) != -1)
{
do
{
//如果是目录,迭代之
//如果不是,加入列表
if ((fileinfo.attrib & _A_SUBDIR))//判断当前文件夹是否为一个文件夹
{
if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)//不为空就向里递归
getFiles(p.assign(path).append("\\").append(fileinfo.name), files);
}
else
{
files.push_back(p.assign(path).append("\\").append(fileinfo.name));//
}
}while (_findnext(hFile, &fileinfo) == 0);//查找成功
_findclose(hFile);
}
}
private:
FileInfo _fileInfo[256];
};
MyHufmanTree.h
#pragma once
#include"MyHeap.h"
template <class W>
struct HuffmanTreeNode
{
HuffmanTreeNode<W>* _left;
HuffmanTreeNode<W>* _right;
HuffmanTreeNode<W>* _parent;
W _w;
HuffmanTreeNode<W>(const W&w)
: _left(NULL)
, _right(NULL)
, _parent(NULL)
, _w(w)
{}
};
template<class W>
class HuffmanTree
{
typedef HuffmanTreeNode<W> Node;
public:
HuffmanTree()
:_root(NULL)
{}
~HuffmanTree()
{
Destory(_root);
}
HuffmanTree(W*a, size_t n, const W&invalid)//定义非法值,遇到不创建
{
struct Compare
{
bool operator()(const Node*left, const Node* right)
{
return left->_w > right->_w;
}
};
Heap<Node*> minHeap;
for (size_t i = 0; i < n; ++i)
{
if (a[i] != invalid)
{
minHeap.Push(new Node(a[i]));
}
}
while (minHeap.Size() > 1)
{
//取两个权值最小的节点
Node* left = minHeap.Top();
minHeap.Pop();
Node*right = minHeap.Top();
minHeap.Pop();
Node*parent = new Node(left->_w + right->_w);
parent->_left = left;
parent->_right = right;
left->_parent = parent;
right->_parent = parent;
minHeap.Push(parent);
}
_root = minHeap.Top();
}
void Destory(Node* head)
{
if (head)
{
Destory(head->_left);
Destory(head->_right);
delete head;
head = NULL;
}
}
void Printf()
{
_printf(_root);
cout << endl;
}
Node* Top()
{
return _root;
}
private:
Node* _root;
};
test.cpp
#include "MyHeap.h"
#include "MyHufmanTree.h"
#include "FileCompress.h"
void testCompress()
{
HuffCompressFile hf;
string str;
str = hf.Compressfile("1");
string out;
out = hf._UnCompressfile("1.huf");
}
int main()
{
testCompress();
system("pause");
return 0;
}