1. 引言

【数据压缩】LZ77算法原理及实现 【数据压缩】LZ78算法原理及实现

LZ77算法是采用字典做数据压缩的算法,由以色列的两位大神Jacob Ziv与Abraham Lempel在1977年发表的论文《A Universal Algorithm for Sequential Data Compression》中提出。
基于统计的数据压缩编码,比如Huffman编码,需要得到先验知识——信源的字符频率,然后进行压缩。但是在大多数情况下,这种先验知识是很难预先获得。因此,设计一种更为通用的数据压缩编码显得尤为重要。LZ77数据压缩算法应运而生,其核心思想:利用数据的重复结构信息来进行数据压缩。举个简单的例子,比如 Ctrl+Alt+N

取之以仁义,守之以仁义者,周也。取之以诈力,守之以诈力者,秦也。

取之以、仁义、,、者、守之以、也、诈力、。均重复出现过,只需指出其之前出现的位置,便可表示这些词。为了指明出现位置,我们定义一个相对位置,如图
数据压缩 - LZ77算法原理及实现 - 图1
相对位置之后的消息串为 取之以诈力,守之以诈力者,秦也。 ,若能匹配相对位置之前的消息串,则编码为以其匹配的消息串的起始与末端index;若未能匹配上,则以原字符编码。相对位置之后的消息串可编码为:[(1-3),(诈力),(6),(7-9),(诈力),(12),(6),(秦),(15-16)],如图所示:
数据压缩 - LZ77算法原理及实现 - 图2

上面的例子展示如何利用索引值来表示词,以达到数据压缩的目的。LZ77算法的核心思想亦是如此,其具体的压缩过程不过比上述例子稍显复杂而已。

2. 原理

本文讲主要讨论LZ77算法如何做压缩及解压缩,关于LZ77算法的唯一可译、无损压缩(即解压可以不丢失地还原信息)的性质,其数学证明参看原论文[1]。

滑动窗口

至于如何描述重复结构信息,LZ77算法给出了更为确切的数学解释。首先,定义字符串S的长度为N,字符串S的子串数据压缩 - LZ77算法原理及实现 - 图3数据压缩 - LZ77算法原理及实现 - 图4。对于前缀子串数据压缩 - LZ77算法原理及实现 - 图5数据压缩 - LZ77算法原理及实现 - 图6为首字符数据压缩 - LZ77算法原理及实现 - 图7的子串与首字符数据压缩 - LZ77算法原理及实现 - 图8的子串最大匹配的长度,即:

数据压缩 - LZ77算法原理及实现 - 图9

我们称字符串 数据压缩 - LZ77算法原理及实现 - 图10 匹配了字符串 数据压缩 - LZ77算法原理及实现 - 图11,且匹配长度为数据压缩 - LZ77算法原理及实现 - 图12。如图所示,存在两类情况:

数据压缩 - LZ77算法原理及实现 - 图13

定义 数据压缩 - LZ77算法原理及实现 - 图14为所有情况下的最长匹配数据压缩 - LZ77算法原理及实现 - 图15值,即
数据压缩 - LZ77算法原理及实现 - 图16
比如,字符串 数据压缩 - LZ77算法原理及实现 - 图17数据压缩 - LZ77算法原理及实现 - 图18,则有

  • 数据压缩 - LZ77算法原理及实现 - 图19,因为数据压缩 - LZ77算法原理及实现 - 图20
  • 数据压缩 - LZ77算法原理及实现 - 图21,因为数据压缩 - LZ77算法原理及实现 - 图22
  • 数据压缩 - LZ77算法原理及实现 - 图23,因为数据压缩 - LZ77算法原理及实现 - 图24

因此,数据压缩 - LZ77算法原理及实现 - 图25 且最长匹配的长度 数据压缩 - LZ77算法原理及实现 - 图26. 从上面的例子中可以看出:子串 数据压缩 - LZ77算法原理及实现 - 图27 是可以由 数据压缩 - LZ77算法原理及实现 - 图28 生成,因而称之为 数据压缩 - LZ77算法原理及实现 - 图29再生扩展(reproducible extension)。LZ77算法的核心思想便源于此——用历史出现过的字符串做词典,编码未来出现的字符,以达到数据压缩的目的。在具体实现中,用滑动窗口(Sliding Window)字典存储历史字符,Lookahead Buffer存储待压缩的字符,Cursor作为两者之间的分隔,如图所示:

数据压缩 - LZ77算法原理及实现 - 图30

并且字典与Lookahead Buffer的长度是固定的。

压缩

数据压缩 - LZ77算法原理及实现 - 图31 表示Lookahead Buffer中字符串的最长匹配结果,其中

  • 数据压缩 - LZ77算法原理及实现 - 图32 表示最长匹配时,字典中字符开始时的位置(相对于Cursor位置),
  • 数据压缩 - LZ77算法原理及实现 - 图33 为最长匹配字符串的长度,
  • 数据压缩 - LZ77算法原理及实现 - 图34 指Lookahead Buffer最长匹配结束时的下一字符

压缩的过程,就是重复输出 数据压缩 - LZ77算法原理及实现 - 图35,并将Cursor移动至 数据压缩 - LZ77算法原理及实现 - 图36,伪代码如下:

  1. Repeat:
  2. Output (p,l,c),
  3. Cursor --> l+1
  4. Until to the end of string

压缩示例如图所示:

数据压缩 - LZ77算法原理及实现 - 图37

解压缩

为了能保证正确解码,解压缩时的滑动窗口长度与压缩时一样。在解压缩,遇到 数据压缩 - LZ77算法原理及实现 - 图38 大致分为三类情况:

  • 数据压缩 - LZ77算法原理及实现 - 图39数据压缩 - LZ77算法原理及实现 - 图40,即初始情况,直接解码 数据压缩 - LZ77算法原理及实现 - 图41
  • 数据压缩 - LZ77算法原理及实现 - 图42,解码为字典 dict[p:p+l+1];
  • 数据压缩 - LZ77算法原理及实现 - 图43,即出现循环编码,需要从左至右循环拼接,伪代码如下:
  1. for(i = p, k = 0; k < length; i++, k++)
  2. out[cursor+k] = dict[i%cursor]

比如,dict=abcd,编码为(2,9,e),则解压缩为 output=abcdcdcdcdcdce。

3. 实现

bitarray的实现请参看A Python LZ77-Compressor,下面给出简单的python实现。

  1. # coding=utf-8
  2. class LZ77:
  3. """
  4. A simplified implementation of LZ77 algorithm
  5. """
  6. def __init__(self, window_size):
  7. self.window_size = window_size
  8. self.buffer_size = 4
  9. def longest_match(self, data, cursor):
  10. """
  11. find the longest match between in dictionary and lookahead-buffer
  12. """
  13. end_buffer = min(cursor + self.buffer_size, len(data))
  14. p = -1
  15. l = -1
  16. c = ''
  17. for j in range(cursor+1, end_buffer+1):
  18. start_index = max(0, cursor - self.window_size + 1)
  19. substring = data[cursor + 1:j + 1]
  20. for i in range(start_index, cursor+1):
  21. repetition = len(substring) / (cursor - i + 1)
  22. last = len(substring) % (cursor - i + 1)
  23. matchedstring = data[i:cursor + 1] * repetition + data[i:i + last]
  24. if matchedstring == substring and len(substring) > l:
  25. p = cursor - i + 1
  26. l = len(substring)
  27. c = data[j+1]
  28. # unmatched string between the two
  29. if p == -1 and l == -1:
  30. return 0, 0, data[cursor + 1]
  31. return p, l, c
  32. def compress(self, message):
  33. """
  34. compress message
  35. :return: tuples (p, l, c)
  36. """
  37. i = -1
  38. out = []
  39. # the cursor move until it reaches the end of message
  40. while i < len(message)-1:
  41. (p, l, c) = self.longest_match(message, i)
  42. out.append((p, l, c))
  43. i += (l+1)
  44. return out
  45. def decompress(self, compressed):
  46. """
  47. decompress the compressed message
  48. :param compressed: tuples (p, l, c)
  49. :return: decompressed message
  50. """
  51. cursor = -1
  52. out = ''
  53. for (p, l, c) in compressed:
  54. # the initialization
  55. if p == 0 and l == 0:
  56. out += c
  57. elif p >= l:
  58. out += (out[cursor-p+1:cursor+1] + c)
  59. # the repetition of dictionary
  60. elif p < l:
  61. repetition = l / p
  62. last = l % p
  63. out += (out[cursor-p+1:cursor+1] * repetition + out[cursor-p+1:last] + c)
  64. cursor += (l + 1)
  65. return out
  66. if __name__ == '__main__':
  67. compressor = LZ77(6)
  68. origin = list('aacaacabcabaaac')
  69. pack = compressor.compress(origin)
  70. unpack = compressor.decompress(pack)
  71. print pack
  72. print unpack
  73. 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.

原文出处:https://www.cnblogs.com/en-heng/p/4992916.html