LZ77

  1. Lossless data compression algorithm
  2. Published in 1977 by Abraham Lempel and Jacob Ziv
  3. Variations: LZW, LZSS, LZMA and others

核心概念

LZ77持有在未压缩数据流出现过的数据的单一拷贝,并且将重复出现的的同样数据替换成对这一份数据的引用,以 实现数据压缩的目的。

匹配 每一个匹配都被编码成(length, distance)对,即长度为length的字符串中的每个字符与相同字符在未压缩的位置相差distance个字符。:question:

滑动窗口 编码器追踪指定数量的最近数据,数量为2KB4KB32KB

伪代码

pseudocode
while input is not empty do
    prefix := longest prefix of input that begins in window

    if prefix exists then
        i := distance to start of prefix
        l := length of prefix
        c := char following prefix in input
    else
        i := 0
        l := 0
        c := first char of input
    end if

    output (i, l, c)

    s := pop l+1 chars from front of input
    discard l+1 chars from front of window
    append s to back of window
repeat

论文

压缩算法

字符串解析成长度不超过指定长度$L_s$的子串或者单词,编码框架将其映射为固定长度$L_c$的唯一可解密的编码。

二者之间的关系为:$L_c = 1 + \lceil log(n - L_s) \rceil + \lceil logL_s \rceil$(1),其中对数的基底是字符集A的基$Card(A) = \alpha$

概念和定义

字符集:A = {0, 1, $\cdots$ , $\alpha$ - 1}

字符串长度:$l(S) = k$, 表示k元有序对$S = s_1s_2\cdots s_k$

子串:$S(i, j)$,当$i \leq j$时,$S(i, j) = s_is_{i+1}\cdots s_j$,当$i \gt j$时,$S(i, j) = NULL$

字符串结合:$S = QR$ ,且$l(S) = l(Q) + l(R)$

(真)前缀:$S(1, j)$叫做字符串S的前缀,如果$j \lt l(S)$,那么$S(1, j)$叫做真前缀。$0 \leq j \leq l(S)$

给定真前缀$S(1, j)$和整数$i \leq j$, $L(i)$ 表示满足$l \leq l(S) - j$的最大非负整数:$S(i, i + l - 1) = S(j + 1, j + l)$,并且让p是$S(1, j)$是其中一个位置满足$L(p) = max_{1\leq i \leq j}\{L(i)\}$

那么,子串$S(j+1, j+L(p))$就被称为字符串S中$S(1, j)$的可重现延伸

现在,为了描述编码过程,令$S = s_1s_2\cdots$表示源发出的字符串。S的序列化编码使得S解析成连续的源单词,$S=S_1S_2\cdots$,并且为每个$S_i$赋值一个编码$C_i$。对于有界延迟编码(bounded-delay encoding),每个$S_i$的长度$l_i$最多等于已确定的参数$L_s$,而每个编码$C_i$的长度为固定长度$L_c$。

为了初始化编码过程,我们假定源的输出S前的字符串Z是$n - L_s$个零,并且我们将$B_1 = ZS(1, L_s)$存储在buffer中。如果$S(1, j)$是字符串$ZS(1, L_s - 1)$中$Z$的可重现延伸,那么$S_1 = S(1, j+1)$以及$l_1 = j + 1$。为了确定下一个源词,我们把前$l_1$个符号从缓存中移出,并且填入下$l_1$个S的符号得到字符串$B_2 = B_1(l_1+1, n)S(L_s+1, L_s+l_1)$。现在我们寻找$B_2(1, n-1)$中$B_2(1, n-L_s)$的可重现延伸E。并且令$S_2 = Es$,此处$s$是在$B_2$中紧接E的符号。总之,如果$B_i$表示buffer中的n个源符号的字符串当我们准备好确定第$ith$个源词$S_i$,连续的编码步骤可以形式化表达如下:

  1. 设置$B_1 = 0^{n-L_s}S(1, L_s)$,也就是长度为$n-L_s$的全零字符串后面跟着S的前$L_s$个字符
  2. 已经确定了$B_i, i \leq 1$。设$S_i = B_i(n - L_s+1, n-L_s+l_i)$,而$S_i$的长度为$l_i - 1$的前缀是$B_i(1, n-1)$中$B_i(1, n - L_s)$的可重现扩展
  3. 如果$p_i$是用于确定$S_i$的可重现指针,那么$S_i$的编码$C_i$由$C_i = C_{i1}C_{i2}C_{i3}$给出,其中$C_{i1}$是$p_i - 1$和$l(C_{i1}) = \lceil log(n-L_s)\rceil$的$radix-\alpha$表示,$C_{i2}$是$l_i - 1$和$l(C_{i2}) = \lceil logL_s\rceil$的$radix-\alpha$表示,$C_{i3}$是$S_i$的最后的字符,即占据了$B_i$中的$n-L_s + l_i$位置。$C_i$的全部长度由下式给出:$l(C_i) = \lceil log(n - L_s)\rceil + \lceil logL_s\rceil + 1$
  4. 为了更新buffer中的内容,移出buffer中前$l_i$个字符,同时从源中取出下$l_i$个字符填充进去,得到$B_{i+1} = B_i(l_i + 1, n)S(h_i+1, h_i+l_i)$,$h_i$表示$B_i$最后的字符在S中占据的位置。

