【项目】基于LZ77和huffman的压缩项目

本文介绍了一个基于LZ77和Huffman压缩算法的项目,详细阐述了LZ77算法的原理、实现细节,包括查找最长匹配的方法,以及Huffman编码的压缩和解压缩步骤。项目实现部分探讨了压缩的两种形式、编码的处理方式以及距离和长度的分组策略,旨在提高压缩效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、项目简介

1.什么是压缩

文件压缩是指在不丢失文件数据信息的前提下,依靠一定的算法对数据进行处理,缩减数据量以减少存储空间,提高其存储,传输和处理的效率,或者按照一定的的算法对数据进行重新组织,从而达到减少数据冗余的一种数据处理手段。

2.为什么需要压缩

  • 进行通信的时候,有必要将待传输的数据进行压缩,以减少带宽需求
  • 计算机存储数据的时候,为了减少磁盘容量需求,也会将文件进行压缩,压缩后的数据容量减小,磁盘访问IO的时间也缩短,尽管压缩和解压缩过程会消耗CPU资源,但是CPU计算资源增长得很快,但是磁盘IO资源却变化得很慢,把磁盘的IO压力转化到CPU上,总体上能够提升系统运行速度。
  • 压缩作为一种非常典型的技术,会应用到很多很多场合下,比如文件系统、数据库、消息传输、网页传输等等各类场合。

二、项目步骤

项目总体分为两大块

1.LZ77

1.使用LZ77压缩算法将源文件中重复语句快速压缩

2.huffman

2.再使用huffman编码方式将LZ77压缩结果从字节层面再次压缩,以达到更好的压缩效果

三、项目分解

1.LZ77

1.1什么是LZ77

LZ77算法是采用字典做数据压缩的算法,由以色列的两位大神Jacob Ziv与Abraham Lempel在1977年发表的论文《A Universal Algorithm for Sequential Data Compression》中提出。

1.2 LZ77算法原理

LZ77压缩算法采用字典的方式进行压缩,是一个简单但十分高效的数据压缩算法。其方式就是把数据中一些可以组织成短语(最长字符)的字符加入字典,然后再有相同字符出现采用标记来代替字典中的短语,如此通过标记代替多数重复出现的方式以进行压缩。
使用(offset, length, nextchar)的三元组进行替换

offset:待匹配的当前字符距离匹配字符串的首字符的距离
length:匹配字符串的长度
nextchar:当前匹配字符串的下一个字符

LZ77的主要算法逻辑就是,先通过前向缓冲区预读数据,然后再向滑动窗口移入(滑动窗口有一定的长度),不断的寻找能与字典中短语匹配的最长短语,然后通过标记符标记。
本质:将源文件中重复的语句采用更短的长度距离来进行替换

解释

前向缓冲区
每次读取数据的时候,先把一部分数据预载入前向缓冲区。为移入滑动窗口做准备。就是下文提到的先行缓冲区

滑动窗口
一旦数据通过缓冲区,那么它将移动到滑动窗口中,并变成字典的一部分。

短语字典
从字符序列S1…Sn,组成n个短语。比如字符(A,B,D) ,可以组合的短语为{(A),(A,B),(A,B,D),(B),(B,D),(D)},如果这些字符在滑动窗口里面,就可以记为当前的短语字典,因为滑动窗口不断的向前滑动,所以短语字典也是不断的变化。

1.3 图解压缩

因为需要从文件中读出数据,才可以判断有无重复的字符,因此需要缓冲区,大小是64K, 至于大小为什么是64K,大致因为LZ77这个算法是77年提出来的,那个年代的内存比较小,可能只有几十兆的空间,基于考虑只给了64K
例如下面这个例子

monabcxzyuvwabc123456abczxydefgh

经过LZ77压缩以后会变成如下这样

monabcxzyuvw(93123456186)defgh

注意
1.标准中采用的是<距离 长度 下一个字符>这样的格式,但是GZIP没采用在这样的方式,而是做了一点改变,因为inextchar对压缩率并没有什么帮助,因此GZIP采用了<距离 长度>这样的格式来进行替换
2.实际压缩写到文件中的不是(9,3)这种格式,这样写是方便我们查看的,实际写进文件的就只是93

分析到这里我们就很容易提出一些问题

❓1.重复几个字节才要替换 ?
❓2.如何在前文找重复?如何找到最长的重复?
❓3.既然只是写入长度距离对,那如何区分什么时候是源字符,什么时候是替换后的长度距离对?
下面一一解答

1.3.1 重复几个字节才要替换 ?

在这里插入图片描述
上图是LZ77的大致描述
一个start指针不断向后移动,将64K缓冲区划分成了两个区间

  • 已经扫描过的查找缓冲区,查找缓冲区中的数据是为了后序在这个区间里找是否有重复
  • 还未扫描的先行缓冲区,先行缓冲区是待压缩的数据
  • 随着start向后移动,查找缓冲区越来越大,先行缓冲区越来越小

