資料壓縮演算法:LZ77 演算法的分析與實現

發表於2018-07-17

LZ77簡介

Ziv和Lempel於1977年發表題為“順序資料壓縮的一個通用演算法(A Universal Algorithm for Sequential Data Compression )”的論文,論文中描述的演算法被後人稱為LZ77演算法。值得說的是,LZ77嚴格意義上來說不是一種演算法,而是一種編碼理論。同Huffman編碼一樣,只定義了原理,並沒有定義如何實現。基於這種理論來實現的演算法才稱為LZ77演算法,或者人們更願意稱為LZ77變種。實際上這類演算法已經有很多了,比如LZSS、LZB、LZH等。至今,幾乎我們日常使用的所有通用壓縮工具,象ARJ,PKZip,WinZip,LHArc,RAR,GZip,ACE,ZOO,TurboZip,Compress,JAR„„甚至許多硬體如網路裝置中內建的壓縮演算法,無一例外,都可以最終歸結為這兩個以色列人的傑出貢獻。

LZ77是一種基於字典的演算法,它將長字串(也稱為短語)編碼成短小的標記,用小標記代替字典中的短語,從而達到壓縮的目的。也就是說,它通過用小的標記來代替資料中多次重複出現的長串方法來壓縮資料。其處理的符號不一定是文字字元,可以是任意大小的符號。

短語字典的維護

不同的基於字典的演算法使用不同的方法來維護它們的字典。LZ77使用的是一個前向緩衝區和一個滑動視窗

LZ77首先將一部分資料載入前向緩衝區。為了便於理解前向緩衝區如何儲存短語並形成字典,我們將緩衝區描繪成S1,…,Sn的字元序列,Pb是由字元組成的短語集合。從字元序列S1,…,Sn,組成n個短語,定義如下:

Pb = {(S1),(S1,S2),…,(S1,…,Sn)}

例如,如果前向緩衝區包含字元(A,B,D),那麼緩衝區中的短語為{(A),(A,B),(A,B,D)}。

一旦資料中的短語通過前向緩衝區,那麼它將移動到滑動視窗中,並變成字典的一部分。為理解短語是如何在滑動視窗中表示的,首先,把視窗想象成S1,…,Sm的字元序列,且Pw是由這些字元組成的短語集合。從序列S1,…,Sm產生短語資料集合的過程如下:

Pw = {P1,P2,…,Pm},其中Pi = {(Si),(Si,Si+1),…,(Si,Si+1,…,Sm)}

例如,如果滑動視窗中包含符號(A,B,C),那麼視窗和字典中的短語為{(A),(A,B),(A,B,C),(B),(B,C),(C)}。

LZ77演算法的主要思想就是在前向緩衝區中不斷尋找能夠與字典中短語匹配的最長短語。以上面描述的前向緩衝區和滑動視窗為例,其最長的匹配短語為(A,B)。

壓縮和解壓縮資料

前向緩衝區和滑動視窗之間的匹配有兩種情況:要麼找到一個匹配短語,要麼找不到匹配的短語。當找到最長的匹配時,將其編碼成短語標記。

短語標記包含三個部分:1、滑動視窗中的偏移量(從頭部到匹配開始的前一個字元);2、匹配中的符號個數;3、匹配結束後,前向緩衝區中的第一個符號。

當沒有找到匹配時,將未匹配的符號編碼成符號標記。這個符號標記僅僅包含符號本身,沒有壓縮過程。事實上,我們將看到符號標記實際上比符號多一位,所以會出現輕微的擴充套件。

一旦把n個符號編碼並生成相應的標記,就將這n個符號從滑動視窗的一端移出,並用前向緩衝區中同樣數量的符號來代替它們。然後,重新填充前向緩衝區。這個過程使滑動視窗中始終有最新的短語。滑動視窗和前向緩衝區具體維護的短語數量由它們自身的容量決定。

