通用数据压缩算法简介
前言
数据压缩技术始终是让我感觉到比较神秘的数学算法之一,而当我接触到其具体的算法时候,发现其原理是如此的简单,所以就写了这篇文件来谈谈自己的感想。但由于本文篇幅有限,就以只以一个最简单的LZ77算法作为例子来讲解。
数据压缩技术其应用十分普遍,WinRar,WinZip等常规数据压缩软件已经成为现在电脑的必备软件了。互连网上到处都可以看到压缩文件包。而常规多媒体文件甚至把压缩算法嵌入到其文件格式的标准内。像现在图形图像方面的jpeg,png,gif,音频mp3,视频VCD,DVD就不用说了。
数据压缩技术的分类
数据压缩算法主要分有损压缩和无损压缩两种。无损压缩就是能够完全还原的压缩算法. 而有损压缩就是不能完全还原的压缩算法。比如典型的mp3音频就是有损压缩算法,虽然它损失了一些本来的音频信息,但是它能极大地提高其压缩比例,而损失的那点信息对整个音乐片段没有多大影响。本文介绍的通用数据压缩算法针对的不是某种具体的音频或者视频信息,而是一种通用的数据信息,我们并不知道什么信息能够损失,什么信息该保留,所以它肯定就是无损压缩了。我们平时在Windows下使用的WinZip,WinRar的压缩就是标准的通用数据压缩,而本文讲解的算法也就是这些算法。
通用数据压缩技术发展
我们在大学课程《离散数学》和《数据结构》里面都学过Huffman树。几乎每本讲Huffman树的书都会提到使用Huffman树构造最小冗余度的前缀编码。Huffman编码就是数据压缩技术的基础。D.A.Huffman在1952发表了他的论文《最小冗余度代码的构造方法》揭开了早期的数据压缩技术形成。早期的数据压缩技术就是基于编码上的优化技术。但是我们都知道这种编码上的优化是要统计数据的出现概率的。但是在处理大文件的时候,统计文件里面的字符概率是件十分麻烦的时候,要消耗很长的计算时间。实际的方法都是采用一种叫做自适应编码的方式,也就是在压缩的时候来不断统计字符的概率。这种方法在压缩开始的时候效果不是很好,但是到了压缩后面,它统计的概率就会越来越接近真实的字符出现的概率。(具体的这类算法我还没有仔细研究过,所以我就不好讲解了。)
1977 年,以色列人 Jacob Ziv 和 Abraham Lempel 发表了论文《顺序数据压缩的一个通用算法》, 1978 年,他们发表了该论文的续篇《通过可变比率编码的独立序列的压缩》。从此,数据压缩技术进入字典型的模式压缩了。字典型的数据压缩十分容易明白,就是尽量不保存重复的信息而已。那两个以色列人发明的算法LZ77,LZ78就是现在流行的Zip压缩算法的前身。1984 年,Terry Welch 发表了名为《高性能数据压缩技术》(A Technique for High-Performance Data Compression)的论文,也就是现在流行的LZW压缩算法的实现。其实LZW压缩算法和LZ77,LZ78的压缩没有多大的区别,只是在实现手段有进一步的优化。现在著名的GIF图片格式中保存内部每个点的颜色的信息就是使用LZW那套算法。LZ77,LZW这种字典型的数据压缩方式压缩比例远远比单纯的从编码上的优化的压缩要高。而且这种压缩算法无论是在压缩还是在解压,执行效率都比以前的编码优化压缩要高得多!
Unix 上当时使用LZW的压缩程序Compress几乎成为Unix上压缩程序的标准。而同时在MS-DOS上也有一个同样的叫ARC程序的压缩程序。80 年代中期以后,人们对 LZ77 进行了改进,随之诞生了一批我们今天还在大量使用的压缩程序。Haruyasu Yoshizaki(Yoshi) 的 LHarc 和 Robert Jung 的 ARJ 是其中两个著名的例子,还有到今天都已经成为压缩标准的Zip压缩。
最小长度的Huffman编码
下面我们开始进入数据压缩的正题。
几乎每篇讲解数据压缩的文章都会把“信息熵”摆出来讲解。但是本文篇幅有限,不打算讲解了。因为一旦把信息熵讲解出来,那么以后的压缩都会信息熵联系在一起了,挺麻烦的。我们直接进入数据的编码部分。不要以为只有早期的编码优化的压缩才涉及到数据的编码,其实现在Zip,Rar压缩内部也使用了优秀的数据编码方式,来缩小信息的冗长。
首先我们得知道如果设计一个变长的编码。通常的方式就是使用前缀码方式。所谓前缀码就是把编码的特征信息表现在编码的最前面。下面来看个例子。
有三个信息A,B,C,他们的二进制码分别是0,1,01,如果我们收到一段信息是01010,解码时候我们怎么区分他们是CCA还是ABABA,或者是ABCA呢?如果使用前缀码,我们就可以避免这种冲突。下面使用前缀码给A,B,C编码。
A: 0 B: 10 C: 110
那么01010 就只能被翻译成ABB.所以这中变长的编码是不会有冲突的。可是下面又有一个问题了,既然0,10,110,1110…这中前缀编码的任意组合都不会产生解码的冲突,如果安排A,B,C的编码能够使A,B,C表示的信息最短?这个问题就是前面提到的Huffman提出的最小编码的构造方法.
Huffman编码就是一种最优的前缀编码(也是最小的编码)。它实现的基础是在已经知道每个字符出现的比例或者概率。比如知道A,B,C,D字符出现的个数分别为12,3,4,5 .那么构造其Huffman二叉树。
root |
A |
D
|
C |
B |
0
|
1
|
0
|
1
|
0
|
1
|
(本文在这里只是简单地提一下Huffman编码,具体的构造方法和其原理请大家参考离散数学相关书籍。)根据构造的Huffman二叉树:
A的编码:0 B的编码:111 C的编码:110 D的编码:10
那么A 就只用了1个bit,B用了3个,C用了3个,D用了2个.那么这段信息总共用了12*1+3*3+4*3+5*2 = 43个bit.而安装一般的2-10进制编码,那么A,B,C,D每个都需要使用2个bit才行,总共就使用了12*2+3*2+3*2+5*2= 46个bit.
其实这中前缀编码无非就是用短的码来表示出现概率高的字符号,这个著名的电报莫斯编码是同一个道理。电报中的莫斯编码就是把英文中最常出现的字母e编成最短的编码。
中学数学里面讲过排序不等式: 顺序和 >= 乱序和 >= 反序和.而这里的编码为了获得“最小”,所以采用了“反序”的构造方式。
可以肯定的是,无论你采用何种编码,能够不冲突的编码方式中,使用Huffman编码方式编码出来的信息的长度是最小的。
除了Huffman编码以外,还有种更优秀的算术编码,不过其原理有点复杂,本文也不打算讲解了。下面我们就来看著名的LZ77算法。
LZ77字典压缩算法的原理
容易想到的字典压缩算法就是构造一本实际的字典。比如说一篇英文文章,其实我们只要保存每个单词在一本英语字典中出现的页数和个数就可以了。页数用两个字节,个数用1 个字节,那么一个单词平均使用了3个字节来保存,而我们知道一般的英语单词长度都在3个以上的。由此,我们的这篇英文文章实现了数据的压缩。但是实际的通用数据压缩算法却不能这么做,因为我们在解压的时候需要一本上千页的英语单词字典,而这部分的信息首先是压缩程序不可预知的,同时它又不能保存在压缩信息里面(英语字典比我们的英文文章还厚)。我们这里介绍的LZ77压缩算法就是使用的动态创建字典方法。也就是说,字典信息就是前面压缩信息本身。
前面已经说过,LZ77算法主要就是避免重复的长信息出现。比如说下面一串字符号
AABAABAAB,我们发祥地里面有三个重复出现的AAB,但是我们只要保存一个AAB就可以了,然后其它两个出现AAB的地方只需要把第一个出现AAB的位置和长度存储下来就可以了。那么我们保存后面两个AAB就只需要一个二元数组<匹配串的相对位置,匹配长度>。解压的时候,我们根据匹配串的相对位置,向前找到第一个AAB的位移,然后再根据匹配长度,直接把第一个AAB复制到当前解压缓冲里面就可以了。如果压缩时找不到之前相同的信息(也就是可匹配的信息),那么我们就直接输出这段信息到压缩缓冲里面,然后移下下段要压缩的信息。
好了,让我们来看看一个实际的例子,字符串AABAABCDDABC
步骤
|
当前输入缓冲
|
当前内存中的字典
|
输出信息
|
1. |
AABAABCDDABC |
空 |
空 |
2. |
ABAABCDDABC |
A |
A |
3. |
BAABCDDABC |
AA |
<0,1>
|
4. |
AABCDDABC |
AAB |
B |
5. |
CDDABC |
AABAAB |
<0,3>
|
6. |
DDABC |
AABAABC |
C |
7. |
DABC |
AABAABCD |
D |
8. |
ABC |
AABAABCDD |
<7,1>
|
9. |
空 |
AABAABCDDABC |
<4,3>
|
|
|
|
|
这里有个麻烦的问题,在解压的时候如何区分一段信息是源数据信息还是一个匹配信息?如果是源数据信息,那么我们直接复制到解压缓冲区,如果是匹配信息,我们得从当前解压缓冲位置向前查找重复的匹配信息。通行的解决办法就是多使用1 个位(bit)来区分压缩信息里面的源数据信息和匹配信息。大家可能会担心,我们这里引进了新的信息进来,会不会让压缩文件压得反而比源文件还大?但是实践已经证明了,对于一般的未压缩的数据信息,那多引进的一个bit的标记信息并不会有多大的影响。不过同时重复压缩一个文件后,文件确实不仅不会压小,还会反而压得比以前大。
记录重复信息的匹配信息包含两项, 一个是匹配的位置,一个是匹配的长度.如何分配这两个信息的保存是个关键的问题.匹配的位置不能是绝对任意的位置,因为有些文件长度高达几MB,甚至上百 MB,那么仅仅保存一个匹配位置的信息就要好几个字节.实际的安排方案是用两个字节来保存一个匹配信息.12位来表示匹配位置,4位来表示匹配长度.那么可寻找匹配
位置长度可以达到4096, 而4位表示的匹配长度可以达到16个字节。而针对匹配信息的寻找也找匹配长度至少为3个字节的,因为如果匹配长度小于3 个字节,那么仅仅保存匹配信息的字节数就高于压缩缩掉的字节数,就得不偿失了。实际的很多压缩软件都是这样分配的,我也自己测试过,这种分配方案的确是最佳的.
那么这固定长度的匹配寻找区间就是很多人所说的字典窗口.
当前分析位置
|
要压缩的源数据
|
用来查找匹配的字典信息
4096个字节长度
|
在当前分析位置提取要压缩的数据信息,在源数据缓冲中当前分析位置之前固定长度的信息区域去找可以匹配的最长匹配串。如果找到后,当前分析位置移动匹配长度个字节,然后字典窗口也移动匹配个长度字节。这样,分析指针不断向后移动,字典窗口也不断向后移动,直到分析指针移动到源数据缓冲结尾。