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 是一个新字符，未在 w1…wj−1 中出现，或者
- wj 是在 w1…wj 中至少出现两次的最长子串。

这些子串 wj 被称为因子或短语。

根据定义我们可以立即知道w1=x 1。其中x 1 .. n表示x中从第i位到第j位的字符串。xi是字符串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｜$\Longrightarrow$ (0, 0, a )

2｜empty｜a｜a｜c｜a｜a｜c｜a｜b｜c｜a｜b｜$\Longrightarrow$ (1, 1, c )

3｜empty｜a｜a｜c｜a｜a｜c｜a｜b｜c｜a｜b｜a｜a｜$\Longrightarrow$ (3, 4, b )

4｜empty｜a｜a｜c｜a｜a｜c｜a｜b｜c｜a｜b｜a｜a｜a｜c｜end｜$\Longrightarrow$ (3, 3, a )

5｜a｜a｜c｜a｜a｜c｜a｜b｜c｜a｜b｜a｜a｜a｜c｜end｜$\Longrightarrow$ (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），即 ISAi=j 当且仅当 SAj=i。此外，记 lcp(i,j) 为字符串 xSAi..n 和 xSAj..n 的最长公共前缀的长度，其中 n=∣x∣。

计算利用了以下事实：对于字符串 x 中的因子 wj 且满足 ∣wj∣>1，为了确定元组位置 pos，只需考虑文本位置 SANSVISAj 和 SAPSVISAj。其中：

- NSVj=min{i∈{j+1,…,n}∣SAi5，因此输出第四个压缩后的因子Factor(1,1,end)。

最后我们得到字符串x=aaaba的压缩因子为(a,0)(1,2)(b,0)(1,1,end)。

$NSV$ 和 $PSV$ 可以由 $SA$ 数组计算得到:

```pascal
 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压缩算法对任意字符串$x$：

```pascal
 calculate SA, ISA, NSV and PSV for x
 k <- 1
 while k <= n; do
     k <- LZ_Factor(k, PSV, NSV)
 end while
```

## 其它

LZ77算法通过使用编码器或者解码器中已经出现过的相应匹配数据信息，替换当前数据从而实现压缩功能。这个匹配信息使用称为长度-距离对的一对数据进行编码，它等同于“每个给定长度个字符都等于后面特定距离字符位置上的未压缩数据流。”（“距离”有时也称作“偏移”。）

编码器和解码器都必须保存一定数量的最近的数据，如最近2 KB、4 KB或者32 KB的数据。保存这些数据的结构叫作滑动窗口，因为这样所以LZ77有时也称作滑动窗口压缩。编码器需要保存这个数据查找匹配数据，解码器保存这个数据解释编码器所指代的匹配数据。所以编码器可以使用一个比解码器更小的滑动窗口，但是反过来却不行。

许多关于LZ77算法的文档都将长度距离对描述为从滑动窗“复制”数据的命令：“在缓冲区中回退距离个字符然后从那点开始复制长度个字符。”尽管对于习惯于指令式编程的人们来说很直观，但是它仍然使得人们很难理解LZ77编码的一个特点：也就是说，长度距离对中的长度超过距离这样一种情况不仅是可接受的而且还是经常出现的情况。作为一个复制命令，就会让人费解：“回退一个字符然后从那点开始复制七个字符。”但是如果缓冲区中只有一个字符的话那该如何复制七个字符呢？然而将长度距离对看作对于特性的描述就可以避免这种混淆：后面的七个字符与前面的七个完全相同。这就意味着每个字符都可以通过在缓冲区找到确定下来：即使在当前数据对解码开始的时候所要查找的字符并不在缓冲区中也可以。通过这个定义，这样的数据对将是距离字符序列的多次重复，也就是说LZ77成了一个灵活容易的行程长度编码形式。

尽管所有的LZ77算法都是根据同样的基本原理工作，但是如何输出编码后的数据可能会大不一样，例如长度与距离的取值范围是多少以及如何区分长度距离对和字面符号（即直接用字符本身，而不是用长度距离对表示）。下面是一些实例：

- 在Lempel与Ziv最初1977年论文中描述的算法每次将数据输出成三个数值：在缓冲区中找到的最大匹配长度与位置以及紧随这个匹配的字面符号。如果输入流中的两个相邻字符只能编码成字面符号的话，那么长度就是0。
- 在PalmDoc格式中，长度距离对经常用两字节序列进行编码，在这两字节的16位中，11位表示距离，3位表示长度，剩余的两位用来表示第一个字节是这样一个两个字节序列的开头。
- 直到2004年，最流行的基于LZ77的压缩方法是同时使用了LZ77与哈夫曼编码的DEFLATE。字面符号、长度以及用于指示当前数据块结束的符号都放到一个字母表中。距离可以安全地放到一个单独的字母表中，由于距离只会出现在一个长度后面，所以它不可能与其它类型的符号混淆。

## LZ78

LZ77算法针对过去的数据进行处理，而LZ78算法却是针对后来的数据进行处理。LZ78通过对输入缓存数据进行预先扫描与它维护的字典中的数据进行匹配来实现这个功能，在找到字典中不能匹配的数据之前它扫描进所有的数据，这时它将输出数据在字典中的位置、匹配的长度以及找不到匹配的数据，并且将结果数据添加到字典中。

尽管在最初得到流行，但是后来LZ78的普及逐渐衰减，这可能是由于在刚LZ78出现的一些年份，一部分LZ78算法获得了美国专利保护。最流行的LZ78压缩形式是LZW算法，这个算法是泰瑞·卫曲所开发的一个LZ78变体。

## 外部链接

- LZ77算法及变体的库、论文及源代码列表