LZ77与LZ78
LZ77与LZ78是以色列計算機科學家亞伯拉罕·藍波(Abraham Lempel) 与傑可布·立夫 (Jacob Ziv) 在1977年以及1978年发表之论文中的两个无损数据压缩算法。这两个算法是大多数LZ算法变体如LZW、LZSS以及其它一些压缩算法的基础。与最小冗余编码器或者行程长度编码器不同,这两个都是基于字典的编码器。LZ77最初是带有“滑动窗”(Slide window)的压缩算法,这ω个算法后来证明等同于LZ78中首次出现的显式字典编码技术。
LZ77
编辑定义
编辑LZ77 对字符串 x [1 .. n]的分解是将其拆分并压缩为非空子串 w1,w2,…,wk,满足以下条件:
- x=w1w2…wk
- 对于每个 wj (1 ≤ j ≤ k)存在两种可能:
- wj 是一个新字符,未在 w1…wj−1 中出现,或者
- wj 是在 w1…wj 中至少出现两次的最长子串。
这些子串 wj 被称为因子或短语。
根据定义我们可以立即知道w1=x [1]。其中x [1 .. n]表示x中从第i位到第j位的字符串。x[i]是字符串x [i .. i]的简写。
算法
编辑计算 LZ77 压缩的算法核心思想是将已处理的字符串前缀用作字典。为了限制搜索时间,实际应用中通常限制字典的大小,因此通常使用滑动窗口(sliding window)。滑动窗口既限制字典大小,也限制要处理的文本范围(预览缓冲区)。压缩过程中,字符逐步从预览缓冲区移动到字典中。实际应用中,字典缓冲区通常包含数千个字符,而预览缓冲区则包含约100个字符或更少。
算法输出由三元组组成,可以用来恢复原始文本。对于因子 wj 的三元组形式为 (pos,len,λ):
- pos: wj 在字典中的先前位置(若无则为0)。
- len: 先前出现的长度(若为新字符则为0)。
- λ: 匹配失败的字符,即对于任一 j ≤ k ,λ= [∣w1w2…wj−1∣+len+1]。
使用滑动窗口时,pos 是字典的边界的相对位置,新字符以 (0,0,λ) 表示。此输出格式无需显式存储字典即可重建文本。
Pseudocode LZ77-Compressionsalgorithmus:
while input buffer is not empty do match := longest repeated occurrence of input that begins in window if match exists then pos := distance to start of match len := length of match λ := char following match in input else pos := 0 len := 0 λ := first char of input end if output (pos, len, λ) discard len + 1 chars from front of window s := pop len + 1 chars from front of input append s to back of window repeat
压缩算法举例
编辑我们以字符串 aacaacabcabaaac
为例说明 LZ77 压缩的计算过程。算法采用字典长度为12(紫色区域)和预览缓冲区(黄色区域)长度为10的滑动窗口模型。算法输出的三元组包括:
- (0, 0, a)
- (1, 1, c)
- (3, 4, b)
- (3, 3, a)
- (12, 3, end)
其中参数pos是距离字典右边界的相对距离。
第一个看到的字符未知,因此将第一个 a
编码为 (0, 0, a)
。在第二行,字典中已有 a
(标记为绿色),因此 c
被作为新字符记录。在第三行,LZ77 算法出现特殊情况:匹配的字符串延伸到预览缓冲区。第4行和第5行与前两行类似,但在最后一个三元组中添加了结束标记,因为文本已完全压缩且无后续字符。
row | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | output | |
1 | empty | a | a | c | a | a | c | a | b | c | a | (0, 0, a ) | ||||||||||||
2 | empty | a | a | c | a | a | c | a | b | c | a | b | (1, 1, c ) | |||||||||||
3 | empty | a | a | c | a | a | c | a | b | c | a | b | a | a | (3, 4, b ) | |||||||||
4 | empty | a | a | c | a | a | c | a | b | c | a | b | a | a | a | c | end | (3, 3, a ) | ||||||
5 | a | a | c | a | a | c | a | b | c | a | b | a | a | a | c | end | (12, 3, end) | |||||||
finish |
关键点:
- 数据流从右侧进入滑动窗口。
- 每次移动时,字典的匹配会避免重复编码新字符。
- 当匹配延伸到缓冲区时,算法会添加标记来终止编码。
这种方法通过减少冗余实现更高的压缩效率。
运行时间分析
编辑该算法的运行时间为 Θ(n),因为在压缩过程中,文本只被遍历一次。这在字典和预览缓冲区大小恒定的情况下成立,同时对大型文本进行匹配搜索的开销可以忽略不计。然而,在实际应用中,如果没有额外的数据结构支持,使用普通搜索方式的实现通常较慢。
LZ77解压算法
编辑解压 LZ77 压缩文件比压缩更简单,因为无需进行匹配字符串的搜索。其运行时间为线性 Θ(n),即与原始文本长度相同的步骤数。例如我们解压 x = (0, 0, a) (1, 1, c) (3, 4, b) (3, 3, a) (12, 3, end):
- 如果三元组的第一个值为 0,直接将第三个值追加到文本末尾 -> a
- 第2个三元组中,(1, 1, c) 使用第1行的字符
a
(位置1,长度1),然后追加 c -> aac - 第3个三元组中,重复字符长度超过了字典长度,需循环读取,随后追加 b -> aacaacab
- 第4和第5个三元组中按相同逻辑处理,结束标记被忽略。
解压后的完整文本为:aacaacabcabaaac
。
The pseudocode LZ77 decompression algorithm with a sliding window:
foreach triplet (offset, length, symbol) do
if length > 0 then traverse the previous output backward and output characters until the specified length is reached, restarting at offset in case of overflow; end if output the symbol repeat
优点与缺点
编辑LZ77 的一个主要优点是,它可以在完全不了解文本内容的情况下进行压缩,同时没有专利限制。其继任者 LZ78 需要初始字典来重建文本(通常是包含所有单个符号的简单字典),并且直到2004年部分受专利保护。
但是LZ77 对于小型或非自然语言文本的压缩效果有限,甚至可能增加数据量。例如,原文本有16个字符,而压缩结果为15个字符,仅节省了1个字符。尽管如此,LZ77 作为预处理器,与其他压缩方法(如哈夫曼编码)结合时,效果显著。 解压缩
优化算法:无滑动窗
编辑为了更高效地计算 LZ77 压缩字符串,以下的做法是在完整的字符串上计算分解因子,无需使用滑动窗。
对于字符串的 LZ77 因式分解,可以借助额外的数据结构实现高效的运行时间。对于一个字符串 x,记 SA 为后缀数组(Suffixarray),ISA 为 x 的反向逆后缀数组(inverse Suffixarray),即 ISA[i]=j 当且仅当 SA[j]=i。此外,记 lcp(i,j) 为字符串 x[SA[i]..n] 和 x[SA[j]..n] 的最长公共前缀的长度,其中 n=∣x∣。
计算利用了以下事实:对于字符串 x 中的因子 wj 且满足 ∣wj∣>1,为了确定元组位置 pos,只需考虑文本位置 SA[NSV[ISA[j]]] 和 SA[PSV[ISA[j]]]。其中:
- NSV[j]=min{i∈{j+1,…,n}∣SA[i]<SA[j]} (NSV 表示 下一个较小值)。
- PSV[j]=max{i∈{1,…,j−1}∣SA[i]<SA[j]} (PSV 表示 前一个较小值)。
- 例如数组x=[2,1,3,4], 默认数组x的0位和5位为负无穷。NSV[3]=5,因为x[3]=3, x[4]=4,第三个元素后再无比它更小的数了,只剩下位置5的负无穷。同理,PSV[3]=2。
算法伪代码
编辑算法 LZ_Factor(i, psv, nsv)
返回需要压缩的下一个字符串的起始位置:PSV和NSV都是计算SA数组中对应的最大、最小值位置。
Algorithmus LZ_Factor(k, psv, nsv):
i=ISA[k]
if lcp(k, SA[psv[i]) > lcp(k, SA[nsv[i]]); then
(pos, len) <- (SA[psv[i]], lcp(k, SA[psv[i]))
else
(pos, len) <- (SA[nsv[i]], lcp(k, SA[nsv[i]])
end if
if len = 0; then
pos <- x[k]
end if
if k + len > n; then
print Factor:(pos, len, 'Ende')
else
print Factor:(pos, len, x[k+len])
end if
return k + max(len, 1)
我们首先直观地解释如何计算从位置 k 开始的下一个 LZ 因子:
考虑字符串 x=aaaba 且 k=1。在字符串 x 的后缀数组 SA 中,我们从索引 i=ISA[k] 开始,此处i=ISA[1]=2,我们可以查表得到psv[2]=0,nsv[2]=6。而无论是lcp[i,psv]还是lcp[i,nsv]都是0,因此输出第一个压缩后的因子Factor(a,0)。下一个需要查找的位置为 (k+max(len,1))=2.
i | SA | ISA | x[SA[i]..n] | psv[i] | nsv[i] |
---|---|---|---|---|---|
0 | - infinity | ||||
1 | 5 | 2 | a | 0 | 2 |
2 | 1 | 3 | aaaba | 0 | 6 |
3 | 2 | 4 | aaba | 2 | 6 |
4 | 3 | 5 | aba | 3 | 6 |
5 | 4 | 1 | b | 4 | 6 |
6 | - infinity |
类似地,当k=2时。在字符串 x 的后缀数组 SA 中,我们查询索引 i=ISA[2]=3,我们可以查表得到SA[psv[3]]=SA[2]=1,nsv[3]=6。此时lcp[2,1]=2,lcp[2,6]=0,因此输出第二个压缩后的因子Factor(1,2)。下一个需要查找的位置为 (k+max(len,1))=4.
当k=4时。在字符串 x 的后缀数组 SA 中,我们查询索引 i=ISA[4]=5,我们可以查表得到SA[psv[5]]=SA[4]=3,nsv[5]=6。此时lcp[4,3]=0,lcp[4,6]=0,因此输出第三个压缩后的因子Factor(b,0)。下一个需要查找的位置为 (k+max(len,1))=5.
当k=5时。在字符串 x 的后缀数组 SA 中,我们查询索引 i=ISA[5]=1,我们可以查表得到psv[1]=0,SA[nsv[1]]=SA[2]=1。此时lcp[5,0]=0,lcp[5,1]=1。因为k+len=6>5,因此输出第四个压缩后的因子Factor(1,1,end)。
最后我们得到字符串x=aaaba的压缩因子为(a,0)(1,2)(b,0)(1,1,end)。
和 可以由 数组计算得到:
for i <- 2 to n+1; do
j <- i-1
while defined(j) and SA[i] < SA[j]; do
NSV[j] <- i
j <- PSV[j]
end while
PSV[i] <- j
end for
最后终 LZ77压缩算法对任意字符串 :
calculate SA, ISA, NSV and PSV for x
k <- 1
while k <= n; do
k <- LZ_Factor(k, PSV, NSV)
end while
其它
编辑LZ77算法通过使用编码器或者解TangentGraphic2器中已经出现过的相应匹配数据信息,替换当前数据从而实现压缩功能。这个匹配信息使用称为长度-距离对的一对数据进行编码,它等同于“每个给定长度个字符都等于后面特定距离字符位置上的未压缩数据流。”(“距离”有时也称作“偏移”。)
编码器和解码器都必须保存一定数量的最近的数据,如最近2 KB、4 KB或者32 KB的数据。保存这些数据的结构叫作滑动窗口,因为这样所以LZ77有时也称作滑动窗口压缩。编码器需要保存这个数据查找匹配数据,解码器保存这个数据解释编码器所指代的匹配数据。所以编码器可以使用一个比解码器更小的滑动窗口,但是反过来却不行。
许多关于LZ77算法的文档都将长度距离对描述为从滑动窗“复制”数据的命令:“在缓冲区中回退距离个字符然后从那点开始复制长度个字符。”尽管对于习惯于指令式編程的人们来说很直观,但是它仍然使得人们很难理解LZ77编码的一个特点:也就是说,长度距离对中的长度超过距离这样一种情况不仅是可接受的而且还是经常出现的情况。作为一个复制命令,就会让人费解:“回退一个字符然后从那点开始复制七个字符。”但是如果缓冲区中只有一个字符的话那该如何复制七个字符呢?然而将长度距离对看作对于特性的描述就可以避免这种混淆:后面的七个字符与前面的七个完全相同。这就意味着每个字符都可以通过在缓冲区找到确定下来:即使在当前数据对解码开始的时候所要查找的字符并不在缓冲区中也可以。通过这个定义,这样的数据对将是距离字符序列的多次重复,也就是说LZ77成了一个灵活容易的行程长度编码形式。
尽管所有的LZ77算法都是根据同样的基本原理工作,但是如何输出编码后的数据可能会大不一样,例如长度与距离的取值范围是多少以及如何区分长度距离对和字面符号(即直接用字符本身,而不是用长度距离对表示)。下面是一些实例:
- 在Lempel与Ziv最初1977年论文中描述的算法每次将数据输出成三个数值:在缓冲区中找到的最大匹配长度与位置以及紧随这个匹配的字面符号。如果输入流中的两个相邻字符只能编码成字面符号的话,那么长度就是0。
- 在PalmDoc格式中,长度距离对经常用两字节序列进行编码,在这两字节的16位中,11位表示距离,3位表示长度,剩余的两位用来表示第一个字节是这样一个两个字节序列的开头。
LZ78
编辑LZ77算法针对过去的数据进行处理,而LZ78算法却是针对后来的数据进行处理。LZ78通过对输入缓存数据进行预先扫描与它维护的字典中的数据进行匹配来实现这个功能,在找到字典中不能匹配的数据之前它扫描进所有的数据,这时它将输出数据在字典中的位置、匹配的长度以及找不到匹配的数据,并且将结果数据添加到字典中。
尽管在最初得到流行,但是后来LZ78的普及逐渐衰减,这可能是由于在刚LZ78出现的一些年份,一部分LZ78算法获得了美国专利保护。最流行的LZ78压缩形式是LZW算法,这个算法是泰瑞·衛曲所开发的一个LZ78变体。
参考文献
编辑- Jacob Ziv and Abraham Lempel; A Universal Algorithm for Sequential Data Compression (页面存档备份,存于互联网档案馆),IEEE Transactions on Information Theory, May 1977.
- Anisa Al-Hafeedh et al.: A Comparison of Index-based Lempel-Ziv LZ77 Factorization Algorithms. In: ACM Computing Surveys, Nr. 1, Volume 45, 2012, S. 5:1–5:17
- Juha Kärkkäinen, Dominik Kempa, Simon J. Puglisi: Linear Time Lempel-Ziv Factorization: Simple, Fast, Small. In: CPM 2013, Lecture Notes in Computer Science, Volume 7922, Springer, 2013
- Mohamed I. Abouelhoda, Stefan Kurtz, Enno Ohlebusch: Replacing suffix trees with enhanced suffix arrays. In: Journal of Discrete Algorithms, Nr. 1, Volume 2, 2004, S. 53–86
- Gang Chen, Simon J. Puglisi, William F. Smyth: Fast and Practical Algorithms for Computing All the Runs in a String. In: Combinatorial Pattern Matching, 18th Annual Symposium, CPM 2007, London, Canada, July 9-11, 2007, Proceedings, S. 307–315
- Gang Chen, Simon J. Puglisi, William F. Smyth: Lempel-Ziv Factorization Using Less Time and Space. In: Mathematics in Computer Science, Nr. 4, Volume 1, 2008, S. 605–623
- Maxime Crochemore, Lucian Ilie: Computing Longest Previous Factor in linear time and applications. In: Information Processing Letters, Nr. 2, Volume 106, 2008, S. 75–80