LZ77算法原理:
LZ77是基于字节的通用压缩算法,它的原理就是将 源文件中的重复字节(即在前文中出现的重复字节)使用 (distance,length,nextchar)的三元组进行替换参数:distance:表示待匹配的当前字符距离匹配字符串首字母的距离length :表示匹配字符串的长度,即有多少 个字符与前文匹配nextchar:表示当前匹配串的下一个字符例如:下面的内容,使用LZ77算法进行压缩的话mnoabczxyuvwabc123456abczxydefgh压缩结果:mnoabczxyuvw(9,3,1)23456(18,6,d)efgh当我们解压缩的时候,如果是一个三元组信息,我们就只需要向前distance个字节,匹配length个字节,匹配完毕之后下一个字节存放nextchar;
LZ77实现:
1.使用二元组代替三元组
但是我们发现nextchar的信息我们不必保留在三元组中,如果我们将三元组替换为(distance ,length) 二元组,也能够完成同样的目的
mnoabczxyuvw(9,3)123456(18,6)defgh
2.采用位图区分二元组和原字符
但是如果我们写入压缩文件中,数据就是mnoabczxyuvw93123456186defgh的样子,我们需要怎么来区别是长度、距离还是原字符呢?
我们采用位图的方式,如果是原字符串我们使用一个为0的比特位标记,如果是长度、距离我们使用一个为1的比特位进行标记
3.匹配区间问题
lz77算法的原理就是替换重复字符串,想替换重复字符串我们必须要向前查找重复字符串,如果采用暴力查找的话,时间复杂度就是O(N^2),倘若我们查找重复字符串的区间是整个文件,即使文件大小仅仅为1KB,那么时间复杂度就会为O(1024^2) ,可以说这样效率十分的低效,即使我们采用更加高效的查找算法,但不可否认随着向前匹配的区间越大,所消耗的时间也就越长;
因此我们需要约定一个区间大小,仅仅前面的这个区间内查找匹配字符串,但是这个大小是多少呢?
unsigned char能够达到255个字节,如果一个中文一个字节的话,一般说255个字,重复的几率基本不会太大,我们需要一个更大的区间
unsigned short能够达到32767个字节,再以上文为例,说3万多个字,重复的可能性就大幅度提高了,并且这个长度,没有太大,效率受到的影响很微小;
因此,我们采用unsigned short作为distance的数据类型;
而length呢?我们采用unsigned char类型就已经足够了,一直重复说255个字是十分少见的;
4.替换的重复字符长度
再者,我们现在约束好了二元组的长度,二元组长度为3个字节,倘若我们匹配到了2个字节的重复字符,就不能替换,因为替换后会导致文件变大,倘若替换,而少于3个字节的重复字符串过大,可能导致压缩文件更加庞大。因此只有重复字符长度等于或者超过3个字节的时候,我们才有必要进行替换。
同时呢?我们可以将匹配的长度扩大三个字节,length=0的时候,我们可以认为重复字符串的长度为3个字节;length=255的时候,我们可以认为重复字符串的长度为258个字节
5.查找重复字符串的效率问题(哈希表)
在讨论匹配区间的时候,我们就提到了,查找重复字符串的效率问题,倘若使用暴力查找的话,效率将会很低,而我们采用哈希映射的方式来高效的查找重复字符串,但是我们知道使用哈希表必定会产生哈希冲突,我们要怎么解决哈希冲突呢?如果使用闭散列的话,同样的问题,查找效率将会成为制约因素;但是使用开散列的话,频繁的插入节点,浪费空间,桶过长的话,效率也成为要思考的一方面。
那么要怎么实现哈希表呢?具体的原理如下:
我们给定一块连续的内存空间,分成相同大小的两份,前面那块称为prev块,用于解决哈希冲突,后面的那块称为head,用来保存重复字符串的首个字符的索引位置
6.动态读取文件
动态读取文件其实和huffman中分块读取文件的想法是一样的,主要是防止文件过大,内存装载不下的问题。动态读取主要是采用了滑动窗口的思想,前面我们提到最大匹配距离为32767,所以我们有一个64k的窗口,从文件中读取相应的数据进行填充。我们将前32k称为左窗口,后32k称为右窗口;当我们在左窗口的时候,正常的查找重复字符,压缩数据就可以了;而当走到右窗口的时候,我们就需要注意,我们查找重复字符的最大区间位置
当右窗口的带压缩数据很小的时候,而文件中还有带压缩数据,这个时候就会出现 右窗口的数据和文件带压缩数据放在一起,可能有更长的重复字符串,这个时候,为了更高的压缩效率,我们可以将数据移动左窗,在读取文件数据填充满整个窗口。 因此,我们需要设定一个阈值MIN_LOOKAHEAD,每当到达这个阈值的时候,就自动将右窗数据搬到左窗,同时读取文件数据填充窗口。
与此同时,我们整个哈希表的大小也就确定下来了,和窗口大小相同为64k
压缩流程:
- 打开带压缩的文件(注意:必须按照二进制格式打开,因为用户进行压缩的文件不确定)
- 获取文件大小,如果文件大小小于3个字节,则不进行压缩
- 读取一个窗口的数据,即64K
- 用前两个字符计算第一个字符与其后两个字符构成字符串哈希地址的一部分,因为哈希地址是通过三个 字节算出来的,先用前两个字节算出一部分,在压缩时,再结合第三个字节算出第一个字符串完整的哈 希地址。
- 循环开始压缩 计算哈希地址,将该字符串首字符在窗口中的位置插入到哈希桶中,并返回该桶的状态 matchHead
- 根据matchHead检测是否找到匹配
- 如果matchHead等于0,未找到匹配,表示该三个字符在前文中没有出现过,将该当前字符 作为源字符写到压缩文件中
- 如果matchHead不等于0,表示找到匹配,matchHead代表匹配链的首地址,从哈希桶 matchHead位置开始找最长匹配,找到后用该(距离,长度对)替换该字符串写到压缩文件中,然后将该替换串三个字符一组添加到哈希表中。
- 如果窗口中的数据小于MIN_LOOKAHEAD时,将右窗口中数据搬移到左窗口,从文件中新读取一个窗口 的数据放置到右窗,更新哈希表,继续压缩,直到压缩结束。
压缩文件必须保存的信息
解压缩流程:
1. 获取标记占用的字节大小2.获取源文件的字节大小3.读取标记,通过标记还原数据如果当前标记为1,表示正在解压缩的数据为长度距离对,向前查找重复字符串,写入压缩文件如果当前标记为0,表示正在解压缩的数据为原字符,写入解压缩文件4.解压缩的字节大小等于源文件大小的时候,解压缩完毕
项目源码:https://github.com/Qyuan926/FileCompress
项目测试:
分析:对图片,视频、音乐等文件进行压缩的时候,我们发现压缩文件不仅没变小,反而增大了;其实这种原因主要有两个方面:一个是这类文件的重复字符串的数量较少;最主要的原因在于,我们保存的标记信息,占用了原文件的八分之一大小,这是导致压缩率较低的最主要的原因。
注:如果本篇博客有任何错误和建议,欢迎伙伴们留言,你快说句话啊!