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的實現文件
(示例所需要的頭文件信息請查閱前面的文章:數據壓縮的重要組成部分--位操作)
/*lz77.c*/ #include <netinet/in.h> #include <stdlib.h> #include <string.h> #include "bit.h" #include "compress.h" /*compare_win 確定前向緩沖區中與滑動窗口中匹配的最長短語*/ static int compare_win(const unsigned char *window, const unsigned char *buffer, int *offset, unsigned char *next) { int match,longest,i,j,k; /*初始化偏移量*/ *offset = 0; /*如果沒有找到匹配,准備在前向緩沖區中返回0和下一個字符*/ longest = 0; *next = buffer[0]; /*在前向緩沖區和滑動窗口中尋找最佳匹配*/ for(k=0; k<LZ77_WINDOW_SIZE; k++) { i = k; j = 0; match = 0; /*確定滑動窗口中k個偏移量匹配的符號數*/ while(i<LZ77_WINDOW_SIZE && j<LZ77_BUFFER_SIZE - 1) { if(window[i] != buffer[j]) break; match++; i++; j++; } /*跟蹤最佳匹配的偏移、長度和下一個符號*/ if(match > longest) { *offset = k; longest = match; *next = buffer[j]; } } return longest; } /*lz77_compress 使用lz77算法壓縮數據*/ int lz77_compress(const unsigned char *original,unsigned char **compressed,int size) { unsigned char window[LZ77_WINDOW_SIZE], buffer[LZ77_BUFFER_SIZE], *comp, *temp, next; int offset, length, remaining, hsize, ipos, opos, tpos, i; /*使指向壓縮數據的指針暫時無效*/ *compressed = NULL; /*寫入頭部信息*/ hsize = sizeof(int); if((comp = (unsigned char *)malloc(hsize)) == NULL) return -1; memcpy(comp,&size,sizeof(int)); /*初始化滑動窗口和前向緩沖區(用0填充)*/ memset(window, 0 , LZ77_WINDOW_SIZE); memset(buffer, 0 , LZ77_BUFFER_SIZE); /*加載前向緩沖區*/ ipos = 0; for(i=0; i<LZ77_BUFFER_SIZE && ipos < size; i++) { buffer[i] = original[ipos]; ipos++; } /*壓縮數據*/ opos = hsize * 8; remaining = size; while(remaining > 0) { if((length = compare_win(window,buffer,&offset,&next)) != 0) { /*編碼短語標記*/ token = 0x00000001 << (LZ77_PHRASE_BITS - 1); /*設置在滑動窗口找到匹配的偏移量*/ token = token | (offset << (LZ77_PHRASE_BITS - LZ77_TYPE_BITS - LZ77_WINOFF_BITS)); /*設置匹配串的長度*/ token = token | (length << (LZ77_PHRASE_BITS - LZ77_TYPE_BITS - LZ77_WINOFF_BITS - LZ77_BUFLEN_BITS)); /*設置前向緩沖區中匹配串后面緊鄰的字符*/ token = token | next; /*設置標記的位數*/ tbits = LZ77_PHRASE_BITS; } else { /*編碼一個字符標記*/ token = 0x00000000; /*設置未匹配的字符*/ token = token | next; /*設置標記的位數*/ tbits = LZ77_SYMBOL_BITS; } /*確定標記是大端格式*/ token = htonl(token); /*將標記寫入壓縮緩沖區*/ for(i=0; i<tbits; i++) { if(opos % 8 == 0) { /*為壓縮緩沖區分配臨時空間*/ if((temp = (unsigned char *)realloc(comp,(opos / 8) + 1)) == NULL) { free(comp); return -1; } comp = temp; } tpos = (sizeof(unsigned long ) * 8) - tbits + i; bit_set(comp,opos,bit_get((unsigned char *)&token,tpos)); opos++; } /*調整短語長度*/ length++; /*從前向緩沖區中拷貝數據到滑動窗口中*/ memmove(&window[0],&window[length],LZ77_WINDOW_SIZE - length); memmove(&window[LZ77_WINDOW_SIZE - length],&buffer[0],length); memmove(&buffer[0],&buffer[length],LZ77_BUFFER_SIZE - length); /*向前向緩沖區中讀取更多數據*/ for(i = LZ77_BUFFER_SIZE - length; i<LZ77_BUFFER_SIZE && ipos <size; i++) { buffer[i] = original[ipos]; ipos++; } /*調整剩余未匹配的長度*/ remaining = remaining - length; } /*指向壓縮數據緩沖區*/ *compressed = comp; /*返回壓縮數據中的字節數*/ return ((opos - 1) / 8) + 1; } /*lz77_uncompress 解壓縮由lz77_compress壓縮的數據*/ int lz77_uncompress(const unsigned char *compressed,unsigned char **original) { unsigned char window[LZ77_WINDOW_SIZE], buffer[LZ77_BUFFER_SIZE] *orig, *temp, next; int offset, length, remaining, hsize, size, ipos, opos, tpos, state, i; /*使指向原始數據的指針暫時無效*/ *original = orig = NULL; /*獲取頭部信息*/ hsize = sizeof(int); memcpy(&size,compressed,sizeof(int)); /*初始化滑動窗口和前向緩沖區*/ memset(window, 0, LZ77_WINDOW_SIZE); memset(buffer, 0, LZ77_BUFFER_SIZE); /*解壓縮數據*/ ipos = hsize * 8; opos = 0; remaining = size; while(remaining > 0) { /*獲取壓縮數據中的下一位*/ state = bit_get(compressed,ipos); ipos++; if(state == 1) { /*處理的是短語標記*/ memset(&offset, 0, sizeof(int)); for(i=0; i<LZ77_WINOFF_BITS; i++) { tpos = (sizeof(int)*8) - LZ77_WINOFF_BITS + i; bit_set((unsigned char *)&offset, tpos, bit_get(compressed,ipos)); ipos++; } memset(&length, 0, sizeof(int)); for(i=0; i<LZ77_BUFLEN_BITS; i++) { tpos = (sizeof(int)*8) - LZ77_BUFLEN_BITS + i; bit_set((unsigned char *)&length, tpos, bit_get(compressed,ipos)); ipos++; } next = 0x00; for(i=0; i<LZ77_NEXT_BITS; i++) { tpos = (sizeof(unsigned char)*8) - LZ77_NEXT_BITS + i; bit_set((unsigned char *)&next, tpos, bit_get(compressed,ipos)); ipos++; } /*確保偏移和長度對系統有正確的字節排序*/ offset = ntohl(offset); length = ntohl(length); /*將短語從滑動窗口寫入原始數據緩沖區*/ i=0; if(opos>0) { if((temp = (unsigned char *)realloc(orig,opos+length+1)) == NULL) { free(orig); return 1; } orig = temp; } else { if((orig = (unsigned char *)malloc(length+1)) == NULL) return -1; } while(i<length && remaining>0) { orig[opos] = window[offset + i]; opos++; /*在前向緩沖區中記錄每個符號,直到准備更新滑動窗口*/ buffer[i] = window[offset + i]; i++; /*調整剩余符號總數*/ remaining --; } /*將不匹配的符號寫入原始數據緩沖區*/ if(remaining > 0) { orig[opos] = next; opos++; /*仍需在前向緩沖區中記錄此符號*/ buffer[i] = next; /*調整剩余字符總數*/ remaining--; } /*調整短語長度*/ length++; } else { /*處理的是字符標記*/ next = 0x00; for(i=0; i<LZ77_NEXT_BITS; i++) { tpos = (sizeof(unsigned char)*8) - LZ77_NEXT_BITS + i; bit_get((unsigned char *)&next, tpos,bit_get(compressed,ipos)); ipos++; } /*將字符寫入原始數據緩沖區*/ if(opos > 0) { if((temp = (unsigned char*)realloc(orig,opos+1)) == NULL) { free(orig); return -1; } orig = temp; } else { if((orig = (unsigned char *)malloc(1)) == NULL) return -1; } orig[opos] = next; opos++; /*在前向緩沖區中記錄當前字符*/ if(remaining > 0) buffer[0] = next; /*調整剩余數量*/ remaining--; /*設置短語長度為1*/ length = 1; } /*復制前向緩沖中的數據到滑動窗口*/ memmove(&window[0], &window[length],LZ7_WINDOW_BITS - length); memmove(&window[LZ77_WINDOW_SIZE - length], &buffer[0], length); } /*指向原始數據緩沖區*/ *original = orig; /*返回解壓縮的原始數據中的字節數*/ return opos; }