文件压缩项目(模拟实现zip压缩算法)
什么是文件压缩
- 文件压缩是指在不丢失有用信息的前提下,缩减数据量以减少存储空间,提高其传输、存储和处理效率,或按照一定的算法对文件中数据进行重新组织,减少数据的冗余和存储的空间的一种技术方法
- 其实说白了,压缩的本质就是占用更少的存储空间,要能对压缩的结果进行还原,如果不能将压缩的结果进行还原的话,那么这种压缩其实是毫无意义的
为什么要需要压缩
- 紧缩数据存储容量,减少存储空间
- 可以提高数据传输的速度,减少带宽占用量,提高通讯效率
- 对数据的一种加密保护,增强数据在传输过程中的安全性
压缩的分类
- 有损压缩----有损压缩是利用了人类对图像或声波中的某些频率成分不敏感的特性,允许压缩过程中损失一定的信息;虽然不能完全恢复原始数据,但是所损失的部分对理解原始图像的影响缩小,却换来了大得多的压缩比,即指使用压缩后的数据进行重构,重构后的数据与原来的数据有所不同,但不影响人对原始资料表达的信息造成误解。(有损压缩一般的使用场景用于图片或者说是视频的压缩)
- 无损压缩----对文件中数据按照特定的编码格式进行重新组织,压缩后的压缩文件可以被还原成与源文件完全相同的格式,不会影响文件内容,对于数码图像而言,不会使图像细节有任何损失。
- 有损和无损的区别主要在于能不能将压缩之后的文件完全的还原回去
- 其实看一个东西到底是有损压缩还是无损压缩其实就是去看压缩完成的问价能不能再完完全全的还原回去,如果是可以还原回去的话,那么这个压缩其实就是无损压缩,否则就是有损压缩
压缩本质
- 压缩的目的是让文件变小,减少文件所占的存储空间。那怎么才能让文件变小呢?方式比较多,比如:
- (1)专有名词采用的固定短语,比如将西安电子科技大学压缩成西电
- (2)缩短文件中重复的数据—比如文件中存放数据为:mnoabczxyuvwabc123456abczxydefgh对文件中重复数据使用(距离,长度)对进行替换,压缩之后的结果为:mnoabczxyuvw(9,3)123456(18, 6)defgh(也就是说其实就是想办法把重复的东西去掉),但是使用这种方式需要注意的点在于:用来替换的长度举例对必须要比替换的内容要短(LZ77压缩算法的原理)
- (3)比如说现在的文件内容是:ABBBCCCCCDDDDDDD,那么我们如何对他进行压缩的操作,我们可以发现,在内容中出现了4种不同种类的字符:我们只需要给每个字符重新找一个更短的比特位编码就可以了,那么其实就相当于是有4种状态,4种状态的话,我们其实只需要用2个比特位就可以搞定了,我们让00代表A,01代表B,10代表C,11代表的就是D了,那么我们就可以把内容表示成000010101101010101011111111111111,这些数字一共占有4个字节,就比之前的一个字符占有8个比特位,存储起来节省空间的多了
- 但是想我上面说的那种查找编码的方式其实是不很优秀的,其实还有更优秀的查找编码的方式,那么就是huffman树来对编码进行查找的操作
GZIP压缩原理简介
- 利用变形的LZ77压缩算法,先快速的从重复语句层面进行快速压缩
- 利用huffman编码方式再从字节上面进行压缩
- 将两种压缩算法结合起来,以达到高效快速的压缩
GZIP之huffman编码的文件压缩
-
假设我当前的文件内容是ABBBCCCCCDDDDDDDDD,那么既然要采用huffman树的方式来对对内容进行压缩的话,那么我就需要为每一个字节找到一个更短的编码来进行表示,比如说,我们将00给A,将01给B,将10给C,将11给D,然后我们就用所找到的编码对源文件重新进行改写的操作,因为每一个编码的长度都是相同的,那么我们就将这种类型的编码称为固定长度的编码
-
现在我们对上面的文件内容来进行改写的操作,改写之后的结果为:000101011010101010111111111111111111,我们在没有进行改写之前,文件内容所占的字节数是1+3+5+9个也就是18个字节,那么,我们在进行了压缩之后,我们文件内容变成了5个字节,所以节省了13个字节的内存空间,效果其实还是比较好的,但是也不是最优秀的方式,我们上面所给出的方式是固定长度的编码,那么同样的道理,我们也可以给出不定长编码,然后也可以去重新测试一下效果,比如我们把A给成111,B给成110,C给成10,D给成0,那么压缩之后的结果就变成了11111011011010101010100000000000,那么就只占了4个字节,所占用的空间会更少一些
-
那么变长编码所给出的原则到底是什么呢?—原则其实就是将出现次数比较多的字符的编码设置的短一些,出现次数比较少的字符编码设置的更长一些
-
那么问题又来了,边长编码是如何来进行获取的呢?—常用的获取变长编码的方式其实就是采用Huffman树方式来进行获取
-
GZIP的第一步是使用LZ77快速的对文件中的重复的短语进行压缩,压缩完成之后,就形成了一个包含原字符、距离、长度的压缩文件,该文件中重复的语句基本已经被全部消除掉,但是各个字节上还有一定程度的重复,因此GZIP的第二步就是从字节方面来进行压缩的。
-
一个字节占用8个比特位,如果能够给一个字节找到更短的编码,即少于8个比特位,就可以起到压缩的目的,编码一般分为:静态等长编码和动态不等长编码。
-
用等长编码对上述源数据进行压缩:01101110 11110111 11100011 10011110,压缩完成后的结果只占4个字节,压缩率还是比较高的。
-
该种压缩方式一般要求文件中字符种类比较少,但是一般情况下文件中字节的种类是比较多的
-
压缩完成后最后一个字节没有用完,还剩余3个比特位,对于该文件中内容,动态不等长编码方式比等长编码方式的压缩率能好点。
-
上述动态不等长编码有一种方式可以简单获取到,huffman树。
huffman树
- 从二叉树的根结点到二叉树中所有叶结点的路径长度与相应权值的乘积之和为该二叉树的带权路径长度WPL。
将带权路径最小的二叉树称为Huffman树
- WPLa = 1 * 2 + 3 * 2 + 5 * 2 + 7 * 2 = 32
- WPLc = 7 * 3 + 5 * 3 + 3 * 2 + 1 * 1 = 43
- WPLd = 1 * 3 + 3 * 3 + 5 * 2 + 7 * 1 = 29
如何创建Huffman树
- 因为huffman树是带权路径最小的二叉树,那么如何使得带权路径最小呢,就是让权值大的根节点尽量靠近根结点,让权值小的结点尽量远离根节点,这样子,带权路径才会是最短的
- 那么我们首先可以创建以权值为根节点的二叉树森林
- 由给定的n个权值{ w1, w2, w3, … , wn}构造n棵只有根节点的二叉树森林F={T1, T2 , T3, … ,Tn},每棵二叉树Ti只有一个带权值wi的根节点,左右孩子均为空。重复以下步骤,直到F中只剩下一棵树为止
- 在F中选取两棵根节点权值最小的二叉树,作为左右子树构造一棵新的二叉树,新二叉树根节点的权
值为其左右子树根节点的权值之和,在F中删除这两棵二叉树,把新的二叉树加入到F中
- 最后将这两棵树进行合并,就得到我们最终所需要的Huffman树了
- 我们如何快速拿到权值最小的二叉树呢?----我们可以使用堆这种数据结构,我们可以使用小堆来维护二叉树这个森林
创建huffman树的代码
HuffmanTree.hpp
#pragma once
#include<vector>
#include<queue>
template<class W> //这个地方的W就表示的是结点中的权值
struct HuffmanTreeNode
{
//同时我们可以给出构造函数
HuffmanTreeNode(const W& weight = W()) //把权值给出来,如果权值没有提供的话
//那我就给你一个默认的权值
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_weight(weight)
{
//这个时候节点就有了,那么继续向下看
}
//W表示的是节点中的权值
HuffmanTreeNode<W>* _left; //树的左子树
HuffmanTreeNode<W>* _right; //树的右子树
HuffmanTreeNode<W>* _parent; //树的双亲
W _weight;
};
template<class W>
class HuffmanTree
{
typedef HuffmanTreeNode<W> Node;
template<class W>
struct Compare
{
//给出这种比较的类
bool operator()(const Node* left, const Node* right)
{
//我们给出按照大于的方式来进行比较的操作
//我们比较的是结点里面的权值
return left -> _weight > right->_weight;
}
};
public:
HuffmanTree()
:_root(nullptr)
{
//构造的时候我们首先把这棵树构造成一颗空的树
}
//因为树种的结点都是我们动态申请的,所以在用完成了之后,记得进行空间的释放
~HuffmanTree()
{
DestroyTree(_root);
}
//那么,我们闲杂就需要用到权值去构建一颗Huffman树
//我们最终是需要给出返回值的我们返回堆顶的元素就可以,所以我们选择返回void *
Node *CreateHuffmanTree(const std::vector<W>& weights,const W& invalid)
//invalid表示的就是遍历到的这个东西其实是一个非法的权值
//就不用对其进行入队的操作了
//我们需要把权值给进去才能去创建那颗哈夫曼树
{
//然后现在的第一步其实就是去构造二叉树的森林
//以每个权值为根结点构造二叉树森林
//用堆这种数据结构去创建
//std::priority_queue<Node*> q;
//但是优先级队列默认是大堆,但是我们需要的是小堆
//那么我们既然要的是小堆,我们只需要将比较的方式进行一下更改其实就可以了
//那么我们就对优先级队列去进行更改的操作
//将Compare()进行传入
std::priority_queue<Node*,std::vector<Node*>,Compare<W>> q;
//在入队列的时候,我们所需要注意的一点就是我们需要过滤掉非法的weights
for (auto e : weights)
{
//以权值为值去创建根节点
if (e == invalid)
//非法的东西就不用进行入对的操作了
continue;
q.push(new Node(e));
}
while (q.size() > 1)
{
//先去找权值最小的点,然后把他拿出来
Node* left = q.top();
q.pop();
Node* right = q.top();
q.pop();
//然后现在去创建一个新的根节点
Node* parent = new Node(left->_weight + right->_weight);
parent->_left = left;
parent->_right = right;
//左子树和右子树的双亲结点都是parent
left->_parent = parent;
right->_parent = parent;
q.push(parent);
}
//当堆中只剩一个结点的时候,就可以返回了
//就是我需要用到的哈夫曼树了
_root = q.top();
return _root;
}
Node* GetRoot()
{
return _root;
}
private:
void DestroyTree(Node* & root)
{
if (root)
{
DestroyTree(root->_left);
DestroyTree(root->_right);
delete root;
root = nullptr;
}
}
private:
Node* _root; //用来标记一棵树的根节点
};
BitZipTest.cpp
#include"HTCompress.h"
int main()
{
HTCompress htc;
//htc.CompressFile("1.txt");
htc.UnCompressFile("compressResult.txt");
return 0;
}
- 当我们把树创建完成之后我们就可以来进行压缩的操作了
- 当我们把huffman树创建出来之后,将树左边的边全部置为0,将树右边的边全部置为1
基于Huffman树的压缩
HTCompress.h
#pragma once
#include<string>
#include<vector>
#include"HuffmanTree.hpp"
using namespace std;
typedef unsigned long long ulg; //因为字符出现的次数可能很多
//而且我也确定他到底出现多少次,所以我给成ulg类型
typedef unsigned char uch;
struct CharInfo
{
//char _ch; //我们要去统计ch这个字符一共出现了多少次
//一开始像上面这样定义出现的次数的话,其实是会崩溃的,因为char所能表示的类型
//范围为:-128~+127,所以在有些情况下像上面这个样子其实是会发生崩溃的问题的
uch _ch;
ulg _appearCount; //代表出现的次数
std::string _strCode;//每一个字符最后还会有他自己的编码
//添加响应的构造函数
CharInfo(ulg appCount=0)
:_appearCount(appCount)
{
}
CharInfo operator+(const CharInfo& c)
{
//我们要的是权值进行相加,所以我们对出现的次数进行重载的操作
return CharInfo(_appearCount + c._appearCount);
}
//用权值来进行比较的操作
bool operator>(const CharInfo& c)const
{
return _appearCount > c._appearCount;
}
bool operator==(const CharInfo& c)const
{
//用出现的次数来比较两个东西是不是相同的就可以了
return _appearCount == c._appearCount;
}
bool operator!=(const CharInfo& c)const
{
//用出现的次数来比较两个东西是不是相同的就可以了
return _appearCount != c._appearCount;
}
};
class HTCompress
{
public:
HTCompress();
//压缩一个文件和解压缩一个文件的时候,我都需要把文件的路径给他传进来
void CompressFile(const std::string& filePath);
void UnCompressFile(const std::string& filePath);
//写入头部信息
void WriteHeadInfo(FILE* fOut,const std::string& filePath);
private:
//我们需要根据huffman树去生成最终的编码
void GeneteCode(HuffmanTreeNode<CharInfo>* root); //把结点的值传入
//我们自己给出一个读取每一行文件内容的函数其实就可以了
//从fIn中进行读取的操作,把读取到的内容放在第二个参数里面就可以了
void GetLine(FILE* fIn, std::string& s);
private:
//数据在文件中都是以字节的方式来进行保存的
//一个字节占有的比特位是8个比特位,可以表示256个字符
//所以我们vector的空间给成256就可以了
//将字符的信息保存到结构体里面就可以了
std::vector<CharInfo> _charInfo;
};
HTCompress.cpp
#include"HTCompress.h"
#include<iostream>
using namespace std;
HTCompress::HTCompress()
{
//给出构造函数
_charInfo.resize(256);
for (size_t i = 0; i < 256; ++i)
{
_charInfo[i]._ch = i;
//一个字符到底出现了多少次,我现在是不知道的的
//我只知道第i个字符他就是第i个字符
//把出现的次数我们一开始给成0
_charInfo[i]._appearCount = 0;
}
}
void HTCompress::CompressFile(const std::string& filePath)
{
//然后就要去统计文件中每个字符出现的次数
//那么我既然要去统计这文件中所需要字符的个数,首先我就需要把这个文件打开才可以
FILE* fIn = fopen(filePath.c_str(), "rb"); //但是fopen只接受C语言格式的字符串
//那也没关系我们使用c_str就会把其转化成为C语言格式的字符串了
//同时我们以只读的方式将这个文件进行打开的操作
if (nullptr == fIn)
{
//说明打开文件失败了
cout << "待压缩文件路径出错" << endl;
return;
}
//打开成功的话,就去统计文件中每个字符出现的次数,然后还需要保存起来
//那么,我既然要去统计文件中每个字符出现的次数的话
//我就需要去读文件
uch readBuff[1024];
while (true)
{
//因为我并不知道文件有多大,所以我不知道我对这个文件需要进行几次的读操作
//索性我直接给成循环去进行读了
size_t rdsize = fread(readBuff, 1, 1024, fIn);
//fread的返回值是成功读取到的元素的个数
if (rdsize == 0)
//说明我这次一个字节都没有读取到
//也说明我已经读取到文件的末尾了
break;
//如果不是0,那么说明文件中其实还是有内容可以读出来的
for (size_t i = 0; i < rdsize; ++i)
{
//那么,如何进行统计呢?我们就以这个字符的ASCII码作为下标来对其进行加一的操作
//以字符的ASCII码作为下标来进行统计的操作
_charInfo[readBuff[i]]._appearCount++;
}
}
//2.然后以ChInfo作为权值去创建huffman树
HuffmanTree<CharInfo> ht;
ht.CreateHuffmanTree(_charInfo, CharInfo(0));
//出现次数为0次的字符,我们就将其视为非法的字符
//不对其进行入队的操作
//3.然后再获取每个字符所对应的哈夫曼编码
GeneteCode(ht.GetRoot());
//获取到了编码之后
//4.我们需要用获取到的编码队源文件中的每个字符进行重新的改写
//然后这个时候我们需要再次去读取一遍文件
//但是我们在之前遍历文件的内容的时候,我们已经遍了了一次文件了,那我们既然已经
//遍历了一次文件了,那么我们的文件指针就已经处于文件的末尾了
//所以我们在进行第二次遍历的时候,我们需要对文件指针进行重新赋值的操作
//将文件指针恢复到起始得位置
rewind(fIn);
//给出压缩文件
FILE* fOut = fopen("compressResult.txt", "wb"); //这个文件是用来写压缩结果的
WriteHeadInfo(fOut,filePath); //文件的路径也需要传入
uch chData = 0; //代表的是字节,一个字节最多只能放置8个比特位
uch bitCount = 0;
while (true)
{
size_t rdSize = fread(readBuff, 1, 1024, fIn);
//fread返回的实际读到的字符的个数
if (rdSize == 0) //如果是0的话,说明我们已经处理完了
break;
for (size_t i = 0; i < rdSize; ++i)
{
//然后我们现在要去找到结点对应的编码
string& strCode = _charInfo[readBuff[i]]._strCode;
//将该编码的每个二进制的比特位放置到一个字节里面
for (size_t j = 0; j < strCode.size(); ++j)
{
//左移一位就是给他的右边补了一个0
chData <<= 1;
//编码的比特位可能是0也可能是1
if (strCode[j] == '1')
{
chData |= 1; //这一步的操作其实就相当于是把第i个比特位放到了chData里面
}
//如果是0的话,那么只需要进行左移的操作,就不用进行或操作
//每次放进去一个字节,就要把count++
bitCount++;
if (8 == bitCount)
{
//当8个比特位放满的时候,就说明第一个字节已经填充好了
//然后我们将该字节写入到压缩文件那就可以了
fputc(chData, fOut);
//既然我已经完成写入了,我把count给成0就ok了
bitCount = 0;
chData = 0;
//那既然我要写入到压缩文件,那么我就需要把压缩文件给出来
}
}
}
}
//出了循环之后,如果chData中有效比特位不够8个的时候,是没有写入到压缩文件当中的
if (bitCount > 0 && bitCount < 8) //那么就说明最后的那个字节其实不够8个比特位的
{
//其实在这种情况也是很好处理的,我们只需要对其进行一次单独的处理就可以了
chData <<= (8 - bitCount);
fputc(chData, fOut);
//但是特殊处理的时候还需要注意一个情况,就是说我们在进行解压缩的时候
//我们是从左往右依次进行解压缩的操作的
//最后一个字节只有低7个比特位对我们来说是有效的
//那既然高位的哪一位是无效的,那么我们就把最后一个字节向左移动一位
//移动完成之后,最后的一个比特位不去进行解压缩就可以了
}
//最后的时候,需要把文件关掉
fclose(fIn);
fclose(fOut);
}
//先走到叶子节点得位置,然后顺着叶子结点的位置向上进行编码的获取
void HTCompress::GeneteCode(HuffmanTreeNode<CharInfo>* root)
{
if (root == nullptr)
return;
//通过递归的方式让代码不断地向下走
GeneteCode(root->_left);
GeneteCode(root->_right);
if (root->_left == nullptr && root->_right == nullptr)
{
//就说明走到了叶子节点了
HuffmanTreeNode<CharInfo>* cur = root;
//同时还要去找到这个节点的双亲结点
HuffmanTreeNode<CharInfo>* parent = cur->_parent;
//用这个来保存我们的编码
string& strCode = _charInfo[cur->_weight._ch]._strCode;
while (parent)
{
if (cur == parent->_left)
strCode += '0';
else
strCode += '1';
cur = parent;
parent = cur->_parent;
}
reverse(strCode.begin(), strCode.end());
}
}
//调用的实际在我们写入压缩数据之间,调用这个方法
void HTCompress::WriteHeadInfo(FILE* fOut, const string& filePath)
//把原文件的路径传进去是为了得到原文件的后缀的名称
{
//文件中所需要包含的信息
//1.原文件的后缀
string filePostFix = filePath.substr(filePath.find('.')); //这一行是为了得到原文件后缀的名称
//得到后缀名之后,我们需要把后缀名写入到文件当中
filePostFix += '\n'; //一次写一行的内容
//2.字节,出现次数的总行数
size_t szCount = 0; //因为我们一开始也不知道他到底出现了多少次
//szCount表示行的次数
//所以我在一开始的时候把出现的次数给成1
//3.字节,出现次数的信息---每条信息占有一行的内容
string chAppear;
for (size_t i = 0; i < 256; ++i)
{
if (0 != _charInfo[i]._appearCount)
{
chAppear += _charInfo[i]._ch;
chAppear += ","; //逗号前面是字符,逗号后面是出现的次数
chAppear += to_string(_charInfo[i]._appearCount); //之所以使用to_stirng
//是因为两个相加的变量的类型是不一样的,所以需要进行强制类型转化的操作
//然后又因为每个内容需要独立的占有一行,所以加上一个\n其实就是可以的了
chAppear += "\n";
szCount++;
}
}
//为什么每条内容需要占一行,因为每条内容占一行的话,我们在读取的时候就会方便一些
//我们只需要去读取那么多行其实就是可以的了
//(上面是在真正写压缩数据之前需要具备的信息)
//当这些压缩信息全部都有了的时候,我们只需要把这些内容进行一个写入的操作其实就是可以的了
//先去写文件的后缀名
fwrite(filePostFix.c_str(), 1, filePostFix.size(), fOut);
//然后写入行数
string strCount;
//把strCount转化成字符串的格式就可以了
strCount = to_string(szCount);
strCount += "\n";
fwrite(strCount.c_str(), 1, strCount.size(), fOut);
//最后写入每一个字符出现的次数
fwrite(chAppear.c_str(), 1, chAppear.size(), fOut);
}
void HTCompress::UnCompressFile(const std::string& filePath)
{
//现在要去进行解压缩的话,首先我们一进来就需要把这个文件给打开
FILE* fIn = fopen(filePath.c_str(), "rb");
if (fIn == nullptr)
{
//如果文件为空的话,那么就说明其实是有问题的
cout << "压缩文件的路径是有问题的" << endl;
return;
}
//当然,我们不仅要保存原文件的路径我还要保存原文件的名称
//否则的话,压缩出来的文件的名称其实可能就不是那么符合我们的想法了
//那我们这个时候还是把名字直接写固定,然后文件的后缀名从压缩文件中获取
string filename("2");
//有了文件的名称之后,我现在需要去获取文件的后缀名
string s;
GetLine(fIn, s);
filename += s;
//这里读取结束之后,我们就相当于是获取到了我们解压缩的文件
//解压缩文件的文件名称就是2.txt
//然后我现在要去获取字节次数总的行数
s = " "; //首先我需要将文件s进行清空的操作,然后从头进行读取的操作
GetLine(fIn, s);
size_t szCount = atoi(s.c_str());
//读取字节次数的每行信息
for (size_t i = 0; i < szCount; ++i)
{
s = "";
//注意,我们在这里需要对换行的符号来单独的进行处理
GetLine(fIn, s);
//如果你读取了一行之后,s里面的东西仍然是空的话,那么就说明
//我们读到的是一个换行,那么我们就把\n加进去
if ("" == s)
{
s += "\n";
//加上去之后,我再次去进行一次读的操作
GetLine(fIn, s);
}
//因为字符实在第一个位置,然后字符的后面还有一个逗号
//然后逗号之后才是字符出现的次数,所以需要+2才可以真正获取到字符出现的次数
_charInfo[(uch)s[0]]._appearCount = atoi(s.c_str() + 2);
}
//既然我们现在需要的关于huffman树的信息我们已经全部获取的到了,那么我们现在其实就只是
//需要去还原我们之前的那一棵huffman树就可以了
HuffmanTree<CharInfo> ht;
ht.CreateHuffmanTree(_charInfo, CharInfo(0)); //去除掉出现次数为0的字符
//出现次数为0的字符就不要存在在huffman树中了
//然后现在huffman树其实也有了
//那么现在我们就需要去进行解压缩的操作了
//当然我们在解压缩成功之后,我们同样需要把我们解压缩之后的结果
//写入到一个新的字符串当中去
FILE* fOut = fopen(filename.c_str(), "w"); //我们要以写的方式将这个文件进行打开的操作
uch readBuff[1024]; //我们将读取到的内容写入到这个空间当中
//给出huffman树
//让其从根节点的位置进行遍历的操作
HuffmanTreeNode<CharInfo>* cur = ht.GetRoot();
uch bitCount = 8; //一开始有8个比特位
uch chBit; //一开始有8个比特位
ulg fileSize = cur->_weight._appearCount; //把文件的大小拿过来
//这样我在进行解压的时候,我就知道我该去解压缩多少的大小了
while (true)
{
size_t rdSize = fread(readBuff, 1, 1024, fIn);
if (0 == rdSize)
break; //如果等于0的话,就说明我们已经读取到了文件的末尾了
//就不会再继续去进行读的操作了
//然后我们一个字节一个字节来进行解压缩的操作
//将rdBuff中每个字节逐比特位的来进行解析的操作
for (size_t i = 0; i < rdSize; ++i)
{
bitCount = 8;
chBit = readBuff[i];
//然后我们需要根据信息去遍历Huffman树,那么我们既然要去遍历huffman树的话
//那我们肯定是要给出Huffman树的
//将readbuff[i]中保存字节的八个比特为从高到低的来逐个进行检测
while (bitCount)
{
if (chBit & 0x80)
{
//这一步其实就是去检测一个字节中最高位的那个比特位到底是不是0
//如果是非0的数据的话,那么就说明这个比特的高位其实是1
cur = cur->_right;
}
else
{
cur = cur->_left;
}
if (cur->_left == nullptr && cur->_right == nullptr)
{
//就说明cur现在在叶子结点的位置
//那么就说明我们已经解压缩出来了一个字符了
//既然这个字符已经解压缩出来了,那么我们需要把这个字符
//写道我们的解压缩文件当中去
fputc(cur->_weight._ch,fOut); //像fOut中去写
//然后既然得到了一个字符之后我们就需要让cur回到根节点的位置
cur = ht.GetRoot();
//然后我们每次解压缩出来一个字节就对其进行--的操作
//这一小部分主要是为了处理最后一字节不够8个比特位的情况
fileSize--;
if (0 == fileSize)
break;
}
bitCount--;
//当8个比特位处理完成的时候我们就需要重新开始
//去读取一个字节
chBit <<= 1; //然后朝左边去移动一位
}
}
}
fclose(fIn);
fclose(fOut);
}
void HTCompress::GetLine(FILE* fIn, std::string& s)
{
uch ch;
//feof用来判断文件指针是不是在文件的末尾
while(!feof(fIn))
{
ch = fgetc(fIn); //fget是用来读取一个字符的
if ('\n' == ch)
break; //如果字符是换行符号的话,那么就说明我这一行的内容就已经读取完毕了
//那么我们就跳出循环
//如果读到的不是换行符号的话,那么我们只需要把他加到文件中就可以了
s += ch;
}
}
- 但是如果给出上面那种形式的huffman树,我们想要去获取编码的话,可能确实没有那么容易就可以获取到编码,所以我们对我们之前所给出的树来进行改写,我们使用孩子双亲表示法来对一棵树进行表示,那么一棵树,就会变成下面的这种结构,修改之后的这棵树,只要我能走到叶子结点的位置就可以获取到结点的出现的次数了,当我们走到叶子结点的位置的时候,我们就可以找到他的双亲结点,当我们知道他的双亲之后,我们去看这个结点是双亲的左孩子还是右孩子,如果是左孩子的话,就置为0,如果是右孩子的话,就置为1,然会利用同样的方式一直向上走
- 我们在创建Huffman树的时候需要注意的一点就是我们需要过滤掉没有出现的字符,也就是说没有出现的字符不应该出现在我们所创建的Huffman树中
- 现在有一个问题就是,我们能不能将C的编码改成10,原因是不行的,如果我们把C的编码给成10的话,那么我们再去进行解压缩的时候,可能就会出现一些问题,所以我们不能将C的编码改成10,也就是说我们不能让一个编码是另一个编码的前缀,如果是前缀的话,可能就会有一些问题
- 那么,我们在借助Huffman树去生成编码的时候时候会存在前缀编码呢?答案其实是不会的,因为这些权值其实都是处在叶子节点的位置上的,所以说不会有一个结点是另一个结点的前缀,因为只有一个结点在另一个结点的前缀路径中才会是的一个的前缀是另一个的前缀,所以借助Huffman树是不会出现这种情况的。
- 获取到了编码之后,我们就需要将文件中的内容按照二进制的方式重新组织起来,但是我们在进行组织的时候,最后一个字节可能是不足8个比特位的,那么如果是不足8个比特位的话,我们要怎么去进行处理呢?
解压缩的操作
- 我们在经过压缩之后,压缩的结果是保存在压缩文件当中的
- 那么,我们如何来进行解压缩的操作呢?
- 解压缩同样需要利用到哈夫曼树
- 首先,我们需要从压缩的文件中逐个获取每个比特位,然后获取到每个字节,当我们把所有的字节全部都获取到的时候,我们现在需要逐个去检查每个比特位,来遍历Huffman树,这个时候我们给出一个指针,将这个指针放在Huffman树根节点的位置,然后对字节进行从高位到低位的检测,如果这个字节中的比特是1的话,那么就让cur向右子树来进行移动,如果cur的值为0,那么就让cur往树的左子树的方向来进行移动,只要没有走到叶子结点的位置的话,我们就重复的进行上述的操作,直到走到叶子的结点的位置,我们就停止我们的操作,当检测到一个内容的时候,我们就结束这一次的操作,然后我们呢把cur重新放置在根节点的位置,然后重复的去进行上述的操作
- 解压缩的本质其实就是利用压缩的比特流的信息来对Huffman树进行遍历的,直到遍历到叶子节点,一个字节就算是压缩成功了
- 那么当我们一直对上述的步骤进行重复的操作的时候,那么我们如何知道解压缩到什么程度就不再去进行解压缩了呢?当我知道了原文件一共了多少个字节的时候,那么,我只需要在解压缩文件中解压缩同样数目的字节数我就可以停止解压缩的操作了,那么我如何去知道原文件中有多少个字节数呢?其实是很明显的,根节点中的权值其实就是源文件中字节的个数,当我把跟接待你的权值保存起来的时候,我就知道原文件中字节的数目了,从而我就可以知道解压缩的字节数了
- 当然,我们在进行解压缩的时候,如果只是知道原文件经过压缩之后的内容的话,其实我们呢是不能将文件再解压缩回去的,所以我们在压缩文件当中还必须要保存我们在压缩的时候所使用的Huffman树的信息—那么,Huffman树应该如何去保存呢?
- 我们可以通过结点和结点出现的个数来保存一棵Huffman树
解压缩
- 假设按照上述操作已经对源文件压缩完毕,怎么解压缩?解压缩之后文件的后缀怎么与源文件保持一致?因此:压缩文件中除了保存压缩数据外,还必须保存解压缩所需的信息。压缩文件格式:
- 那么我们的解压缩文件中就应该包含有如下的信息:
解压缩的过程
- 从压缩文件中获取源文件的后缀
- 从压缩文件中获取字符次数的总行数
- 获取每个字符出现的次数
- 重建huffman树
- 解压压缩数据—a. 从压缩文件中读取一个字节的获取压缩数据ch----->b. 从根节点开始,按照ch的8个比特位信息从高到低遍历huffman树:该比特位是0,取当前节点的左孩子,否则取右孩子,直到遍历到叶子节点位置,该字符就被解析成功,将解压出的字符写入文件,如果在遍历huffman过程中,8个比特位已经比较完毕还没有到达叶子节点,从a开始执行----->c. 重复以上过程,直到所有的数据解析完毕。
项目中可能遇到的问题
变长编码和定长编码
- 2个比特位就可以去表示四种状态,所以4种字符就可以用2个比特位来表示他们的状态
- 那儿么我们其实就是给每个字符重新找一个更短的比特位编码就可以了
- 但是现在有一个问题就是,对于普通类型的文件,我们是不知道它里面存储的是什么类型的数据的,那么我们如何获取每个字节对应的编码呢?这个问题之后再进行求解
- 但是给出定长编码的这种方式其实并不是最优秀的编码方式,那么我们其实可以给出一种更优秀的编码方式,其实就是采用huffman树进行编码的方式
代码的书写过程
- 首先给出huffman树的结点的结构,这里一开始我其实并没有考虑到孩子双亲法的表示,一开始只是考虑到了左子树右子树的孩子表示法
- 其实给出huuffman树的创建方式,一开始将这一颗树给成空树,然后去创建这颗huffman树,我们需要通过权值的大小去创建一颗huffman树,拿到一个权值就放入堆里面,然后我们去创建一个小堆
- 当堆种最终只剩一棵树的时候,这棵树就是我们所需要的huffman树
- 因为优先级队列他其实默认是一个大队的结构,但是我们其实需要的是小堆的结构,那么我们改如何进行操作,才能通过优先级队列的形式才能得到我们所需要的小堆的结构呢?
- 那么我们只需要将所给出的默认的比较的方式进行修改其实就可以的,大队使用的方式默认是小于的比较方式,我们只需要给成大于的比较方式其实就可以了
- 当我们把huffman树创建出来之后,现在就需要去获取每个结点所对应的权值了
- 这里注意,fopen只可以处理C语言格式的字符串
- 这里,我们将字符的信息保存在结构体里面,然后我们呢将vector的大小初始化成256个元素,那么为什么要初始化成为256个元素,原因在于数据在文件中是按字节保存的,字节一共就有256种,所以初始化成256就可以了**(但是这里可能存在有隐患)**
- 将ASCII码值作为下标来统计对应元素出现的次数
- 我们在获取结点编码的时候,需要对我们一开始所给出的树的结构进行调整的操作,我们需要给出还在的双亲界定啊,这样子的话,我们才可以去知道一个结点他到底是根节点的左孩子还是右孩子,从而我们才可以去得到结点对应的编码
- huffman树的创建规则
- 但是如果按照上面的思路那样子给出代码的话,其实代码是会崩溃的,由下面的位置可以看出此时ch的值变成了-117,那么为什么ch所显示出来的值是-117呢?原因在于我们所给的结构体其实是有问题的,因为我们呢结构体种ch的类型我们给出的是char类型的
- char在这里其实是一个有符号的类型,那么也就是说char的范围为-128~+127之间,但是ch又是我们从源文件中读取出来的一个一个字节,那么字符的最高位其实有可能是0,也有可能是1,如果是1的话,其实就是一个负数了,所以说,这里我们是不可以给成char类型的
- 那么我们给成无符号的char类型其实就是可以的
- 我们获取到了编码之后,我们要对原文件的内容进行改写的操作,但是在进行改写的操作的时候就很有可能会有一个问题存在,前面的字节基本没有什么问题,有问题的地方在于最后一个字节的位置,因为最后一个字节很有可能不够8个比特位,那么按照我们下面这种代码的写法不够8的比特位的话其实是无法将内容写进去的,那么我们应该如何对代码进行修改的操作呢?
- 通过调试可以看出我们给出的用例其实最后一个字节只存了7个比特位,那么他是不够8个比特位的,不够8个比特位的话是写不进去内容的
- 那么,现在我们对我们的代码进行修改的操作
- 但是我们在进行解压缩的时候,我们对于之前所给出的文件内容去进行解压缩的时候,是没有任何问题的,但是当我们对要进行压缩和解压缩的文件进行修改的时候,这个时候,有可能就会出现错误,比如说,像下面我给出的这个例子,在进行了解压缩之后就会出现错误了
- 那么如何去检查这个问题呢?
- 出现错误的原因是在下面的这里,之所以出错其实就是因为换行的原因,所以我们需要对换行来进行单独的处理
- 对换行符号来单独的进行处理
- 解决了换行所带来的问题之后,现在其实可能还存在有别的问题,比如说我等待压缩的文件中存在有汉字字符,经过我对程序的运行,我发现了程序发生了崩溃的现象,那么程序为什么会崩溃呢?
- 当放入了汉字你好之后,通过监视窗口我们就可以发现出现了负数的情况,汉字在保存的时候可能是用了多个字节来进行保存的
- 崩溃的原因就是在于你readBuff拿到了负数,所以代码发生了崩溃的问题
- 那么,这个问题其实也是很好解决的,我把它给成无符号类型的其实就是可以的了
- 还存在有一种别的问题
- 当需要压缩的文件篇幅很长的时候,只能压缩和解压缩一部分,后续的内容没有进行解压缩
- 二进制的文件按照文本的方式来进行打开可能就是会出错的