【數據壓縮】LZ77算法原理及實現


1. 引言

【數據壓縮】LZ77算法原理及實現
【數據壓縮】LZ78算法原理及實現

LZ77算法是采用字典做數據壓縮的算法,由以色列的兩位大神Jacob Ziv與Abraham Lempel在1977年發表的論文《A Universal Algorithm for Sequential Data Compression》中提出。

基於統計的數據壓縮編碼,比如Huffman編碼,需要得到先驗知識——信源的字符頻率,然后進行壓縮。但是在大多數情況下,這種先驗知識是很難預先獲得。因此,設計一種更為通用的數據壓縮編碼顯得尤為重要。LZ77數據壓縮算法應運而生,其核心思想:利用數據的重復結構信息來進行數據壓縮。舉個簡單的例子,比如

取之以仁義,守之以仁義者,周也。取之以詐力,守之以詐力者,秦也。

取之以仁義守之以詐力均重復出現過,只需指出其之前出現的位置,便可表示這些詞。為了指明出現位置,我們定義一個相對位置,如圖

相對位置之后的消息串為取之以詐力,守之以詐力者,秦也。,若能匹配相對位置之前的消息串,則編碼為以其匹配的消息串的起始與末端index;若未能匹配上,則以原字符編碼。相對位置之后的消息串可編碼為:[(1-3),(詐力),(6),(7-9),(詐力),(12),(6),(秦),(15-16)],如圖所示:

上面的例子展示如何利用索引值來表示詞,以達到數據壓縮的目的。LZ77算法的核心思想亦是如此,其具體的壓縮過程不過比上述例子稍顯復雜而已。

2. 原理

本文講主要討論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}\)的子串最大匹配的長度,即:

\[L_i^j = \max \{ l | S_{i,i+l-1} = S_{j+1,j+l} \} \quad \text{subject to} \quad l \le N-j \]

我們稱字符串\(S_{j+1,j+l}\)匹配了字符串\(S_{i,i+l-1}\),且匹配長度為\(l\)。如圖所示,存在兩類情況:

定義\(p^j\)為所有情況下的最長匹配\(i\)值,即

\[p^j = \mathop {\arg \max }\limits_{i} \{ L_i^j \} \quad \text{subject to} \quad 1 \le i \le j \]

比如,字符串\(S=00101011\)\(j=3\),則有

  • \(L_1^j=1\),因為\(S_{j+1,j+1}=S_{1,1}\), \(S_{j+1,j+2} \ne S_{1,2}\);
  • \(L_2^j=4\),因為\(S_{j+1,j+1}=S_{2,2}\), \(S_{j+1,j+2} = S_{2,3}\)\(S_{j+1,j+3} = S_{2,4}\)\(S_{j+1,j+4} = S_{2,5}\)\(S_{j+1,j+5} \ne S_{2,6}\)
  • \(L_3^j = 0\),因為\(S_{j+1,j+1} \ne S_{3,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\)表示最長匹配時,字典中字符開始時的位置(相對於Cursor位置),
  • \(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)\)大致分為三類情況:

  • \(p==0\)\(l==0\),即初始情況,直接解碼\(c\)
  • \(p>=l\),解碼為字典dict[p:p+l+1]
  • \(p<l\),即出現循環編碼,需要從左至右循環拼接,偽代碼如下:
for(i = p, k = 0; k < length; i++, k++)
    out[cursor+k] = dict[i%cursor]

比如,dict=abcd,編碼為(2,9,e),則解壓縮為output=abcdcdcdcdcdce。

3. 實現

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'

4. 參考資料

[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.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM