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的方式。語言比較生硬,還請大家海涵。

 

 

 

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