LZ词典编码系列算法介绍
相对于Huffman编码和算术编码,LZ词典编码算法具有独特的优越性。它编码简单效率高,不需要预知信源分布,当待编码的信源足够长时可以无限逼近信息熵的极限,达到最佳的编码效率。常见的LZ系列算法有LZ77、LZ78、LZW、LZSS以及其他变种。以下介绍几种常用LZ算法。
2.1.1 LZ编码若干种算法的介绍及评价
Ziv和Lempel在1977年提出的LZ77算法可以说是基于词典编码的第一个具有实用价值的数据压缩算法,曾一直是压缩领域的主流算法。由于待编码的字符串可能包含在已编码的信息结构中,因为整个数据源在待编码的字符串上呈现出冗余,即根据已编码的字符可以构造出后续与之相同的数据。LZ77压缩算法通过使用先前看的正文词典,用词典中的指针即索引号代替正文中的短语来实现压缩,一个文件压缩程序可以使用例如4KB的窗口作为一个词典,当读入新的字符串时,该算法就在前面已经读入的4KB的数据中寻找匹配串,任何匹配都编码成索引号送到输出流。LZ77中的主要数据结构是正文窗口或称工作缓冲区,它分成连个部分,第一部分是最近被编码的一大块正文或称历史表,第二部分和一个超前查看缓冲区,用于存放刚从输入文件中读入但还未编码的字符。该算法试图将超前查看缓冲区的内容与词典中的字符串进行匹配,获得重复字串的起始位置、重复字串索引号和重复的长度[5]。
例如,待编码的数据流如表2.1所示:
表2.1 部分字符流列表
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
C | C | A | B | C | A | B | B | C | A | C |
按照上述编码方式,用表2.2描述LZ77编码过程如表2.2所示。
表2.2 LZ77编码示例
步骤 | 位置 | 匹配串 | 字符 | 输出 |
1 | 1 | -- | C | (0,0) C |
2 | 2 | C | A | (1,1) A |
3 | 4 | -- | B | (0,0) B |
4 | 5 | CAB | B | (3,3) B |
5 | 9 | CA | C | (4,2) C |
以上介绍了LZ77编码算法的流程,鉴于LZ77和LZ78的互补特性,在一些文献中给出了混合二者的LZI算法以及改进算法,实质上也是该法的变种,它比LZ77有更好的压缩比[6]。
LZSS解决了LZ77可能输出空指针和字符重复出现在下一个字符串中的问题。该法相比LZ77有更高的压缩比,且译码同样简单。因此许多文档压缩程序如PKZip, ARJ, LHArc和ZOO等,都是基于LZSS。
LZW类似于LZ78,仅增加了一个术语—前缀根。它也是用词典库查找方法,读入待压缩的数据,与以自适应方式建立的词典库(词典库开始是空的)中的字符串进行比对,如果有匹配则输出该字符串在词典中的索引,否则将字符串插入词典中更新词典。它压缩效率高,实现简单,使用非常广泛,当然也属于无损压缩。
LZ系列的各种算法都是基于自适应方式建立词典,通过查找匹配串并以索引号代替字符串来实现压缩。在解压时只需将索引号换成相应的字符串即可还原出原文。
2.1.2 LZ78词典编码算法描述及评价
前面主要介绍了LZ系列算法中的LZ77、LZSS以及提到LZI和LZW算法。相对于LZ77和LZSS, LZ78编码由于是根据所有已编码的全文进行编码,因此一般来说具有更高的压缩率。LZ78的编码思想是不断地从字符流中提取新的缀-符串,也就是短语,然后用索引号和字符组成的码字表示这个短语,对字符流的编码变成用码字去替换字符流,生成码字流,从而达到压缩数据的目的[7]。
LZ78词典编码的思想大体可以简述如下:设信源符号集
共
个符号,设输入信源符号序列为
编码时将此序列分成不同的段。分段的规则是:尽可能取最少个相连的信源符号,保证各段都不相同。开始时,先取一个符号作为第一段,然后继续分段。若出现与前面相同的符号时,就再取紧跟后面的一个符号一起组成一个段,使之与前面的段不同。这些分段构成字典。当字典达到一定大小之后,再分段时就应查看是否语字典中的短语相同,若有重复就添加符号,以便与字典中的短语不同,直至信源符号序列结束。这样,不同的段内信源符号可看成一个短语,可得到不同段所对应的短语词典表[8]。
编码的码字由段号加后面一个符号组成。设
构成的词典中的短语共有
个,如果用于通信系统,需编为二元码,则段号所需码长
,每个符号需要的码长为
。这里提到的段号也就是所谓的短语索引号。
下面举例来说明这一编码思想的编码过程。设
,信源序列为
,按照分段规则,可以分为
,词典表如表2.3[1]所示。
表2.3 编码词典表示例
段号 | 短语 | 编码 |
1 | 00000 | |
2 | 00001 | |
3 | 00110 | |
4 | 01011 | |
5 | 10010 | |
6 | 00100 | |
7 | 00011 |
由于前面没有出现过的单个符号都是唯一的,因此为简化编码起见,上表中将单符号的码字段号即索引号全设为0。信源符号集共有4个字符,
,因此
可用一个两位的二进制数表示,分别编码为00,01,10,11。上例中的所有信源符号共分成了七段,
,即可用一个3位的二进制数表示段号,7段的段号分别编码为001,010,011,100,101,110,111。如表2.4所示。
表2.4 信源符号编码表
a1 | a2 | a3 | a4 |
00 | 01 | 10 | 11 |
单符号段号全为0,编码过程中,查看每一段中除最后一个字符的字符串在前面哪一段出现过,找到重复段,并用重复段的段号编码和该段的最后一个字符的编码组成该段的编码,例如第5段的
,除末尾的
,在前面第4段出现过,该段的段号编码为100,
的编码为10,因此第5段
的编码为10010。
如信号接收端得到一串信源符号序列为上例的编码结果,即00000 00001 00110 01011 01101 00100 00011,则按单符号的段号为0的原则,前两个5位二进制数分别表示两个单符号,由信源符号编码对照表,分别译码为
。第3个5位二进制数段号001为a1,字符编号10,即a3,故译码结果为
,其他依次类推。
上例的编码和译码中,每段的码长为
,即5个bit。可以看出LZ78编码方法非常便捷,译码也很简单,可以一边译码一边建立词典,只需要传输词典大小,无需传输词典本身,当编码的信源序列较短时,LZ性能似乎会变坏,但是当序列增长时,编码效率会高,平均码长通常会逼近信息熵[9]。
2.2 LZ78编译码算法的设计与理论分析
2.2.1 LZ78算法设计与分析
LZ78编码实质上使用了一种分析算法称为贪婪分析算法。在贪婪分析算法中,每一次分析都要串行地检查来自字符流的字符串,从中分解出已经识别的最长的字符串,也就是已经在词典中出现的最长的前缀。用已知的前缀加上下一个输入字符C也就是当前字符作为该前缀的扩展字符,形成新的扩展字符串即缀-符串。这个新的缀-符串是否要加到词典中,还要看词典中是否存有和它相同的缀-符串。如果有,那么这个缀-符串就变成前缀,继续输入新的字符,否则就把这个缀-符串写到词典中生成一个新的前缀,并赋予一个索引号,即新的短语在词典中的编号[4]。
在编码开始时词典是空的,不包含任何缀-符串。在这种情况下编码器就输出第一个字符C,并把这个字符C添加到词典中作为一个由一个字符组成的缀-符串。在编码过程中,如果出现类似的情况,也照此办理。在词典中已经包含某些缀-符串之后,如果“当前前缀P +当前字符C”已经在词典中,就用字符C来扩展这个前缀,这样的扩展操作一直重复到获得一个在词典中没有的缀-符串为止。此时就输出表示当前前缀P的码字和字符C,并把P+C添加到词典中作为前缀,然后开始处理字符流中的下一个前缀。LZ78编码的具体步骤如下:
1. 在开始时,词典和当前前缀P都是空的。
2. 当前字符C=字符流中的下一个字符。
3. 判断P+C是否在词典中,如果是则用C扩展P,让P= P+C ;如果否则输出与当前前缀P相对应的码字和当前字符C,把字符串P+C 添加到词典中,再令P=空值。
4. 判断字符流中是否还有字符需要编码,是则返回到步骤2;否则输出相应于当前前缀P的码字,然后结束编码[4]。
至此,LZ78编码结束,词典建立完毕。下面通过一个例子来说明这种算法的操作过程。例如,部分待编码的字符串如表2.5所示。
表2.5 部分字符流数据
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
A | B | B | C | B | C | A | B | A |
编码过程如表2.6所示。
表2.6 LZ78编码过程示例
步骤 | 位置 | 词典 | 输出 |
1 | 1 | A | (0,A) |
2 | 2 | B | (0,B) |
3 | 3 | BC | (2,C) |
4 | 5 | BCA | (3,A) |
5 | 8 | BA | (2,A) |
上表中,步骤栏表示编码步骤,位置栏表示在输入数据中的当前位置,词典栏表示添加到词典中的缀-符串,缀-符串的索引等于步骤序号,输出栏以(当前码字W, 当前字符C简化为(W, C)的形式输出。
下面介绍译码算法。
每当从码字流中读入一对码字-字符对时,码字就参考已经在词典中的缀-符串,然后把当前码字的缀-符串和字符输出到字符流,而把添加到词典中。在译码结束之后,重构的词典与编码时生成的词典完全相同。LZ78译码的具体算法如下[4]:
1. 在开始时词典是空的。
2. 当前码字W=码字流中的下一个码字。
3. 当前字符C=紧随码字之后的字符。
4. 把当前码字的缀-符串(string.W)输出到字符流,然后输出字符C。
5. 把string.W+C添加到词典中。
6. 判断码字流中是否还有码字要译,是就返回到步骤2,否则结束。
2.2.2 LZ78算法的理论分析
当编码的信源序列较短时,LZ78编码性能似乎会变坏,但是当序列增长时,编码效率会提高,平均码长通常会逼近信息熵。下面从理论上证明这一点。
设长为
的信源序列
分为
个短语,每段短语的二元码符号长度为
,因此,
编码后总码元长度为
每个源符号的平均码长为
(2.1)
去掉取整符号,有不等式
(2.2)
长度为
的段有
种。若把长为
的信源序列
分为
个码段后,设最长的段的长度为
,而且包含各种长度的所有段型,则所有可能的段型数量
(2.3)
则所有可能的长为L的信源序列的总长为
(2.4)
当
足够大时,
和
近似为
(2.5)
(2.6)
代入式(2.2),得到
(2.7)
设信源为平稳无记忆信源,信源符号的分布概率为
,
,当
足够大时,典型段中
出现的次数接近
个,设这种段型的数目为
种,则
(2.8)
取对数得
(2.9)
若不计较短的段型,则由(2.9),得到
(2.10)
即总段数
,代入式(2.7),得到
(2.11)
当编码的信源序列趋于无穷时,
也趋于无穷,平均码长
趋近于信息熵
[1]。
上面的数学推导利用了概率统计理论的几个公式。从推导结果可以直观的看到,文件中的字符流越长,或者在通信系统发送端的信源序列越长,信源编码可达到的效果就越好,平均码长越短,压缩率越高。因此,LZ编码除了简明高效,理论上也可逼近渐进最佳的编码效果。