理论上查找重复的字符串应该查找到查找缓冲区的最左侧,向左找的距离越长,可能重复的匹配效率越高,但是这样也带来的缺陷

  • 找的越远,需要花费的时间就越多
  • 根据局部性原理,大部分重复的距离都不会太远

因此在大佬设计LZ77的时候,并没有查找到查找缓冲区的最左侧,而是设置了一个MAX_DIST,表示从start位置向左能够找到的最大距离,因此距离使用了最大15个比特位来表示————解决距离问题

正常情况下一句话中超过255个字节的情况比较少,(人也是需要换气的),即使存在超过255个的情况,也是极少数,如果为了这极少数的情况而使用两个字节来进行保存,有点浪费,因此关于长度是用一个字节进行保存,(即使超过了一个字节的也可以拆成两个),长度的范围是[3, 258]————解决长度问题

回到问题1:重复几个字节才要替换 ?
距离是不到两个字节,长度是一字节,因此只要大于等于三字节的重复就可以压缩————问题1解决

我们在查找缓冲区中没有向左找到尽头,而是设置之了一个MAX_DIST,那先行缓冲区也没有找到最后一个位置,因为先行缓冲区是不能任意小的,如果任意小,可能就达不到最长匹配
解释一下
我们每次从源文件中读取64K的数据,但是源文件可能并不止64K,如果将先行缓冲区中设于的最小长度与源文件中剩余是数据拼接起来,可能达到的匹配长度更长,因此给先行缓冲区设置了一个最小的长度MIN_LOOKAHEAD
下面是一些助于理解小公式

MIN_MATCH = 3;//单位字节
MAX_MATCH = 258;
MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1
MAX_DIST = WSIZE - MIN_LOOKAHEAD

当压缩过程达到该临界值的时候(压缩到MIN_LOOKAHEAD),就会将左窗中的数据搬移到右窗中
在这里插入图片描述
所以有人说LZ77的思想有点像滑动窗口

1.3.2 如何快速找到最长匹配
方法一

暴力破解法:即循环向前寻找匹配串

在这里插入图片描述
缺点:该算法虽然可以是实现我们的需求,但时间复杂度为O(n^2),处理的效率太低,这样的话一旦遇到了较大的待压缩文件,这样暴力求解的方式会大大影响压缩的时间和效率。

方法二

采用哈希桶来实现

采用哈希桶的方式,我们将每三个相邻的字符构成的字符串首字符作为索引保存在哈希桶中,压缩时每遇到新字符,计算该字符串所构成的串的哈希地址,然后将该字符串的首字符在窗口中的索引插入哈希桶,如果当前桶为空,说明未找到匹配,否则可能会找到匹配,在定位到具体的匹配串的位置进行匹配即可。

1.哈希桶的大小分析

而在分析时我们发现,对于哈希桶的大小我们应该如何界定?

如果按照单纯的想法,3个字符那就有(2^24) 种取值方式,桶的个数则需要(2^24)个, 而索引的大小为2个字节,这样算下来桶就要占到(2^25 )也就是32M字节,这是一个非常大的开销,而且在压缩的过程中,表中的数据是在不断变化的,这样的话对于程序而言会严重影响程序运行的效率,因此我们在这里将哈希桶的个数界定为(2^15)也就是32K。

2.哈希表的结构

避免哈希冲突:

前面我们分析了为了保证程序运行的效率,我们将哈希桶的个数界定为了(2^15) 个,而原本需要的哈希桶个数应该为(2^24)个,哈希桶的减少造成了key->value时,目标地址可能会被占用,正所谓一山不容二虎,因此必然会产生哈希冲突,而如果采用开散列来解决的话,链表中的节点要不断的申请和释放,影响效率,因此在这里我们将哈希表由一块连续的内存空间组成,同时分为两个部分,每部分大小为WSIZE(32K)
在这里插入图片描述
如图所示:prev指向整个内存的起始位置,而因为内存是连续的,因此head=prev+WISZE;我们将prev和head看成两个数组,prev数组用来保存三个字符串首字符的索引位置,head的索引为三个字符通过哈希函数计算得出的哈希值

3.哈希函数

而对于哈希函数的设计我们则要遵循一个原则:简单,离散。

因此在这里我们将哈希函数设计成这样:

A(4,5) + A(6,7,8) ^ 8(1,2,3) + 8(4,5) + 8(6,7,8) ^ C(1,2,3) + C(4,5,6,7,8);

给一个简单的说明:

  • A是指3个字节中的第1个字节,B是指第2个字节,C指第3个字节

  • A(4,5)是指第一个字节的第4,5位二进制码。

  • "+"是连接而并不是”加“,”^“的优先级高于”+”

  • 这样的结果使三个字节都尽量参与到结果


//HashAddr   上一个字符串计算得到的哈希地址
//而本次的哈希地址是通过上一次的哈希地址结合当前字符ch算出的;
//而这里将哈希地址与哈希掩码相与的目的是防止哈希地址越界
 
void HashTable::HashFunc(USH& HashAddr,UCH ch)
{
   
    HashAddr = ((HashAddr) << H_SHIFT()) ^ (ch) & HASH_MASK;
}
 
 
USH 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风铃奈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值