詞典,分爲靜態詞典與自適應詞典兩種,而大多數基於自適應詞典的技術都源於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的方式。語言比較生硬,還請大家海涵。