讓我們熟悉一下 LZ77 算法的基本流程。
1、從當前壓縮位置開始,考察未編碼的數據,並試圖在滑動窗口中找出最長的匹配字符串,如果找到,則進行步驟 2,否則進行步驟 3。
2、輸出三元符號組 ( off, len, c )。其中 off 爲窗口中匹配字符串相對窗口邊界的偏移,len 爲可匹配的長度,c 爲下一個字符。然後將窗口向後滑動 len + 1 個字符,繼續步驟 1。
3、輸出三元符號組 ( 0, 0, c )。其中 c 爲下一個字符。然後將窗口向後滑動 len + 1 個字符,繼續步驟 1。
我們結合實例來說明。假設窗口的大小爲 10 個字符,我們剛編碼過的 10 個字符是:abcdbbccaa,即將編碼的字符爲:abaeaaabaee
我們首先發現,可以和要編碼字符匹配的最長串爲 ab ( off = 0, len = 2 ), ab 的下一個字符爲 a,我們輸出三元組:( 0, 2, a )
現在窗口向後滑動 3 個字符,窗口中的內容爲:dbbccaaaba
下一個字符 e 在窗口中沒有匹配,我們輸出三元組:( 0, 0, e )
窗口向後滑動 1 個字符,其中內容變爲:bbccaaabae
我們馬上發現,要編碼的 aaabae 在窗口中存在( off = 4, len = 6 ),其後的字符爲 e,我們可以輸出:( 4, 6, e )
這樣,我們將可以匹配的字符串都變成了指向窗口內的指針,並由此完成了對上述數據的壓縮。
解壓縮的過程十分簡單,只要我們向壓縮時那樣維護好滑動的窗口,隨着三元組的不斷輸入,我們在窗口中找到相應的匹配串,綴上後繼字符 c 輸出(如果 off 和 len 都爲 0 則只輸出後繼字符 c )即可還原出原始數據。
當然,真正實現 LZ77 算法時還有許多複雜的問題需要解決,下面我們就來對可能碰到的問題逐一加以探討。
編碼方法
我們必須精心設計三元組中每個分量的表示方法,才能達到較好的壓縮效果。一般來講,編碼的設計要根據待編碼的數值的分佈情況而定。對於三元組的第一個分量——窗口內的偏移,通常的經驗是,偏移接近窗口尾部的情況要多於接近窗口頭部的情況,這是因爲字符串在與其接近的位置較容易找到匹配串,但對於普通的窗口大小(例如 4096 字節)來說,偏移值基本還是均勻分佈的,我們完全可以用固定的位數來表示它。
編碼 off 需要的位數 bitnum = upper_bound( log2( MAX_WND_SIZE ))
由此,如果窗口大小爲 4096,用 12 位就可以對偏移編碼。如果窗口大小爲 2048,用 11位就可以了。複雜一點的程序考慮到在壓縮開始時,窗口大小並沒有達到MAX_WND_SIZE,而是隨着壓縮的進行增長,因此可以根據窗口的當前大小動態計算所需要的位數,這樣可以略微節省一點空間。
對於第二個分量——字符串長度,我們必須考慮到,它在大多數時候不會太大,少數情況下才會發生大字符串的匹配。顯然可以使用一種變長的編碼方式來表示該長度值。在前面我們已經知道,要輸出變長的編碼,該編碼必須滿足前綴編碼的條件。其實 Huffman編碼也可以在此處使用,但卻不是最好的選擇。適用於此處的好的編碼方案很多,我在這裏介紹其中兩種應用非常廣泛的編碼。
第一種叫 Golomb 編碼。假設對正整數 x 進行 Golomb 編碼,選擇參數 m,令
b = 2m
q = INT((x - 1)/b)
r = x - qb - 1
則 x 可以被編碼爲兩部分,第一部分是由 q 個 1 加 1 個 0 組成,第二部分爲 m 位二進制數,其值爲 r。我們將 m = 0, 1, 2, 3 時的 Golomb 編碼表列出:
值 x m = 0 m = 1 m = 2 m = 3
-------------------------------------------------------------
1 0 0 0 0 00 0 000
2 10 0 1 0 01 0 001
3 110 10 0 0 10 0 010
4 1110 10 1 0 11 0 011
5 11110 110 0 10 00 0 100
6 111110 110 1 10 01 0 101
7 1111110 1110 0 10 10