词典,分为静态词典与自适应词典两种,而大多数基于自适应词典的技术都源于Jacob Ziv和Abraham Lempel在1977年和1978年发表的两篇里程碑式的论文。这两篇论文提供了两种不同的方法,用于自适应的构建词典,每种方法都衍生出多种变体。人们将基于1977年论文的方法划归LZ77系列,将基于1978年论文的方法划归LZ78系列。第二章链接中的文章对这部分内容的背景以及各种变体有更加详细的阐述,兹不赘述。
这里介绍的LZ77是最原始的LZ77,并不是deflate中使用的,但原理是相同的。我们先介绍原始的,再分析实际的。在LZ77算法中,所谓字典其实就是之前已编码(可以暂时不去理解何谓“已编码”)序列的一部分,算法使用一个滑动窗口(滑动窗口的概念很重要,源码中也要涉及)来查看输入序列。这个窗口包括两个部分,
i. 查找缓冲区:该缓冲区包含了最近已编码序列的一部分,这个概念后续源码分析也要用;
ii. 先行缓冲区:该缓冲区包含了待编码序列的下一部分,这个概念后续源码分析也要用;
如下图所示,
三元组中第三个元素的存在意义是考虑到某些情况下,在查找缓冲区中无法找到先行缓冲区的匹配符号。在这种情况下,偏移量和匹配长度值被设定为0,三元组的第三个元素就是该符号自身的代码(码字)。但是使用三元组的效率极低,当出现频率较低的字符大量存在时尤为严重。所以LZ77算法的大多数变体都杜绝使用三元组对单个字符进行编码,比如LZSS。
Deflate算法中使用的LZ77就是原始LZ77的一种变体,但依然是LZ77而不是LZSS或者别的什么变体,它只是把原始LZ77中的三元组直接改成了二元组,只有“匹配长度+偏移量”。另外,deflate算法对最长匹配项的查找做了优化,如下图所示,
看到这里,我想有些读者应该会有疑问了,比如:“用三元组替换后,解压时怎么把普通符号和三元组中的内容区分来?”,“被替换的匹配项和三元组占用内存一样大怎么办?”,“专门找上文中所说的最长匹配项?”,等等。不用着急,这些问题后续都会一一说明,这里要做的仅仅就是知道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的方式。语言比较生硬,还请大家海涵。