以上完整地描述了编码过程。很容易验证由(2)定义的解析法则在每次迭代中确保了一个有界,正的源词长度。实际上,$1 \leq l_i \leq L_s$对于每个$i$都成立,因此考虑到$l_i - 1$和来自于A的$(\lceil log(L_s)\rceil$个字符的$radix-\alpha$表示。因为$1 \leq p_i \leq n -L_s$对于每个$i$都成立,表示$p_i-1$和来自A的$\lceil log(n - L_s)\rceil$个字符是可能的。

解码过程只要将编码过程逆过来即可。我们采用长度为$n - L_s$的buffer来存储最近的解码字符。最开始,buffer中填充$n-L_s$个零字符。如果$D_i = d_1d_2\cdots d_{n-L_s}$表示buffer中的内容在$C_{i-1}$已经被解码成$S_i - 1$,然后

$,S_{i-1} = D_i(n - L_s - l_{i-1} + 1, n - L_s),\quad l_{i-1} = l(s_{i-1})$

同时,$D_{i+1}$可以根据$D_i$和$C_i$获取,具体方式如下:

可以根据第一个$\lceil log(n - L_s)\rceil$和$C_i$的下$\lceil logL_s\rceil$个字符确定$p_i - 1$和$l_i - 1$。然后,在填充$p_i$到$n-L_s$的同时进行$l_i-1$次位移操作。第一次移位操作会将缓存内容由$D_i$变成:

$D_{i}^{(1)} = d_2d_3\cdots d_{n-L_s}d_{p_i}= d_{1}^{(1)}d_{2}^{(1)}\cdots d_{n-L_s}^{(1)}$

类似地,如果$j \lt l(S_i - 1)$,第j个移位将$D_{i}^{(j-1)} = d_1^{(j-1)}d_2^{(j-1)}\cdots d_{n-L_s}^{(j-1)}$转换为$D_{i}^{(j)} = d_1^{(j)}d_2^{(j)}\cdots d_{n-L_s}^{(j)}$,完成$l_i - 1$次移位操作之后,再移位一次,把$C_i$的最后一个字符移入buffer的位置$n-L_s$。很容易验证buffer在最后的$l_i = l(S_i)$个位置上包含$S_i$。

示例

$\alpha = 3$

$S = 001010210210212021021200\cdots$

$L_s = 9$

$n = 18$

根据(1)得到$L_c = 1 + log_{3}(18 - 9) + log_{3}9 = 5$

最开始buffer填充$n - L_s = 9$个零值,紧接着是S的前$L_s = 9$个字符,即 $$ B_1 = \underbrace{000000000}_{n - L_s = 9} + \underbrace{001010210}_{L_s = 9} $$

为了确定第一个源词$S_1$,我们必须找到$B_1(10, 17) = 00101021$的最长前缀$B_1(10, 9+l_1-1)$。匹配$B_1$中起始位置$p_1 \leq 9$的子串然后设$S_1 = B_1(10, 9+l_1)$。易得$S_1 = 001,\ l_1 = 3$。指针$p_1$可以是1-9之间的任何一个数。令$p_1 = 9$。以3为基数的$p_1 - 1 = 8$两位数表示为$C_{11} = 22$,$C_{12} = 02$,$C_{i3}$是$S_i$的最后一个字符,即$C_{13} = 1$。由此得出$C_1 = 22021$。

为了得到buffer内容$B_2$,将前$l_1 = 3$个字符移出并填入下3个字符$S(10, 12) = 210$。第2,3和4步的细节展示如下: $$ B_2 = 000000000\overset{\downarrow}{0}1010210, \quad C_2 = 21102 \\ B_3 = 000010\overset{\downarrow}{1}02102102120, \quad C_3 = 20212\\ B_4 = 21\overset{\downarrow}{0}210212021021200, \quad C_4 = 02220 $$