LZ77算法是採用字典作數據壓縮的算法,由以色列的兩位大神Jacob Ziv與Abraham Lempel在1977年發表的論文《A Universal Algorithm for Sequential Data Compression》中提出。python
基於統計的數據壓縮編碼,好比Huffman編碼,須要獲得先驗知識——信源的字符頻率,而後進行壓縮。可是在大多數狀況下,這種先驗知識是很難預先得到。所以,設計一種更爲通用的數據壓縮編碼顯得尤其重要。LZ77數據壓縮算法應運而生,其核心思想:利用數據的重複結構信息來進行數據壓縮。舉個簡單的例子,好比git
取之以仁義,守之以仁義者,周也。取之以詐力,守之以詐力者,秦也。github
取之以
、仁義
、,
、者
、守之以
、也
、詐力
、。
均重複出現過,只需指出其以前出現的位置,即可表示這些詞。爲了指明出現位置,咱們定義一個相對位置,如圖算法
相對位置以後的消息串爲取之以詐力,守之以詐力者,秦也。
,若能匹配相對位置以前的消息串,則編碼爲以其匹配的消息串的起始與末端index;若未能匹配上,則以原字符編碼。相對位置以後的消息串可編碼爲:[(1-3),(詐力),(6),(7-9),(詐力),(12),(6),(秦),(15-16)]
,如圖所示:app
上面的例子展現如何利用索引值來表示詞,以達到數據壓縮的目的。LZ77算法的核心思想亦是如此,其具體的壓縮過程不過比上述例子稍顯複雜而已。ide
本文講主要討論LZ77算法如何作壓縮及解壓縮,關於LZ77算法的惟一可譯、無損壓縮(即解壓能夠不丟失地還原信息)的性質,其數學證實參看原論文[1]。編碼
至於如何描述重複結構信息,LZ77算法給出了更爲確切的數學解釋。首先,定義字符串\(S\)的長度爲\(N\),字符串\(S\)的子串\(S_{i,j},\ 1\le i,j \le N\)。對於前綴子串\(S_{1,j}\),記\(L_i^j\)爲首字符\(S_{i}\)的子串與首字符\(S_{j+1}\)的子串最大匹配的長度,即:spa
咱們稱字符串\(S_{j+1,j+l}\)匹配了字符串\(S_{i,i+l-1}\),且匹配長度爲\(l\)。如圖所示,存在兩類狀況:ssr
定義\(p^j\)爲全部狀況下的最長匹配的\(i\)值,即
好比,字符串\(S=00101011\)且\(j=3\),則有
所以,\(p^j = 2\)且最長匹配的長度\(l^j=4\). 從上面的例子中能夠看出:子串\(S_{j+1,j+p}\)是能夠由\(S_{1,j}\)生成,於是稱之爲\(S_{1,j}\)的再生擴展(reproducible extension)。LZ77算法的核心思想便源於此——用歷史出現過的字符串作詞典,編碼將來出現的字符,以達到數據壓縮的目的。在具體實現中,用滑動窗口(Sliding Window)字典存儲歷史字符,Lookahead Buffer存儲待壓縮的字符,Cursor做爲二者之間的分隔,如圖所示:
而且字典與Lookahead Buffer的長度是固定的。
用\((p,l,c)\)表示Lookahead Buffer中字符串的最長匹配結果,其中
壓縮的過程,就是重複輸出\((p,l,c)\),並將Cursor移動至\(l+1\),僞代碼以下:
Repeat: Output (p,l,c), Cursor --> l+1 Until to the end of string
壓縮示例如圖所示:
爲了能保證正確解碼,解壓縮時的滑動窗口長度與壓縮時同樣。在解壓縮,遇到\((p,l,c)\)大體分爲三類狀況:
dict[p:p+l+1]
;for(i = p, k = 0; k < length; i++, k++) out[cursor+k] = dict[i%cursor]
好比,dict=abcd
,編碼爲(2,9,e)
,則解壓縮爲output=abcdcdcdcdcdce。
bitarray的實現請參看A Python LZ77-Compressor,下面給出簡單的python實現。
# coding=utf-8 class LZ77: """ A simplified implementation of LZ77 algorithm """ def __init__(self, window_size): self.window_size = window_size self.buffer_size = 4 def longest_match(self, data, cursor): """ find the longest match between in dictionary and lookahead-buffer """ end_buffer = min(cursor + self.buffer_size, len(data)) p = -1 l = -1 c = '' for j in range(cursor+1, end_buffer+1): start_index = max(0, cursor - self.window_size + 1) substring = data[cursor + 1:j + 1] for i in range(start_index, cursor+1): repetition = len(substring) / (cursor - i + 1) last = len(substring) % (cursor - i + 1) matchedstring = data[i:cursor + 1] * repetition + data[i:i + last] if matchedstring == substring and len(substring) > l: p = cursor - i + 1 l = len(substring) c = data[j+1] # unmatched string between the two if p == -1 and l == -1: return 0, 0, data[cursor + 1] return p, l, c def compress(self, message): """ compress message :return: tuples (p, l, c) """ i = -1 out = [] # the cursor move until it reaches the end of message while i < len(message)-1: (p, l, c) = self.longest_match(message, i) out.append((p, l, c)) i += (l+1) return out def decompress(self, compressed): """ decompress the compressed message :param compressed: tuples (p, l, c) :return: decompressed message """ cursor = -1 out = '' for (p, l, c) in compressed: # the initialization if p == 0 and l == 0: out += c elif p >= l: out += (out[cursor-p+1:cursor+1] + c) # the repetition of dictionary elif p < l: repetition = l / p last = l % p out += (out[cursor-p+1:cursor+1] * repetition + out[cursor-p+1:last] + c) cursor += (l + 1) return out if __name__ == '__main__': compressor = LZ77(6) origin = list('aacaacabcabaaac') pack = compressor.compress(origin) unpack = compressor.decompress(pack) print pack print unpack print unpack == 'aacaacabcabaaac'
[1] Ziv, Jacob, and Abraham Lempel. "A universal algorithm for sequential data compression." IEEE Transactions on information theory 23.3 (1977): 337-343.
[2] guyb, 15-853:Algorithms in the Real World.