GZIP压缩原理分析(11)——第五章 Deflate算法详解(五02) 预备知识(01) LZ77算法

词典,分为静态词典与自适应词典两种,而大多数基于自适应词典的技术都源于Jacob Ziv和Abraham Lempel在1977年和1978年发表的两篇里程碑式的论文。这两篇论文提供了两种不同的方法,用于自适应的构建词典,每种方法都衍生出多种变体。人们将基于1977年论文的方法划归LZ77系列,将基于1978年论文的方法划归LZ78系列。第二章链接中的文章对这部分内容的背景以及各种变体有更加详细的阐述,兹不赘述。

 

这里介绍的LZ77是最原始的LZ77,并不是deflate中使用的,但原理是相同的。我们先介绍原始的,再分析实际的。在LZ77算法中,所谓字典其实就是之前已编码(可以暂时不去理解何谓“已编码”)序列的一部分,算法使用一个滑动窗口(滑动窗口的概念很重要,源码中也要涉及)来查看输入序列。这个窗口包括两个部分,

 i.       查找缓冲区:该缓冲区包含了最近已编码序列的一部分,这个概念后续源码分析也要用;

ii.       先行缓冲区:该缓冲区包含了待编码序列的下一部分,这个概念后续源码分析也要用;

如下图所示,

 这里只是用作示例,实际的窗口没有这么小。上图所示的查找缓冲区包含8个符号,先行缓冲区包含7个符号。“当前指针”位置就是此时此刻要编码的数据,图中该指针左面的数据已经完成编码,右面是未来要编码的数据。算法通过“匹配指针”在查找缓冲区中找出与先行缓冲区中“当前指针”相匹配的符号,(“当前指针”是我为了描述方便而虚拟的,实际上该指针指向的内容就是先行缓冲区的第一个符号,可以把该指针定义为:指向先行缓冲区第一个符号的指针),这两根指针的距离(更准确的讲,是匹配指针与先行缓冲区第一个符号之间的距离)称为偏移量。算法随后查看匹配指针位置右边的符号,看它们是否与先行缓冲区中“当前指针”右边的连续的符号相匹配。查找缓冲区中的连续符号与先行缓冲区中连续符号相匹配的数目(从“当前指针”位置算起)称为匹配长度。算法会在查找缓冲区中搜索最长匹配项。找到最长匹配后,算法会用一个三元组<o,l,c>对其编码,其中,o为偏移量,l为匹配长度,c是先行缓冲区中跟在该匹配项之后的符号的码字。上图中,匹配指针指向最长匹配项的开头,偏移量o为7,匹配长度l为4,先行缓冲区中跟在匹配项之后的符号为r。看到这里,可以初步体会LZ77对于压缩的意义,LZ77使用匹配项来替换字符串,上例中就可以用那个三元组去替换“当前指针”往右(包括该指针指向的那个符号)的连续四个符号“a、b、r、a”,从而达到将数据初步压缩的目的。这个例子因为数据量小,只是个示例,所以LZ77的效果体现并不明显,只要明白基本原理即可。解压的时候,读到三元组,那么只要将三元组各个成员解析出来,根据解析结果从已经解码的数据中找到匹配数据并用这些数据替换这个三元组即可实现解压。

 

三元组中第三个元素的存在意义是考虑到某些情况下,在查找缓冲区中无法找到先行缓冲区的匹配符号。在这种情况下,偏移量和匹配长度值被设定为0,三元组的第三个元素就是该符号自身的代码(码字)。但是使用三元组的效率极低,当出现频率较低的字符大量存在时尤为严重。所以LZ77算法的大多数变体都杜绝使用三元组对单个字符进行编码,比如LZSS。

 

Deflate算法中使用的LZ77就是原始LZ77的一种变体,但依然是LZ77而不是LZSS或者别的什么变体,它只是把原始LZ77中的三元组直接改成了二元组,只有“匹配长度+偏移量”。另外,deflate算法对最长匹配项的查找做了优化,如下图所示,

 箭头所指的地方,的确可以找到一个匹配串,但只有三个字符“the”,如果往后一个字符,即红框中的匹配,那就是“he ne”共四个字符(带上空格),匹配长度更长。在deflate算法中,找匹配长度的过程不是找到一个就匹配,而是要在“局部”(预备知识:贪心算法思想)找到最长的那一个匹配。上图中的情况,对于deflate中的LZ77,它会把t作为一个单独的字符输出,而把“he ne”作为一个匹配,输出一个“匹配长度+偏移量”,简称“长度、距离对儿”。在gzip1.2.4源码中,有一个叫“longest_match()”的函数专门干这件事,后续章节分析。

 

看到这里,我想有些读者应该会有疑问了,比如:“用三元组替换后,解压时怎么把普通符号和三元组中的内容区分来?”,“被替换的匹配项和三元组占用内存一样大怎么办?”,“专门找上文中所说的最长匹配项?”,等等。不用着急,这些问题后续都会一一说明,这里要做的仅仅就是知道LZ77的原理而已,先明白原理,再分析实现细节。

 

“LZ77算法做了一个隐含假设:相似的模式会在一起聚集出现。该方法以序列中最近解码得出的部分作为编码词典,从而利用了上述假设结构。但这意味着,对于任意一种模式,只要其重复周期长于编码器窗口的覆盖长度,就不会再被捕捉到。最糟糕的情景是:待编码序列是周期序列,但其周期长于查找缓冲区。”——《数据压缩导论(第四版)》。这句话的意思就是说“重复现象具有局部性”(摘自本章开头提到的那篇博客),最简单的例子就是我们平时说的“重要的事情说三遍”!!!但这是LZ77算法的一个隐含假设罢了(但假设的很有道理,感觉现实中就是这个样子)。如果碰到数据重复的周期超过查找缓冲区的情况,那么匹配项的查找将是很糟糕的。这个其实很容易理解,假设对一个字符串使用LZ77算法,这个字符串就是由a到z这26字母按顺序组成并重复三次,共79个字符(包括'\0'),但是查找缓冲区只能放15个字符,先行缓冲区能放5个字符,其中一个比较糟糕的情况就是,a~o在查找缓冲区,p~t在先行缓冲区,根本没得匹配也没得替换!!!对于这种情况,书中提到了LZ78,而deflate采取的方法是使用窗口。虽然不能完全抛掉那个隐含假设,也不能完全避免那种极端情况,但是已经非常有效了。

 

总结,上面我们简单介绍了原始LZ77以及deflate中的LZ77变体,对LZ77有了一个基本认识,提到了longest_match()函数、贪心算法思想、滑动窗口概念,对LZ77的极端情况作了简要介绍,简单提了一下解码LZ77的方式。语言比较生硬,还请大家海涵。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章