文件压缩项目的源代码(完整版):
文件压缩项目: 基于哈夫曼编码的文件压缩源码分享 (gitee.com)
目录
一、什么是文件压缩
文件压缩是指在不丢失有用信息的前提下,缩减数据量以减少存储空间,提高其传输、存储和处理效率,或按照一定的算法对文件中数据进行重新组织,减少数据的冗余和存储的空间的一种技术方法。
二、为什么要压缩文件
- 紧缩数据存储容量,减少存储空间
- 可以提高数据传输的速度,减少带宽占用量,提高通讯效率
- 对数据的一种加密保护,增强数据在传输过程中的安全性
三、压缩的分类
1、有损压缩
有损压缩是利用了人类对图像或声波中的某些频率成分不敏感的特性,允许压缩过程中损失一定的信息;虽然不能完全恢复原始数据,但是所损失的部分对理解原始图像的影响缩小,却换来了大得多的压缩比,即指使用压缩后的数据进行重构,重构后的数据与原来的数据有所不同,但不影响人对原始资料表达的信息造成误解
2、无损压缩
对文件中数据按照特定的编码格式进行重新组织,压缩后的压缩文件可以被还原成与源文件完全相同的格式,不会影响文件内容,对于数码图像而言,不会使图像细节有任何损失。
四、怎么实现文件压缩
而对于文件的压缩,常用的方式有:
1、专有名词采用的固定短语
2、缩短文件中重复的数据
3、给文件中每个字节找一个更短的编码
其中,给文件中每个字节找一个更短的编码这种方式较为常用。因为,文件中的数据在磁盘中都是以字节的方式来进行存储的,一个字节占用8个比特位,如果能够给一个字节找到更短的编码,即少于8个比特位,就可以起到压缩的目的,编码一般分为:静态等长编码和动态不等长编码。
比如:ABBBBCCCCCDDDDDDD字符串。
1、静态等长编码
文件中共有4个不同种类的字符,因为每个字符可以用两个二进制的比特位表示。
字符 | 编码 |
A | 00 |
B | 01 |
C | 10 |
D | 11 |
用等长编码对上述源数据进行压缩:01101110 11110111 11100011 10011110,压缩完成后的结果只占4个字节,压缩率还是比较高的。
该种压缩方式一般要求文件中字符种类比较少,但是一般情况下文件中字节的种类是比较多的。
2、动态不等长编码
根据文件中字节的分布情况获取每个字节的编码。
字符 | 编码 |
A | 100 |
B | 101 |
C | 11 |
D | 0 |
使用不等长编码对源数据进行压缩:10111011 00101001 11000111 01011
压缩完成后最后一个字节没有用完,还剩余3个比特位,对于该文件中内容,动态不等长编码方式比等长编码方式的压缩率能好点。
上述动态不等长编码有一种方式可以简单获取到,huffman树。
五、创建Huffman树
1.哈夫曼编码的方式
前面介绍用不等长编码的方式来对文件进行改写,但这些编码是怎么来的呢?为什么A:100 ,B:101, C:11, D:0。这种编码方式是通过哈夫曼树来进行编排的:
2.构建一棵哈夫曼树
知道了哈夫曼编码的方式之后,到底该如何构建一棵哈夫曼树呢?
从二叉树的根结点到二叉树中所有叶结点的路径长度与相应权值的乘积之和为该二叉树的带权路径长度WPL,将带权路径最小的二叉树称为Huffman树。
下图展示了哈夫曼树的构建方法:
二叉树的构建代码如下:
// 二叉树节点
template <class T>
struct HuffmanTreeNode
{
HuffmanTreeNode<T>* _left;
HuffmanTreeNode<T>* _right;
HuffmanTreeNode<T>* _parent;
T _weight;
HuffmanTreeNode(const T& weight = T())
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_weight(weight)
{}
};
template<typename W>
class HuffmanTree
{
typedef HuffmanTreeNode<W> Node;
public:
class Compare
{
public:
// 优先级队列中元素大小按降序排列
bool operator()(const Node* left, const Node* right)
{
return left->_weight > right->_weight;
}
};
HuffmanTree()
:_root(nullptr)
{}
HuffmanTree(const vector<W>& vw, const W& invaild)
{
// 使用节点权值构建二叉树节点(只有根节点的二叉树森林)
// 使用堆(优先级队列)来储存这些二叉树森林
std::priority_queue<Node*, vector<Node*>, Compare> q;
for (auto e : vw)
{
// 当字符出现次数不为0时,插入队列
if (e != invaild)
{
q.push(new Node(e));
}
}
while (q.size() > 1)
{
// 取出两个队列顶部元素作为左右两子树
Node* left = q.top();
q.pop();
Node* right = q.top();
q.pop();
// 将left和right作为新节点的左右字数
Node* parent = new Node(left->_weight + right->_weight);
// 链接父节点和左右子节点
parent->_left = left;
left->_parent = parent;
parent->_right = right;
right->_parent = parent;
// 将新节点重新放入二叉树森林中
q.push(parent);
}
_root = q.top();
}
~HuffmanTree()
{
Destroy(_root);
}
private:
void Destroy(Node* root)
{
if (root)
{
Destroy(root->_left);
Destroy(root->_right);
delete root;
root = nullptr;
}
}
private:
Node* _root;
};
六、文件压缩
压缩过程需要要经过四个步骤:
1、统计字符出现的次数
对于获取文件中每个字节出现的次数,采用一个含有256个结构体类型的数组来保存较为方便。因为文件在磁盘中都是以字节的方式来进行存储的,可以用每个字符对应的ASCII码值来作为数组的下标,只需遍历文件中的每一个字节,遇到对应ASCII码的元素对结构体中的count进行++即可。如下图,数组的下标与字符的ASCII码值一一对应:
2、利用统计的次数作为权值来构建Huffman树
在构建哈夫曼树时,直接调用之前写过的创建树的方式即可,但需要注意一点:
哈夫曼树每个节点类型为结构体类型,因此在定义结构体的地方,需要对运算符(+,>,!=,==)进行重载,让编译器知道count之间的比较方式。
3、获取哈夫曼编码
我们现在已经有了根据频次信息创建的Huffman树,现在只需要遍历Huffman树来获取编码即可。对于编码的获取方式,可以从根节点遍历到叶子节点来获取,也可以从叶子节点向上找到根节点获取。Huffman树节点的表示方法为孩子双亲表示法。通过这种方式获取的编码最后需要进行逆置才是最终结果,下图是获取编码后的结果,与期望的编码相同。
4、写压缩文件头部信息(用来解压的信息)
压缩文件中必须含有用来解压文件的信息,主要包含以下几点:
- 原文件后缀名
- 字节频次信息的总行数(方便对数据进行操作)
- 原文件字节出现的频次信息
- 压缩数据
5、用获取到的编码对源文件进行改写
前三个步骤进行完之后,只需改写文件即可。还是需要循环读取文件内容,采取位操作按位或的方式将Huffman编码放置到比特位当中。
步骤:1.定义bits = 0,先让bits左移一位,再判断Huffman编码的每个比特位是否为1,为1则让ch与Huffman编码为1比特位进行或操作,这样就能将文件按照编码的方式改写。
七、文件解压缩
1、读取解压缩所用到的信息
在解压缩过程中用到的信息:
- 获取原文件后缀名
- 获取频次信息的总行数
- 获取原文件字节出现的频次信息
- 获取压缩数据
2、还原huffman树
有了上面获取的信息,就可以对哈夫曼树进行恢复,继续进行解压缩的过程了,恢复哈夫曼树的过程相对简单,基于之前写过的构建哈夫曼树的方法,只需对文件中压缩信息做相对应的处理。
3、解压缩文件
解压缩过程就是:根据压缩数据来遍历恢复的哈夫曼树,压缩数据为1就往右子树走,为0就往左子树走,知道走到叶子节点。通过以上所有步骤就能够完成压缩与解压缩的所有过程了。
这里还有一个小问题,压缩方法不能处理文件中的汉字,因为汉字是采用UTF-8编码进行编排的,它的编码不在ASCII码表中。因此需要将所有的char类型改变为unsigned char类型!!!
关于具体解压缩的代码(最终代码),请参考:
文件压缩项目: 基于哈夫曼编码的文件压缩源码分享 (gitee.com)https://gitee.com/Bai-Yun-Shuai/file-compression
总结
以下是我总结完成过程中注意的一些问题:
1、创建哈夫曼树时,使用优先级队列来保存字符出现的频次信息时,优先级队列的比较方式需要自己进行给出,使用仿函数的方法让比较方式为每个节点中的权值。
2、在使用结构体来创建哈夫曼树时,需要对+,>,==,!=,进行重载,重载的目的:让编译器知道权值中的count之间的比较方式
3、在构建哈夫曼树时,发现数组中还存放着许多字符出现次数为0的节点,需要进行过滤,设置第二个参数invalid来过滤权值为0的节点。
4、再次读取文件时,需要将文件指针的位置重置到文件头部,使用fseek函数或者rewind函数。
5、在解压缩时,不一定是8个比特位都需要解压缩,因此需要增加判定条件,只要解压缩后的字节数与原文件相等就停止解压缩。
6、压缩和解压缩代码写完后,需要讲char类型改成unsigned char类型,因为原文件可能出现汉字,汉字对应的是UTF-8编码,不在ASCII码表中。
7、在测试多行数据压缩时,又出现了问题,原因是当读取到\n换行时,并没有将换行所在的数据GetLine,因此解压缩时数据少了一行。
8、在进行大量数据测试时出现问题,发现解压缩一部分内容就停止了,部分数据丢失,但已经解压缩的部分内容与原文件一样。原因是解压缩文件是二进制文件(即压缩结果是二进制数字),二进制文件中可能会出现-1(FF),当为-1时就默认读到了文件末尾就结束解压缩过程了。
改进方法是:
将所有打开文件(fopen)中文件的读写方式由"r",“w"改为二进制读写式"rb”“wb”
9、经过测试,发现对不同的文件(文本文件,二进制文件),图片,视频都能够进行压缩,但哈夫曼编码的文件压缩对不同种类的压缩率是不同的。
对于txt/c/cpp类后缀的文本文件,压缩率较高在30%~50%
对于png/jpg类后缀的图片,压缩率一般在70%~90%
对于exe类后缀的可执行文件,压缩率较低在80%~90%
10、在对文件进行多次压缩时,压缩结果的大小不是每次会减少,有一个限度。
同时,哈夫曼编码的压缩方法也是可能会出现压缩结果变大的可能性。当文件中字节的种类偏多,并且字节出现的次数比较平均的情况下,压缩效率就会变差,因为统计字节出现的频次信息时,各个字节出现的次数差不多,构建出来的哈夫曼树就接近平衡二叉树,效果就会不理想。当然,也会出现压缩后文件变大的情况:当哈夫曼编码的平均长度大于8字节时,压缩文件就会变大。