下圖(1)展示了用LZ77演算法壓縮字串的過程,其中滑動視窗大小為8個位元組,前向緩衝區大小為4個位元組。在實際中,滑動視窗典型的大小為4KB(4096位元組)。前向緩衝區大小通常小於100位元組。

圖(1):使用LZ77演算法對字串ABABCBABABCAD進行壓縮

我們通過解碼標記和保持滑動視窗中符號的更新來解壓縮資料,其過程類似於壓縮過程。當解碼每個標記時,將標記編碼成字元拷貝到滑動視窗中。每當遇到一個短語標記時,就在滑動視窗中查詢相應的偏移量,同時查詢在那裡發現的指定長度的短語。每當遇到一個符號標記時,就生成標記中儲存的一個符號。下圖(2)展示瞭解壓縮圖(1)中資料的過程。

圖(2):使用LZ77演算法對圖(1)中壓縮的字串進行解壓縮

LZ77的效率

用LZ77演算法壓縮的程度取決於很多因素,例如,選擇滑動視窗的大小,為前向緩衝區設定的大小,以及資料本身的熵。最終,壓縮的程度取決於能匹配的短語的數量和短語的長度。大多數情況下,LZ77比霍夫曼編碼有著更高的壓縮比,但是其壓縮過程相對較慢。

用LZ77演算法壓縮資料是非常耗時的,國為要花很多時間尋找視窗中的匹配短語。然而在通常情況下,LZ77的解壓縮過程要比霍夫曼編碼的解壓縮過程耗時要少。LZ77的解壓縮過程非常快是因為每個標記都明確地告訴我們在緩衝區中哪個位置可以讀取到所需要的符號。事實上,我們最終只從滑動視窗中讀取了與原始資料數量相等的符號而已。

LZ77的介面定義

lz77_compress


int lz77_compress(const unsigned char *original, unsigned char **compressed, int size);

返回值:如果資料壓縮成功,返回壓縮後資料的位元組數;否則返回-1;

描述:   用LZ77演算法壓縮緩衝區original中的資料,original包含size個位元組的空間。壓縮後的資料存入緩衝區compressed中。lz77_compress需要呼叫malloc來動態的為compressed分配儲存空間,當這塊空間不再使用時,由呼叫者呼叫函式free來釋放空間。

複雜度:O(n),其中n是原始資料中符號的個數。

lz77_uncompress


int lz77_uncompress(const unsigned char *compressed, unsigned char **original);

返回值:如果解壓縮資料成功,返回恢復後資料的位元組數;否則返回-1;

描述:   用LZ77演算法解壓縮緩衝區compressed中的資料。假定緩衝區包含的資料之前由lz77_compress壓縮。恢復後的資料存入緩衝區original中。lz77_uncompress函式呼叫malloc來動態的為original分配儲存空間。當這塊儲存空間不再使用時,由呼叫者呼叫函式free來釋放空間。

複雜度:O(n)其中n是原始資料中符號的個數。

LZ77的實現與分析

LZ77演算法,通過一個滑動視窗將前向緩衝區中的短語編碼成相應的標記,從而達到壓縮的目的。在解壓縮的過程中,將每個標記解碼成短語或符號本身。要做到這些,必須要不斷地更新視窗,這樣,在壓縮過程中的任何時刻,視窗都能按照規則進行編碼。在本節所有的示例中,原始資料中的一個符號佔一個位元組。

lz77_compress

lz77_compress操作使用LZ77演算法來壓縮資料。首先,它將資料中的符號寫入壓縮資料的緩衝區中,並同時初始化滑動視窗和前向緩衝區。隨後,前向緩衝區將用來載入符號。

壓縮發生在一個迴圈中,迴圈會持續迭代直到處理完所有符號。使用ipos來儲存原始資料中正在處理的當前位元組,並用opos來儲存向壓縮資料緩衝區寫入的當前位。在迴圈的每次迭代中,呼叫compare_win來確定前向緩衝區與滑動視窗中匹配的最長短語。函式compare_win返回最長匹配串的長度。

