参考资料:
1.gzip压缩系列
2.ZIP压缩算法详细分析及解压实例解释
3.An Explanation of the Deflate Algorithm
4.RFC1951
1.deflate压缩
Deflate是一种数据无损压缩算法,它广泛用于zip文件压缩以及png图片压缩。deflate结合了huffman编码和LZ77编码,流程如下:
- 1.LZ77算法,利用相邻数据的相关性对原始数据压缩,该模块输入为原始数据,输出为literal、distance-length数据对;
- 2.huffman编码,对LZ77的结果分别进行数据统计生成huffman表,再对数据进行huffman编码。该模块的输入为literal和distance-length数据对,其中literal和length共用一个huffman表压缩,distance单独一个huffman表压缩。该模块的输出为两个huffman表以及经过huffman压缩后的数据;
- 3.序列化、游程编码,huffman表数据可进一步用Huffman编码压缩。该模块的输入为两个huffman表,输出为该huffman表统计后生成的huffman表,以及压缩后的数据流。
2.LZ77算法
由于字符串有很多子串是重复出现的,LZ77压缩算法正是利用这一特点压缩数据的。压缩过程中,会从已压缩的数据中查找该字符是否在前面出现过,如果出现过,则只需保存该字符与以前出现字符的距离以及字符长度。其中已压缩的数据会有最大长度,如果超出该长度则滑动窗需要前移。
当下一个需要压缩的字符序列能够在滑动窗口中找到, 这个序列会被两个数字代替: 一个是距离,表示这个序列在窗口中的起始位置离窗口的距离, 一个是长度, 字符串的长度。
以如下数据为例:
Blah blah blah blah blah!
字符串开始于: ‘B’,‘l’,‘a’,‘h’,‘ ’and ‘b’, 再来看接下来的5个字符:
前面的字符可以和这5个字符完全匹配,且刚好从当前数据的前5个字符开始。我们可以用当前子串起始字符与匹配字符串的距离distance和匹配字符串长度length。
目前数据是:
Blah blah b
压缩后的数据为
Blah b[D=5,L=5]
压缩还能继续进行, 对比这两个相同的字符串,它们各自的下一个字符都是‘1’, 所以我们可以把长度更新为6。如果我们继续比较,发现下一个, 下下一个, 下下下一个都是一样的。最终我们发现, 有18个字符相等。
最终压缩的数据为:
Blah b[D=5, L=18]!
其中压缩后的数据‘Blah b’以及最后的‘!’与原始字符串完全一样,称为literal。因此压缩后的数据由三类元素构成literal、length、distance。
LZ77算法的核心是在前面的历史数据中寻找重复字符串,同时重复现象是具有局部性的,它的基本假设是,如果一个字符串要重复,那么也是在附近重复,远的地方就不用找了,因此设置了一个查找数据的滑动窗口。LZ77滑动窗口是32KB,那么就是往前面32KB的数据中去找,这个32KB随着编码不断进行而往前滑动。
把滑动窗口设置得很大,那样就有更大的概率找到重复的字符串,压缩率会更高吗?初看起来如此,找的范围越大,重复概率越大,不过仔细想想,可能会有问题,一方面,找的范围越大,搜索时计算量会增大,不利于工程实现;另一方面,找的范围越大,距离越远,也不利于对距离进行进一步压缩。
3. huffman编码
huffman编码是一种前缀编码,每一个元素都有一个对应码字,其中每个码字都不可能是其他码字的前缀。huffman编码的基本原理是出现概率高的字符用尽可能短的码字编码。
以下面数据为例,
3、6、4、3、4、3、4、3、5
这个例子里,3出现了4次,4出现了3次,5出现了1次,6出现了1次。
根据数据出现频次我们可以构建一个huffman树,如下图:
各数字及其编码后码字分别为0–>3;10–>4;110–>5;111–>6。
上一章我们知道LZ77算法结果由三部分构成(literal、length、distance),以distance为例,LZ77中滑动窗大小固定为32K,也意味着distance的取值范围是1-32768。如果通过上面的方式,统计频率后将得到32768个码字,构建出来的huffman树会很大,不利于压缩存储。Deflate算法通过对huffman树增加一些特性使huffman码表更容易记录。
3.1 deflate中huffman树的记录方式
分析上面的例子,我们得到这个码表:
0–>3;10–>4;110–>5;111–>6。
码字中的0和1就是二进制的一个标志,互换一下其实根本不影响编码长度,所以,下面的码表也可以表示同样的符号。他们编码长度一样,只是码字不同。
1–>3;00–>4;010–>5;011–>6。
1–>3;01–>4;000–>5;001–>6。
0–>3;11–>4;100–>5;101–>6。
对比第一个和第二个例子,对应码树如下:
也就是说,我们把码树的任意节点的左右分支旋转(0、1互换),也可以称为树的左右互换,其实不影响编码长度。既然这些树的压缩程度都一样,那干脆使用最特殊的那棵树,只要规定了这棵树的特殊性,那么记录的信息就可以最少。
不同的树当然有不同的特点,deflate选择的树就是左边那棵,这棵树有一个特点,越靠左边越浅,越往右边越深,是这些树中最不平衡的树。这棵树也称为Deflate树,对应的解压缩算法称为Inflate。
按RFC1951总结的Deflate树相对于huffman树的新增特性:
- 同样位长的叶子节点的编码值为按字典顺序连续,右边的总比左边的大1;
- N+1位长最左边的叶子节点(也就是该层编码值最小的叶子节点)的值为N位长最右边叶子节点的值+1,然后左移以位,也就是code=(code+bl_count[N])<<1;
基于以上原则,我们可以通过字符对应的huffman码字长度记录huffman表。比如,一个有效的码表是0–>3;10–>4;110–>5;111–>6。但只需要记录这个对应关系即可:
3 4 5 6
1 2 3 3
也就是说,把1、2、3、3记录下来,解压时照着左边那棵树的形状构造一颗树,然后只需要1、2、3、3这些信息就可得到huffman码字是0、10、110、111。这就是deflate最核心的一点:这棵码树用码字长度序列记录下来即可。
当然,只把1、2、3、3这个序列记录下来还不行,比如不知道111对应5还是对应6?所以,构造出树来只是知道了有哪些码字了,但是这些码字到底对应哪些整数还是不知道。
前面已经说了,记录1、2、3、3还是记录1、3、2、3,或者3、3、2、1,其实都能构造出这棵树来,那么,我们可以选择一个特殊码字顺序记录,这个顺序就是按照整数由小到大的排列顺序,比如上面的3、4、5、6是整数大小顺序排列的,那么,记录的顺序就是1、2、3、3,而不是2、3、3、1。
根据1、2、3、3这个信息构造出了码字,这些码字对应的整数一个比一个大,假如我们知道编码前的整数就是3、4、5、6这四个数,那就能对应起来了,不过实际情况到底是哪四个数字还是不知道。以distance为例,由于distance的范围是1-32768,那么就按照这个顺序记录。上面的例子1和2没有,那就记录长度0。所以记录下来的码字长度序列为:
码字: 0、0、1、2、3、3、0、0、0、0、0、………
对应数值:1、2、3、4、5、6、7、8、9、10、11、……
这样就知道构造出来的码字具体对应哪个整数,但因为distance可能出现的数值有32768个,但实际出现的往往不多,中间会出现很多0(也就是根本就没出现这个距离),如果每个数字都编码,会有很多冗余数据,因此需要继续优化。
3.2 distance的压缩方式
每个distance肯定对应唯一一个码字,使用Huffman编码可以得到所有码字,但是因为distance可能非常多,虽然一般不会有32768这么多,但对一个大些的文件进行LZ编码,distance上千还是很正常的,所以这棵树很大,计算量、消耗的内存都容易超越了那个时代的硬件条件。Deflate算法把distance划分成多个区间,每个区间当做一个整数来看,这个整数称为Distance Code。当一个distance落到某个区间,则相当于是出现了那个Code,多个distance对应于一个Distance Code,Distance虽然很多,但Distance Code可以划分得很少,只要我们对Code进行Huffman编码,得到Code的编码后,Distance Code再根据一定规则扩展出来。我们分析过,越小的距离,出现的越多;越大的距离,出现的越少,所以这种区间划分不是等间隔的,而是越来越稀疏的,类似于下面的划分:
1、2、3、4这四个特殊distance不划分,或者说1个Distance就是1个区间;5,6作为一个区间;7、8作为一个区间等等,基本上,区间的大小都是1、2、4、8、16、32这么递增的,越往后,涵盖的距离越多。为什么这么分呢?首先自然是距离越小出现频率越高,所以距离值小的时候,划分密一些,这样可以对小的距离进行更精细地编码,使得其编码长度尽可能小;对于距离较大那些,因为出现频率低,所以可以适当放宽一些。另外,只要知道这个区间Code的码字,那么对于这个区间里面的所有distance,后面追加相应的多个比特(extra bits)即可,比如,17-24这个区间的Huffman码字是110,因为17-24这个区间有8个整数,于是按照下面的规则即可获得其distance对应的码字:
17–>110 000
18–>110 001
19–>110 010
20–>110 011
21–>110 100
22–>110 101
23–>110 110
24–>110 111
这样需要进行Huffman编码的整数变少了,这棵huffman树不会太大,计算起来时间和空间复杂度降低,扩展起来也比较简单。当然,从理论上来说,这样的编码方式实际上将编码过程分为了两级,并不是理论上最优的,把所有distance当作一个大空间去编码才可能得到最优结果,不过还是那句话,工程实现的限制,在压缩软件实现上,我们不能用压缩率作为衡量一个算法优劣的唯一指标,其实耗费的时间和空间同样是指标,所以需要看综合指标。
Distance划分为30个区间,左边的code表示该区间的编号,范围是0-29,共30个区间,这只是个编号,没有特别的含义,但Huffman就是对0-29这30个Code进行编码的,得到区间的码字;
Extra bits表示distance的码字需要在Code的码字基础上扩展几位,比如0就表示不扩展,最大的13表示要扩展13位,因此,最大的区间包含的distance数量为8192个。
Distance一列则表示这个区间涵盖的distance范围。
3.3 literal和length的压缩方式
literal和length的压缩方式与distance类似。Literal是未匹配的字符,deflate是针对字节作为基本字符来编码的,所以literal的数值范围是0-255。Length表示重复字符串长度,length=1当然不会出现,因为一个字节不值得用distance+length来记录,deflate规定length最小值是3,必须3个以上字符的字符串出现重复才用distance+length记录。那么,最大的length是多少呢?理论上当然可以很长很长,比如一个文件就是连续的0,这个重复字符串长度其实接近于这个文件的实际长度。但是length的范围做了限制,限定length的个数跟literal一样,也只有256个,因为一个重复字符串达到了256个已经很长了,概率非常小。
为什么要把literal和length二者合二为一呢?因为当解码器接收到一个比特流的时候,首先可以按照literal/length这个码表来解码,如果解出来是0-255,就表示未匹配字符,如果是256,那自然就结束,如果是257-285之间,则表示length,把后面扩展比特加上形成length后,后面的比特流肯定就表示distance,因此,实际上通过一个Huffman码表,对各类情况进行了统一,而不是通过加一个什么标志来区分到底是literal还是重复字符串。
和distance一样, length(总共256个值)划分为29个区间,其结果如下图:
理解了上面的过程,就理解了ZIP压缩的第二步,第一步是LZ编码,第二步是对LZ编码后结果(literal、distance、length)进行的再编码,因为literal/length是一个码表,我们称其为Huffman码表1,distance那个码表称为Huffman码表2。前面我们已经分析了,Huffman码树用一个码字长度序列表示,称为CL(Code Length),记录两个码表的码字长度序列分别记为CL1、CL2。码树记录下来,对literal/length的编码比特流称为LIT比特流;对distance的编码比特流称为DIST比特流。
按照上面的方法,LZ的编码结果就变成四块:CL1、CL2、LIT比特流、DIST比特流。CL1、CL2是码字长度的序列,这个序列就是一堆正整数,因此还可以继续对码表进行压缩。
4. 对码表压缩
对码表的压缩仍然沿用Huffman的想法,因为码表序列也是一堆整数,那么当然可以再次应用Huffman编码。不过在此之前,需要先对序列进行了一点处理。
对于literal/length,总共有0-285这么多符号,前面章节已经说过,defalte算法构造的特殊huffman树可以使用码字长度序列来表示,所以这个序列长度为286,每个符号都有一个码字长度,当然,这里面可能会出现大段连续的0,因为某些字符或长度不存在,尤其是对英文文本编码的时候,非ASCII字符就根本不会出现,length较大的值出现概率也很小,所以出现大段的0是很正常的;对于distance也类似,也可能出现大段的0。因此先进行游程编码可以有效压缩数据。
4.1 游程编码
literal/length的编码符号总共286个(256个Literal+1个结束标志+29个length区间),distance的编码符号总共30个(30个区间),所以这颗码树不会特别深,Huffman编码后的码字长度不会特别长,最长不会超过15,也就是树的深度不会超过15(为什么)。因此,CL1和CL2这两个序列的任意整数值的范围是0-15。0表示某个整数没有出现(比如literal=0x12, length Code=8, distance Code=15等等)。
什么叫游程呢?就是一段完全相同的数的序列。什么叫游程编码呢?说起来原理更简单,就是对一段连续相同的数,记录这个数一次,紧接着记录出现了多少个即可。比如CL序列如下:
4, 4, 4, 4, 4, 3, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2
那么,游程编码的结果为:
4, 16, 01(二进制), 3, 3, 3, 6, 16, 11(二进制), 16, 00(二进制), 17,011(二进制), 2, 16, 00(二进制)
这是什么意思呢?因为CL的范围是0-15,如果重复出现2次认为太短就不用游程编码,所以游程长度从3开始。用16这个特殊的数表示重复出现3、4、5、6个这样一个游程,分别由后面跟着的2bit数据00、01、10、11表示(实际存储的时候需要低比特优先存储,需要把比特倒序来存,博文的一些例子有时候会忽略这点,实际写程序的时候一定要注意,否则会得到错误结果)。于是4,4,4,4,4,这段游程记录为4,16,01,也就是说,4这个数,后面还会连续出现了4次。6,16,11,16,00表示6后面还连续跟着6个6,再跟着3个6;因为连续的0出现的可能很多,所以用17、18这两个特殊的数专门表示0游程,17后面跟着3个比特分别记录长度为3-10(总共8种可能)的游程;18后面跟着7个比特表示11-138(总共128种可能)的游程。17,011(二进制)表示连续出现6个0;18,0111110(二进制)表示连续出现62个0。总之记住,0-15是CL可能出现的值,16表示除了0以外的其它游程;17、18表示0游程。因为二进制实际上也是个整数,所以上面的序列用整数表示为:
4, 16, 1, 3, 3, 3, 6, 16, 3, 16, 0, 17, 3, 2, 16, 0
4.2 huffman压缩
我们又看到了一串整数,这串整数的值的范围是0-18。这个序列称为SQ(Sequence的意思)。因为有两个CL1、CL2,所以对应的有两个SQ1、SQ2。
针对SQ1、SQ2,deflate使用第三个Huffman码表来对这两个序列进行编码。通过统计各个整数(0-18范围内)的出现次数,按照相同的思路,对SQ1和SQ2进行了Huffman编码,得到的码流记为SQ1 bits和SQ2 bits。同时,这里又需要记录第三个码表,称为Huffman码表3。同理,这个码表也用相同的方法记录,也等效为一个码长序列,称为CCL,因为至多有0-18个,该树的深度至多为7,因此CCL的范围是0-7。
当得到了CCL序列后,对这个序列用普通的3比特定长编码记录下来即可,即000代表0,111代表7。这个序列如果全部记录,那就需要19*3=57个比特, CL序列里面CL范围为0-15,特殊的几个值是16、17、18,如果把CCL序列位置置换一下,把16、17、18这些放前面,那么这个CCL序列就很可能最后面跟着一串0(因为CL=14,15这些很可能没有),所以最后还引入了一个置换,其示意图如下,分别表示置换前的CCL序列和置换后的CCL。可以看出,16、17、18对应的CCL被放到了前面,这样如果尾部出现了一些0,就只需要记录CCL长度即可,后面的0不记录。可以继续节省一些比特,不过这个例子尾部置换后只有1个0:
5. Deflate数据格式
Deflate压缩数据时会先把数据分块再压缩,下面的deflate格式只针对其中一个数据块。其格式为:
-
Header:3个比特,第一个比特BFINAL,如果是1,表示此部分为最后一个压缩数据块;否则表示这是deflate文件的某个中间压缩数据块,但后面还有其他数据块。这是ZIP中使用分块压缩的标志之一;
-
第2、3比特表示3个选择:压缩数据中没有使用Huffman、使用静态Huffman、使用动态Huffman,这是对LZ77编码后的literal/length/distance进行进一步编码的标志。我们前面分析的都是动态Huffman,其实Deflate也支持静态Huffman编码,静态Huffman编码原理更为简单,无需记录码表(因为自己定义了一个固定的码表),但压缩率不高,所以大多数情况下都是动态Huffman。
-
HLIT:5比特,记录literal/length码树中码长序列(CL1)个数的一个变量。后面CL1个数等于HLIT+257(因为至少有0-255总共256个literal,还有一个256表示解码结束,但length的个数不定)。
-
HDIST:5比特,记录distance码树中码长序列(CL2)个数的一个变量。后面CL2个数等于HDIST+1。哪怕没有1个重复字符串,distance都为0也是一个CL。
-
HCLEN:4比特,记录Huffman码表3中码长序列(CCL)个数的一个变量。后面CCL个数等于HCLEN+4。PK认为CCL个数不会低于4个,即使对于整个文件只有1个字符的情况。
-
接下来是3比特编码的CCL,一共HCLEN+4个,用以构造Huffman码表3;
-
接下来是对CL1(码长)序列经过游程编码(SQ1:缩短的整数序列)后,并对SQ1继续用Huffman编码后的比特流。包含HLIT+257个CL1,其解码码表为Huffman码表3,用以构造Huffman码表1;
-
接下来是对CL2(码长)序列经过游程编码(SQ2:缩短的整数序列)后,并对SQ2继续用Huffman编码后的比特流。包含HDIST+1个CL2,其解码码表为Huffman码表3,用于构造Huffman码表2;
总之,上面的数据都是为了构造LZ解码需要的2个Huffman码表。
接下来才是经过Huffman编码的压缩数据,解码码表为Huffman码表1和码表2。 -
最后是数据块结束标志,即literal/length这个码表输入符号位256的编码比特。
对倒数第1、2内容块进行解码时,首先利用Huffman码表1进行解码,如果解码所得整数位于0-255之间,表示literal未匹配字符,接下来仍然利用Huffman码表1解码;如果位于257-285之间,表示length匹配长度,之后需要利用Huffman码表2进行解码得到distance偏移距离;如果等于256,表示数据块解码结束。