基于Huffman树的文件压缩小项目
1.文件压缩
1.1文件压缩的概念
文件压缩是指在不丢失有用信息的条件下缩减数据量以减少存储空间,提高其传输,存储和处理效率,或者按照一定的算法对文件中的数据进行重新组织,减少数据的冗余和存储空间的一种技术方法。
1.2文件压缩的分类
通常我们可以按照解压缩后的结果是否有损害将文件压缩的方式分为:有损压缩和无损压缩。
无损压缩:就是利用数据的统计冗余进行压缩可完全恢复原始数据而不引起任何失真,但压缩率是受到数据统计冗余度的理论限制,一般为2:1到5:1.这类方法广泛用于文本数据,程序和特殊应用场合的图像数据(如指纹图像,医学图像等)的压缩。
有损压缩:利用了人类对图像或声波中的某些频率成分不敏感的特性,允许压缩过程中损失一定的信息;虽然不能完全恢复原始数据,但是所损失的部分对理解原始图像的影响缩小,却换来了大得多的压缩比。
有损压缩广泛应用于语音,图像和视频数据的压缩。
一般有损压缩比无损压缩算法的压缩率要高
(压缩率=压缩后的文件/源文件)
1.3文件压缩的原理
电脑里文件都是以二进制储存的。压缩原理就是通过特定的算法,将文件转化,而转化以后的文件占用的空间较小。
1.4常见的几种文件压缩的方式
1.对于熟知的名称用固定短语进行替换
例如:西安科技大学,一般简称西科大,西安交通大学,简称西交大。
优点:原理上比较简单
缺点:需要提前准备好熟知的名称以及简称。
2.缩短文件中重复出现的数据
压缩方式是以重复出现数据与第一次出现数据的(距离和长度)。
重复出现的字节是3个或者3个以上才会替换,没有重复出现的内容原封不动的往压缩文件中写。
例如:源文件为 mnoabczxyuvwabc123456abczxydefgh
压缩的结果为mnoabczxyuvw(9,3)123456(18,6)defgh
3.用更短的比特位对字节进行替换一般分为等长替换和不等长替换
源文件:
DDCB DCDC BDDC ADCB
不等长编码表示(让出现次数多的字节对应的编码短一些):
结果:
字符 | 等长编码 | 不等长编码 |
---|---|---|
A | 00 | 100 |
B | 01 | 101 |
C | 10 | 11 |
D | 11 | 0 |
等长压缩结果:
11111001 11101110 01111110 00111001
不等长编码压缩结果:
00111010 11011101 00111000 11101
通过对比:不等长的压缩效果比等长的压缩效果好。
用比特位替换压缩后只用了4个字节,源文件用了16个字节,达到压缩的目的。
上述动态不等长编码有一种方式可以简单获取到,huffman树
2.Huffman树的文件压缩
2.1Huffman树
2.1.1huffman树的概念
二叉树的根结点到二叉树中所有叶结点的路径长度与相应权值的乘积之和为该二叉树的带权路径长度WPL。
上述四棵树的带权路径长度分别为:
WPLa = 1 * 2 + 3 * 2 + 5 * 2 + 7 * 2 = 32
WPLb = 1 * 2 + 3 * 3 + 5 * 3 + 7 * 1 = 33
WPLc = 7 * 3 + 5 * 3 + 3 * 2 + 1 * 1 = 43
WPLd = 1 * 3 + 3 * 3 + 5 * 2 + 7 * 1 = 29
其中·将带权路径最小的二叉树称为Huffman树。
2.1.2Huffman树的构造
- 由给定的n个权值{ w1, w2, w3, … , wn}构造n棵只有根节点的二叉树森林F={T1, T2 , T3, … ,Tn},每棵二叉树Ti只有一个带权值wi的根节点,左右孩子均为空。
- 重复以下步骤,直到F中只剩下一棵树为止
在F中选取两棵根节点权值最小的二叉树,作为左右子树构造一棵新的二叉树,新二叉树根节点的权
值为其左右子树根节点的权值之和
在F中删除这两棵二叉树
把新的二叉树加入到F中
按照上述图解
代码部分:
void CreateHuffmanTree(const W arry[], size_t size,const W& invaild)
{
//使用优先级队列保存二叉树森林的根节点
//建小堆--优先级队列的默认情况是大堆,要修改其比较规则
std::priority_queue<Node*, vector<Node*>, com<W>> q;
//注意这一块的greaer拿根节点的地址进行比较了 ,按照地址建小堆了;
//1.先使用所给的权值创建只有根节点的二叉树森林
for (size_t i = 0; i < size; i++)
{
if (arry[i] != invaild)
{
//创建一个只有根的二叉树,将arry中的树依次放到优先级队列中
q.push(new Node(arry[i]));
}
}
//循环进行一下步骤,直到二叉树森林中只剩下一颗二叉树的位置
while (q.size() > 1)
{
//从二叉树中先 去掉 权值最小的两颗二叉树
Node* left = q.top();
q.pop();
Node* right = q.top();
q.pop();
//将left和right作为某个新节点的的左右树中,构造成一个新的二叉树
//将左右子树的权值相加作为他们的根节点
Node* parent = new Node(left->quanzhi + right->quanzhi);
parent->left = left;
parent->right = right;
left->parent = parent;
right->parent = parent;
//将新的二叉树插入到二叉树森林中
q.push(parent);
}
root = q.top();//循环结束,剩余的树就是我们所需要的二叉树
}
//获取根节点
Node* GetRoot()
{
return root;
}
2.2压缩流程
2.2.1统计源文件中出现字符的次数
我们使用一个256的数组来统计字符出现的次数,每个元素的ASCII值刚好与数组的下标是一一对应的。因此:在统计时就可以直接以字节的ASCII值作为数组的下标进行统计。组数中的值就是字符出现的次数。
2.2.2 根据统计的结果构建Huffman树
将统计字符的次数根据上述Huffman树的原理进行构建
2.2.3通过Huffman树获取字符编码
对Huffman树进行遍历,当走到叶子节点的时候,往回退如果当前节点是左子树则往相应字符储存编码的的位置加0,如果是右子树则加1。等到当前节点的父节点为0时,结束,编码的位置是由叶子节点开始放的需要逆置,则该字符的对应编码就得到了,然后递归左子树,递归右子树,直到全部的字符编码都得到。
图解:
2.2.4编写解压缩的文件信息
2.2.5根据源文件的字符将对应的编码写入压缩文件中
将源文件中的每个字节替换成对应字节的编码
注意:在后面读取pf文件时,需要将pf文件指针放到文件起始的位置。
因为刚开始在统计字节出现的次数的时候,已经读取过i一遍文件了,pf已经在文件的末尾的位置了.
bool FileCompress::CompressFile(const string & filePath)
{
//1.统计源文件中每个字节出现的次数--->保存每个字符的信息
//"r"是以文本的形式进行读的,遇到-1会结束,所以需要以二进制"rb"
FILE* pf = fopen(filePath.c_str(), "rb");
if (nullptr == pf)
{
cout << "打开压缩文件失败!!" << endl;
return false;
}
//文件大小不知道---需要循环采用的方式来获取源文件的内容
uch readbuff[1024];//一次性读取的内容大小
while (true)
{
//rdsize表示实际读取的字节数
size_t rdsize = fread(readbuff, 1, 1024, pf);
if (0 == rdsize)
{
//现在已经读取到文件的末尾了
break;
}
//对读取的字节进行统计
for (size_t i = 0; i < rdsize; i++)
{
//利用:直接定制法--以字符的ASCII值作为数组的下标来进行快速的统计
fileByte[readbuff[i]].appearCount++;
}
}
//2.根据统计的结果创建huffman树
//在创建Huffman树时,需要将出现次数为0的字节去除掉
HuffmanTree<Byte>ht;
Byte invaild;
ht.CreateHuffmanTree(fileByte, 256,invaild);
///
//3.借助Huffman树获取每个字节的编码
GetHuffmanCode(ht.GetRoot());
//
//4.写解压缩时需要用到的信息
FILE* fOut = fopen("压缩.hzp", "wb");
WriteHead(fOut, filePath);
///
//5.使用字节的编码对源文件重新进行改写
//注意:在后面读取pf文件时,需要将pf文件指针放到文件起始的位置。
//因为刚开始在统计字节出现的次数的时候,已经读取过i一遍文件了,pf已经在文件的末尾的位置了
//fseek(pf, 0, SEEK_SET);
rewind(pf);//这两种方法都可以将pf放到起始的位置
uch ch = 0;
uch bitCount = 0;
while (true)
{
size_t rdsize = fread(readbuff, 1, 1024, pf);
if (0 == rdsize)
{
break;
}
//用编码改写字节--改写的结果需要放置到压缩结果文件当中
for (size_t i = 0; i < rdsize; i++)
{
//readbuff[i]--->'A'-->'100'
string& Code = fileByte[readbuff[i]].Code;
//将字符串格式的二进制编码往字节中存放
for (size_t j = 0; j < Code.size(); j++)
{
ch <<= 1;//高位丢弃,低位补0;
if (Code[j] == '1')
{
ch |= 1;
}
//当ch中的8个比特位填满之后,需要将该字节写入到压缩文件中
bitCount++;
if (8 == bitCount)
{
fputc(ch, fOut);//将该字节写入到压缩文件
bitCount = 0;
}
}
}
}
//检测:ch不够8个比特位,实际是没有写进去的
if (bitCount > 0 && bitCount < 8)
{
//解压缩是要从高位开始,所以不够8位时,要将剩余的放到高位
ch <<= (8 - bitCount);
fputc(ch, fOut);
}
fclose(pf);
fclose(fOut);
return true;
}
2.3解压缩流程
2.3.1读取解压缩信息
源代码:
//读取源文件的后缀
string postFix;
GetLine(fIn, postFix);
//频次信息的总行数、
string strContent;
GetLine(fIn, strContent);
size_t lineCount = atoi(strContent.c_str());
//循环读取lineCount行,获取字节的频次信息
strContent = "";
for (size_t i = 0; i < lineCount; i++)
{
GetLine(fIn, strContent);//将读取的一整行放到strContent中
if ("" == strContent)
{
//换行需要特殊处理
//说明刚刚读取到的是一个换行
strContent += "\n";
GetLine(fIn, strContent);//需要在读一次,将换行读进去
}
//fileByte[strContent[0]].ch = strContent[0];
fileByte[(uch)strContent[0]].appearCount = atoi(strContent.c_str() + 2);//A:1
strContent = "";
}
2.3.2构建Huffman树
//2.Huffman树
HuffmanTree<Byte> ht;
Byte invaild;
ht.CreateHuffmanTree(fileByte, 256, invaild);
2.3.3解压缩
根据压缩数据对应的二进制比特流来遍历Huffman树,
从压缩文件中逐个字节比特位来进行解压缩,依次获取每个比特位
如果该比特位为0,让cur往其左子树中移动
如果该比特位为1,往其右子树移动
只要cur走到叶子节点的位置,则成功解压缩一个字节,
将解压的字符写到文件中,回到根节点的位置,继续遍历。
代码部分:
string filename("解压缩");//解压缩文件的名字
filename += postFix;
FILE* fOut = fopen(filename.c_str(), "wb");//打开这个文件中去写
uch readbuff[1024];
uch bitCount = 0;
HuffmanTreeNode<Byte>* cur = ht.GetRoot();
const int fileSize = cur->quanzhi.appearCount;//根节点的权值为源文件的大小
int compressSize = 0;//解压缩的字节数
while (true)
{
size_t rdsize = fread(readbuff, 1, 1024, fIn);
if (0 == rdsize)
{
break;
}
for (size_t i = 0; i < rdsize; i++)
{
//逐个字节比特位来进行解压缩
uch ch = readbuff[i];
bitCount = 0;
//每走过一个比特位,用bitCount标记
while (bitCount < 8)
{
//保留高位
if (ch & 0x80)//检测ch的高位,如果高位为1,向右子树移动
{
cur = cur->right;
}
else
{
cur = cur->left;
}
if (nullptr == cur->left && nullptr == cur->right )
{
fputc(cur->quanzhi.ch, fOut);//一个字符解压缩成功
cur = ht.GetRoot();//回到根节点位置
compressSize++;
//如果成功解压缩字节的大小与原文件大小相同时则解压结束
if (compressSize == fileSize)
{
break;
}
}
bitCount++;
ch <<= 1;
}
}
}
fclose(fIn);
fclose(fOut);
return true;
}
3项目中出现的问题
3.1解压缩时解压不完全
由于使用文本形式读取压缩文件,有可能提前遇到文件结束标志,将读取方式改为二进制形式读写问题得到了解决。
3.2压缩汉字时出现问题
因为汉字是由多个字符表示,这些字符的范围是0—255,所以将程序中的char全部改为unsigned char问题得到解决。
3.3在压缩文本文件时成功但压缩图片和视频时程序崩溃
问题显示:
1.fileByte[strContent[0]].ch = strContent[0];
2.for (int i = 0; i < 256; i++)
{
fileByte[i].ch = i;
}
通过查找发现在程序中对数组中的存放的字符进行多次的初始化,当数据少时,即文本格式,数组没有发生越界,当为图片或者视频时,数据量非常大,可能造成了数组发现越界,将其中一个删掉,问题得到了解决。
4.关于项目的测试
这是文本格式的压缩和解压缩的结果,可以看出对于文本格式的压缩还有所效果。
这是图片格式的压缩和解压缩结果,有点尴尬压缩的结果变大了。
对于视频的压缩也是如此。
这里只展示了一组数据 可能结果不精确,但我们可以从中分析出现的原因。
为什么压缩的结果会变大?
在压缩时,通过Huffman树获取每个字节的编码
如果该字节的编码长度小于8个比特位,就会起到压缩效果,如果大于8个比特位,说明该字节是增大了,平均下来编码长度小于8比特位的字节总数<编码长度大于8比特位的字节总数,则压缩结果一定会变大。
为什么二进制文件的压缩效果不明显?
由于二进制格式文件中字符的的种类非常的多,如果字符出现次数比较均匀的情况下——压缩的效果就越差——压缩变大的可能性也越高,当Huffman树越接近二叉平衡树————压缩效果就越差。
所以这就是为什么二进制文件的压缩效果比文本格式的效果差。