當找到一個匹配串時,compare_win設定offset為滑動視窗中匹配串的位置,同時設定next為前向緩衝區中匹配串後一位的符號。在這種情況下,向壓縮資料中寫入一個短語標記(如圖3-a)。在本節展示的實現中,對於偏移量offset短語標記需要12位,這是因為滑動視窗的大小為4KB(4096位元組)。此時短語標誌需要5位來表示長度,因為在一個32位元組的前向緩衝區中,不會有匹配串超過這個長度。當沒有找到匹配串時,compare_win返回,並且設定next為前向緩衝區起始處未匹配的符號。在這種情況下,向壓縮資料中寫入一個符號(如圖3-b)。無論向壓縮資料中寫入的是一個短語還是一個符號,在實際寫入標記之前,都需要呼叫網路函式htonl來轉換串,以保證標記是大端格式。這種格式是在實際壓縮資料和解壓縮資料時所要求的。

圖3:LZ77中的短語標記(A)和符號標記(B)的結構

一旦將相應的標記寫入壓縮資料的緩衝區中,就調整滑動視窗和前向緩衝區。要使資料通過滑動視窗,將資料從右邊滑入視窗,從左邊滑出視窗。同樣,在前向緩衝區中也是相同的滑動過程。移動的位元組數與標記中編碼的字元數相等。

lz77_compress的時間複雜度為O(n),其中n是原始資料中符號的個數。這是因為,對於資料中每個n/c個編碼的標記,其中1/c是一個代表編碼效率的常量因素,呼叫一次compare_win。函式compare_win執行一段固定的時間,因為滑動視窗和前向緩衝區的大小均為常數。然而,這些常量比較大,會對lz77_compress的總體執行時間產生較大的影響。所以,lz77_compress的時間複雜度是O(n),但其實際的複雜度會受其常量因子的影響。這就解釋了為什麼在用lz77進行資料壓縮時速度非常慢。

lz77_uncompress

lz77_uncompress操作解壓縮由lz77_compress壓縮的資料。首先,該函式從壓縮資料中讀取字元,並初始化滑動視窗和前向緩衝區。

解壓縮過程在一個迴圈中執行,此迴圈會持續迭代執行直到所有的符號處理完。使用ipos來儲存向壓縮資料中寫入的當前位,並用opos來儲存寫入恢復資料緩衝區中當前位元組。在迴圈的每次迭代過程中,首先從壓縮資料讀取一位來確定要解碼的標記型別。

在解析一個標記時,如果讀取的首位是1,說明遇到了一個短語標記。此時讀取它的每個成員,查詢滑動視窗中的短語,然後將短語寫入恢復資料緩衝區中。當查詢每個短語時,呼叫網路函式ntohl來保證視窗中的偏移量和長度的位元組順序是與作業系統匹配的。這個轉換過程是必要的,因為從壓縮資料中讀取出來的偏移量和長度是大端格式的。在資料被拷貝到滑動視窗之前,前向緩衝區被用做一個臨時轉換區來儲存資料。最後,寫入該標記編碼的匹配的符號。如果讀取的標記的首位是0,說明遇到了一個符號標記。在這種情況下,將該標記編碼的匹配符號寫入恢復資料緩衝區中。

一旦將解碼的資料寫入恢復資料的緩衝區中,就調整滑動視窗。要將資料通過滑動視窗,將資料從右邊滑入視窗,從左邊滑出視窗。移動的位元組數與從標記中解碼的字元數相等。

lz77_uncompress的時間複雜度為O(n),其中n是原始資料中符號的個數。

示例:LZ77的實現檔案

(示例所需要的標頭檔案資訊請查閱前面的文章:資料壓縮的重要組成部分–位操作)

相關文章