准备工作
//将字符信息包装成为一个结构体
struct CharInfo{
CharInfo(size_t _charCount = 0)
:_charCount(_charCount)
{}
CharInfo operator+(const CharInfo& info)
{
return CharInfo(_charCount + info._charCount);
}
bool operator>(const CharInfo& info)
{
return (_charCount > info._charCount);
}
bool operator!=(const CharInfo& info)const
{
return (_charCount != info._charCount);
}
bool operator==(const CharInfo& info) const
{
return (_charCount == info._charCount);
}
unsigned char _ch; //字符
long long _charCount; //字符出现的次数
string _strCode; //字符的对应编码
};
//将哈弗曼树的节点包装为一个结构体
template<class W>
struct HuffmanTreeNode{
HuffmanTreeNode(const W& weight)
: _parenet(nullptr)
,_lchild(nullptr)
,_rchild(nullptr)
,_weight(weight)
{}
HuffmanTreeNode* _parenet;
HuffmanTreeNode* _lchild;
HuffmanTreeNode* _rchild;
W _weight;
};
一、压缩
1.获取原文件中每个字符出现的次数
首先我们需要统计一下文档中每个字符出现的次数,由于是测试阶段,我们可以将文档的内容编辑的较为简单,便于我们进行调试观察,在这里我用来测试的文档内容为:
“ABBBCCCCCDDDDD”
因为字符的编码有限,所以我采取的哈希表的方式来进行统计,大小为256个元素的数组,当每次读到一个字符时,将该字符的ASCII码作为下标,对应元素值(数组中的每一个元素均为一个 CharInfo 的结构体)的次数进行加一,至于为什么要将字符类型设置为无符号类型,因为其中涉及到字符编码的问题。。。这个我们就不深究了,总之用到无符号类型的时候,即使是汉字也会被压缩进去,不然的话,程序在压缩文本文档时就会崩溃。
2.以每个字符出现的次数作为权值构建哈夫曼树
在我们得到每个字符的出现次数之后,我们可以以该次数为权值创建哈夫曼树,所以每个叶子结点上的权值即为字符在文章中出现的次数,规定向左子树方向为0,右子树方向为1,这样一来就为后续第三步获取字符编码作基础。
3.根据哈弗曼树获取每个字符的编码
因为第二步已经将哈弗曼树建好,所以这一步的作用就是将每个叶子节点的编码放入到对应叶子结点的编码值中去,为了方便起见,我将每一个树的节点包装成一个结构体,通过它的叶子结点向上方查找,知道找到根节点为止,不过需要注意的是,由于子节点向上查找到过程中,我们所得到的编码是完全相反的,所以需要调用一下string类中的reverse方法将字符串逆置,然后存储在对应 charInfo数组中。
4.根据每个字符的编码重新改写原文件
在将所有信息记录完成之后,我们就要开始压缩改写原文件了,首先我们需要注意的就是,要先将原文件的后缀名读取出来,存在改写后的文件的第一行,还有要解决的问题就是,为了能够解压缩,我们必须将哈夫曼树保存下来便于解压时查询编码,从而得到对应的字符,但是保存一颗哈弗曼树显然是不可能的,所以我将哈弗曼树的叶子结点信息保存下来,等到解压的时候,可以通过这些信息重新开始建树,从而通过编码得到对应字符,但是问题又来了,我们如何确定当先改写文件中的信息是节点信息而不是对应的编码信息呢?在这里,我是这样解决的,在写完文件后缀之后,再换行输入一个数字n,这个数字n代表的意义就是从当前行开始,往下n行就是对应的叶子结点的信息。这样我们就可以分辨叶子结点信息与编码信息了。
在输入完叶子结点信息之后,我们就要将对应的编码信息进行写入,当原文件字符所匹配到的编码达到8位时,也就是一个字节,就可以进行写入了,那么当文件的结尾不够八位时,我们可以加一个计数器,然后将最后一个编码左移,补全一个字节再进行写入。
二、解压缩
首先,解压缩较为容易,它其实是压缩的一个逆过程,但是里面不乏有很多
“坑”,在最后我会分享一些我在编码时遇到的一些“大坑”,希望读者以后不要再犯类似的错误。
1. 从压缩文件中获取源文件的后缀
首先将文件指针移到压缩文件开始位置,进行读取一行的数据,将其存在一个字符串中,这就是解压后的文件后缀名
2. 从压缩文件中获取字符次数的总行数 、获取每个字符出现的次数
读取第二行的数据 n,n 就是从这行开始往下 n行,都为压缩的字符信息。
注意!!!
在这里需要强调的是,在压缩的过程中,如果遇到了多个空行,他不会将该换行作为一个字符存储到文件中,就会导致压缩文件中出现空行的问题,在这个地方需要特别注意!当文件指针读取到空行的时候,需要自己手动添加一个换行符,再往下一行读取换行符的个数。
4. 重建huffman树, 解压压缩数据
根据保存下来的权值构建新的huffman树,进而根据压缩文件最后一部分的编码信息,从根节点向下找寻需要写入解压缩文件中的字符。这一步需要特别留心的就是,在文件指针指向最后一个字节信息时,有可能因为压缩过程中编码信息由于不足一个字节而向高位偏移,导致书写出来的内容与原文不匹配,这时候就需要用到我们的文件总大小,也就是重构树的根节点的权值,计算出压缩时最后一个字节的偏移量,从而得到正确的编码信息。
以下附上源代码:
https://github.com/colourlife/1998/tree/master/FileCompress