原文链接:http://blog.inkernel.info/archives/13.html
LZ77字典压缩算法简介
字典压缩的原理是构建一个字典,用索引来代替重复出现的字符或字符串。如果字符串相对长,那么对整个字符串构建字典,这个字典将会很大,并且随着字典的增大,匹配速度也会快速下降。原始的LZ77算法是利用了字符串中上下文的相关性特点,通过一个滑动窗口(一个查找缓冲区)来作为字典,对要压缩的字符串保留一个look-aheadbuffer。压缩后的字符串采用三元组来表示:<位移,长度,下一个字符>,在滑动窗口中从后往前找,如果在窗口中有曾经出现过的相同字符,看最多可以匹配多少字节,完了继续往前查找,查找完了取窗口中最长的匹配串(如果有多个相同长度的串可以匹配,取最后一个),将这个匹配串距当前位置的位移,长度,及下一个字符构成的三元组写出。如果在滑动窗口找不到匹配串,那么位移=长度=0,加上不能压缩的字符一起输出。滑动窗口可以通过循环队列实现。
解压缩是一个比较简单而且快速的过程,在需要解压缩时通过位移和长度拷贝之前出现过的字符串就可以完成。
LZ77字典压缩的改进
LZ77压缩算法的大部分时间都会花在字符串匹配上,针对这一点有多种改进:
- 构建查找树,该思路也有多种实现方法,如LZSS。
- 构建链表法实现的哈希表,通过对一个字符串的头三个字节做哈希,快速找到匹配字符串的位置之后再沿链表做顺序搜索,如LZRW。
Deflate算法,其在ZIP和GZIP,以及许多软件中使用到,是使用LZ77的变种和huffman编码结合的一种压缩算法。在ZLib的源码中可以找到详细的算法说明。
类似的还有鼎鼎有名的7-ZIP所使用的LZMA算法,也是LZ77算法和算术编码结合的算法,对LZ77的三元组都根据概率再次进行编码,压缩比很高, 但是压缩速度相对较慢。
LZ78/LZW系列算法
LZ77算法针对过去的数据进行处理,而 LZ78 算法却是针对后来的数据进行处理。LZ78通过对输入缓存数据进行预先扫描与它维护的字典中的数据进行匹配来实现这个功能,在找到字典中不能匹配的数据之前它扫描进所有的数据,这时它将输出数据在字典中的位置、匹配的长度以及找不到匹配的数据,并且将结果数据添加到字典中。
比较好的字典数据结构表示是字符查找树(trie,有多种译法),即上一楼提到的解析树(ParseTree),在这个数据结构中,前缀相同的字符串共享祖先节点,依此衍生。因为要压缩的字符串可能会很长,在LZ78中新出现的symbol并且在字典中没有匹配时,会加入到字典中,字典会不断膨胀,需要限制字典的大小。这里对trie有几种处理方法:
- 停止继续构造字典,形成一个静态的字典,用现有的字典对数据进行压缩(这个处理方法的缺点就是前面的字符串构造出来的字典对后来的数据不一定有效)
- 重新开始构造字典。这样会使数据分离为一个一个的块,每个块都有独立的字典。(这个方法保留了LZ77的优点,即新数据比旧数据更有相关性,类似Oracle的块压缩)
- 从trie中替换节点。但是需要考虑有效的替换算法。
LZW算法是LZ78的变种,假如字符串S在字典中有匹配,但是Sx在字典中无匹配,那么Sx会被加入到字典中。因为字典一开始就被初始化为256个字符,所以无需用到LZ77中的三元组表示法,每一个phrase或char都可以用字典中的一个索引表示(总是能在字典中找到),缺点跟LZ78一样,字典会快速增大。LZW中的字典使用了trie结构。
LZMW是LZW的一个改进版本(DB2参考的压缩算法),改进如下:
- 如果trie满了,应该找到最近最少使用的节点进行替换。一种方法是:找出所有没有子节点的节点(即没有被当做前缀引用的节点),删除其中最老的。
- 加入字典的phrase由两个子串组成:前匹配项与当前项。LZW中的trie的新增节点表示的是后续不能匹配的symbol,但是LZMW中trie新增节点表示的是后续不能匹配的串。这可以使得字典的单元在逻辑上更自然,生成字典的速度也更快。缺点是:
- 并不是所有在字典中存在的串其前缀都可以在字典中找到(注:LZW的trie结构是可以的),那么假如一个只部分匹配字典项的串要加入字典中会是个问题,《Data compression, the completereference》这本书中提到的可能的替代方法是一个串加入trie中时,其所有的前缀也加入到trie中,并且每个节点必须有标志位注明其是否在字典中(因为节点有可能只是上一步提到的前缀节点,而不是实际的字典项)。
- 查找最长匹配项可能需要回溯。