GZIP压缩原理分析(33)——第五章 Deflate算法详解(五24) 动态哈夫曼编码分析(13)构建哈夫曼树(05)

*构建CCL树

压缩数据已经成了比特流,基本没有继续压缩的空间。仔细观察literal/length码字长度数列和distance码字长度数列发现,这两个数列中有大量的0存在,就像海绵里的水,挤一挤应该还能继续压缩。所以,我们现在要压缩的就是literal/length码字长度数列和distance码字长度数列。回顾游程编码原理以及性质。

已知literal/length码字长度数列记录的是literal的码字长度和length区间码的码字,distance码字长度数列记录的是distance区间码的码字长度。压缩中规定,literal的码字长度、length区间码的码字长度、distance区间码的码字长度,都不能超过15bits源码中的注释是这样说的:“Allcodes must not exceed MAX_BITS bits”,MAX_BITS是一个宏,值为15。实际压缩过程中,很多时候码字长度都是超过15的,源码在函数gen_bitlen()中对超过15的情况进行了专门处理,我称该处理过程为哈夫曼树的“嫁接”!所谓嫁接就是说找到码字长度超过15的那一枝,把超过的部分“掰”下来嫁接到深度不够15的分枝上去同时保证新的树枝深度不超过15(有点像把不平衡树变平衡的过程)。嫁接的过程后续源码分析章节还会涉及,这里只要知道这个规定即可。

知道了这个规定,再来看这两个数列:

Literal/length码字长度数列,

0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、3、0、0、0、0、0、0、0、0、0、0、0、6、0、6、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、6、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、466536、0、55、0、64545、0、0、544666、0、5、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、656、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0

Distance区间码码字长度数列,

0、0、0、2、0、0、0、0、1、0、2、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0

规定要求数列中每一个数必须在闭区间[0, 15]中,这两个数列是符合这个要求的。现在来看怎么压缩这两个数列。

在一个窗口中,某些literal或length可能根本不存在,尤其是对英文文本编码的时候,非ASCII字符就根本不会出现;length较大的值出现概率也很小; distance也类似。所以这两个数列都出现大段的0,而且还是连续的,这种情况是非常正常的,而且并不仅仅限于这个例子。既然有这种“特性”,那么压缩就可以在这种特性上做文章了。

首先,我们可以把这两个数列截短。对于literal/length数列,从第259个数(从0开始的)开始往后,就全部是0了,这些0留着一点用处也没有,所以该数列现在就是,

0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、3、0、0、0、0、0、0、0、0、0、0、0、6、0、6、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、6、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、466536、0、55、0、64545、0、0、544666、0、5、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、656

共有259个数。同理,对于distance数列,从第11个数开始往后,全部截掉,所以该数列现在就是,

0、0、0、2、0、0、0、0、1、0、2

共有11个数。注意这个25911,这两个个数统计值在后面至关重要。

还记得游程编码吗,我们现在用游程编码来处理这两个数列。对于literal/length数列,经过游程编码,得到结果,

18、21、3、18、0、6、0、618、7、618、20、466536、0、55、0、64545、0、0、544666、0、518、123、656

对于distance数列,经过游程编码,得到结果,

17、0、217、1、1、0、2

可以看出,经过游程编码,这两个数列所含有的“项数”已经大大减少了。注意,顿号是我为了方便说明而自己加的,不是数列原本就有的。

在这两个数列中,“码字长度”不会超过15,游程编码的“头”只有16、17、18三个数,游程编码的“游程”类似length区间码表和distance区间码表的“extra bits”部分,可自行编码,所以游程编码之后的这两个数列,刨掉游程部分,每一项的大小都不会超过18,取值范围对应闭区间[0, 18]。对这两个数列的压缩,再次用到哈夫曼编码。因为这两个数列每项大小都在闭区间[0, 18]范围内,所以把这两个数列合成一个数列,构建一棵哈夫曼树即可。注意,我这里说的合成并不是真的把这两个数列连在一起变成一个数列,只是放在一起同时统计每个数的出现频率罢了;游程可自行编码,所以不参与哈夫曼编码过程。

对这两个数列的哈夫曼编码过程与对LZ77之后结果的哈夫曼编码过程相同,都使用范式哈夫曼编码。不同的是,压缩规定,literal/length区间码和distance区间码的码字长度不超过15bits,而这两个数列中,每一项的码字长度都不能超过7bits,并且没有区间码的概念,直接对闭区间[0,18]中的数编码。构建完哈夫曼树,如果发现有叶子节点深度超过7,同样会使用“嫁接”的方式将码字长度限制在7bits。

统计这两个数列中,[0, 18]每个数的出现频率,构建哈夫曼树,过程与前面完全相同。为简便起见,直接使用范式哈夫曼树作为图示,如下,

 得到哈夫曼码表如下: 

码字长度数列码表

码字长度值/游程头

码字直接计算结果

实际码字(作为压缩结果,从右往左看)

5

00

00

6

01

10

0

100

001

4

101

101

18

110

011

1

11100

00111

2

11101

10111

3

11110

01111

17

11111

11111

 “游程”值的码表如下: 

“游程”值码表

游程头

0的个数

游程值

占用比特数

游程值码字(作为压缩结果)

18

32

21

7

0010101

18

11

0

7

0000000

18

18

7

7

0000111

18

31

20

7

0010100

18

134

123

7

1111011

17

3

0

3

000

17

4

1

3

001

 提示,只有哈夫曼编码的码字需要按位逆序,其他码字不用。

得到这两张码表,现在将literal/length码字长度数列和distance码字长度数列转换成比特流,

18(011)21(0010101)3(01111)18(011)0(0000000)6(10)0(001)6(10)18(011)7(0000111)

6(10)18(011)20(0010100)4(101)6(10)6(10)5(00)3(01111)6(10)0(001)5(00)5(00)0(001)

6(10)4(101)5(00)4(101)5(00)0(001)0(001)5(00)4(101)4(101)6(10)6(10)6(10)0(001)5(00)

18(011)123(1111011)6(10)5(00)6(10)

17(11111)0(000)2(10111)17(11111)1(001)1(00111)0(001)2(10111);

但这并不是最终的比特流,必须将其按规则放到一个个“字节”中,这个规则与前面对实际数据的压缩相同,结果如下,

(将literal/length码字长度数列比特流让出一位,不足一个字节先不管,原因后面介绍)

1

01010101    11011110    00000000    11000110    00011101    00011100    10100101    11001010

00110011    00010000    01001011    01001001    01101000    11010101    10110000    10111101

1000

(将distance码字长度数列比特流让出四位,不足一个字节先不管,原因后面分析)

1111

01110001    01111111    0100111

101110

对应的十六进制分别是

55     DE    00     C6     1D     1C     A5     CA

33     10    4B     49      68      D5    B0     BD

71     7F    4E 

这是我们人工压缩的结果,看看压缩工具的结果,如下图,

 从图中可以看出,按照压缩原理,我们人工压缩的结果和压缩工具的结果是一样的。注意,这里先不关心这几个结果之间的衔接部分。

现在,我们已经得到了literal/length码字长度比特流、distance码字长度比特流和实际压缩数据比特流,不足一个字节的部分,我用“*”暂时占位,这三个比特流分别为,

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

1*******

01010101    11011110    00000000     11000110     00011101    00011100     10100101     11001010

00110011    00010000    01001011     01001001     01101000    11010101     10110000     10111101

****1000

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

1111****

01110001    01111111    01001110 

**101110

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

11******

11100110    10101000    10110100     00101000     11001101    10011000     00100000     01111011

01111011    10111000    01000100     01100110     00100111    01100100     01010110     11000101

00000110    10101110    01100010     11001001     11010001    01001110     10111100     10100101

01010011    11101001    00001110     00011111     00011110    10101100     11010011     11111110

00010011    10010001    11000101     01110000     01010000    11110101     01010110     11101001

11101011    00000111

+++++++++++++++++++++++++++++++++++++++++++++++++++++++=+++++++++++

到这个时候,压缩一共用了三次哈夫曼编码,前两次用来编码实际压缩数据,最后一次用来编码码字长度数列,该码字长度数列包括literal/length码字长度数列和distance码字长度数列。对实际数据的哈夫曼编码,要求码字长度不能超过15bits(length和distance的区间码码字长度,而不是具体length值和distance值的码字长度;literal是一对一的,所以就是literal的码字长度);对码字长度数列的哈夫曼编码,要求码字长度不能超过7bits。

从上面的哈夫曼编码码表可以看出,所有哈夫曼编码的码字,作为实际压缩结果输出时都需要一个“逆序”的过程。非哈夫曼编码的码字或输出比特,比如extra bits的编码,“游程”的编码,标明是动态哈夫曼编码还是静态哈夫曼编码还是存储的那两比特等,都不需要按位逆序。

比特流真正作为压缩结果输出时是以字节为单位的,所以必须把比特流填充到一个个字节中去。填充的过程,要从字节的低位开始填起,如果一个字节不够,就将码字剩余部分填充到下一个字节中,当然,也是从下一个字节的低位开始填起。

压缩结束了吗,不,还差临门一脚。literal/length码字长度比特流、distance码字长度比特流记录实际压缩数据比特流的码字长度信息,可谁来记录literal/length码字长度比特流和distance码字长度比特流的码字长度信息?接下来我们分析如何记录literal/length码字长度比特流和distance码字长度比特流的码字长度信息。

我们已经知道,这两条比特流使用同一棵哈夫曼树,所以只要把这“一棵”哈夫曼树每个叶子节点的深度记录下来即可。再回顾一下这棵树,如下图所示,

 对应下表, 

叶子节点

深度(码字长度,不超过7bits)

5

2 bits

6

2 bits

0

3 bits

4

3 bits

18

3 bits

1

5 bits

2

5 bits

3

5 bits

17

5 bits

码字长度的记录方式都是一样的,这个也不例外。游程编码之后的结果,刨掉无需哈夫曼编码的“游程”,剩下的数都在闭区间[0, 18]范围内,所以该码字长度序列就是,

3、5、5、5、3、2、2、0、0、0、0、0、0、0、0、0、0、5、3,

每个数在该序列中的“顺序(从0开始)”就是闭区间[0, 18]中的值,即哈夫曼树的叶子节点,而这个数就是对应叶子节点的深度,即码字长度。

在这个数列中,我们看到有很多0出现,这些0没有任何用处,所以压缩会对该数列做一个排序,使这些0项尽可能的排到数列的末尾。排序方式非常简单粗暴,就是将序号这样的数列

0、1、2、3、4、5、6、7、8、9、10、11、12、13、14、15、16、17、18

直接排成这样

16、17、18、0、8、7、9、6、10、5、11、4、12、3、13、2、14、1、15

的数列。把第16个数排到第0个,第17个数排到第1个,第……等。这种排序方式与压缩内容无关,无论什么内容,到了这一步,都按这个顺序重新排列。至于为什么这样,我猜可能是基于某种统计结果,发现这样排序对于大多数待压缩内容来讲可以把码字长度为0的项排到数列的后面。

按照这个顺序,将码字长度序列

3、5、5、5、3、2、2、0、0、0、0、0、0、0、0、0、0、5、3,

重新排序后就是

0、5、3、3、0、0、0、2、0、2、0、3、0、5、0、5、0、5、0,

从这里可以看出这个排序对于本例的支持并不好,因为只有一个0排在该数列的后面。

与literal/length码字长度数列和distance码字长度数列的处理方式相同,我们把重排后的这个数列(可以称其为“literal/length码字长度数列和distance码字长度数列的码字长度数列^o^”)末尾的0项全部去掉。放到这个例子里,也就去掉末尾的那一个0而已。得到新的数列,

0、5、3、3、0、0、0、2、0、2、0、3、0、5、0、5、0、5,

该数列共有18项(闭区间[0, 18]共19个数,这里去掉了一个,所以共18个)。

把这个数列转换成比特流。不要怕,这次不用哈夫曼编码了。该数列最多也就有19个数(代表闭区间[0, 18]),每个数都不会超过7(别忘了“对码字长度数列的哈夫曼编码,要求码字长度不能超过7bits”),所以压缩直接规定用三比特对这个数列中的每一个数编码。编码方式异常简单,和extra bits的编码方式相同,0就是000,1就是001,2就是010……7就是111。

将该数列转换成比特流就是(提示,这里不用逆序):

0(000)5(101)3(011)3(011)0(000)0(000)0(000)2(010)0(000)

2(010)0(000)3(011)0(000)5(101)0(000)5(101)0(000)5(101)

把它们放到一个个字节中,才能成为真正的比特流,即

(空出一位,原因后面解释)

1101000

00001101    10000000     0010000   00001100   01000101    

1010001

对应十六进制为

0D  80 20  0C  45 

这是我们人工压缩的结果,压缩工具的压缩结果如下图所示,

我们人工的压缩结果与实际的压缩结果是完全相同的。

现在,我们一共得到四条比特流,分别是“literal/length码字长度数列和distance码字长度数列的码字长度比特流(即,这两个数列的码字长度比特流)”、literal/length码字长度比特流、distance码字长度比特流和实际压缩数据比特流,不足一个字节的部分,我用“*”暂时占位,这四个比特流分别为,

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

1101000*

00001101     10000000    00100000     00001100     01000101 

*1010001

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

1*******

01010101     11011110    00000000     11000110     00011101    00011100     10100101     11001010

00110011     00010000    01001011     01001001     01101000    11010101     10110000     10111101

****1000

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

1111****

01110001  01111111 01001110 

**101110

++++++++++++++++++++++++++++++++++++++++++++++++++=+++++++++++++++

11******

11100110  10101000 10110100  00101000  11001101 10011000  00100000  01111011

01111011  10111000 01000100  01100110  00100111 01100100  01010110  11000101

00000110  10101110 01100010  11001001  11010001 01001110  10111100  10100101

01010011  11101001 00001110  00011111  00011110 10101100  11010011  11111110

00010011  10010001 11000101  01110000  01010000 11110101  01010110  11101001

11101011  00000111

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

得到三个关键的数,即2591118,分别表示literal/length码字长度数列的有效个数、distance码字长度数列的有效个数、以上两个数列的码字长度数列的有效个数。将这四条比特流咬合在一起,就是,

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

1101000*

00001101  10000000 00100000  00001100  01000101 

11010001

01010101  11011110 00000000  11000110  00011101 00011100  10100101  11001010

00110011  00010000 01001011  01001001  01101000 11010101  10110000  10111101

11111000

01110001  01111111 01001110 

11101110

11100110  10101000 10110100  00101000  11001101 10011000  00100000  01111011

01111011  10111000 01000100  01100110  00100111 01100100  01010110  11000101

00000110  10101110 01100010  11001001  11010001 01001110  10111100  10100101

01010011  11101001 00001110  00011111  00011110 10101100  11010011  11111110

00010011  10010001 11000101  01110000  01010000 11110101  01010110  11101001

11101011  00000111

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

衔接部分的三个字节,十六进制表示为D1、F8、EE,对应的实际压缩结果如下图,

我们将四条比特流合成一条比特流,上图说明我们的合成方法是对的。其实压缩中并没有将这四条比特流“咬合”的说法,也不会像我这样把这四条比特流这么“严格”的分开。压缩中都是直接“输出”的(暂且理解为直接输出,其实源码里还有个中间过程,后面分析),一条比特流完成,后面的比特流紧跟着往后续的字节里填充, 千万不要理解成分别获得每一条比特流然后再把他们拼接起来!!!我在这篇博客里分开说这些比特流是为了理解起来思路清晰而已,后面源码分析我们会看到实际的压缩结果是怎么输出的。

现在这条比特流是不能被解压缩工具解开的!虽然我们记录了所有涉及到的码字长度信息,但是,从该比特流开始到实际压缩数据比特流还有很长的距离,不知道这个距离,就不知道什么时候应该开始用构造好的哈夫曼树去解码实际压缩数据。现在来解释一下这张图,

蓝色部分就是实际压缩数据比特流;SQ2是distance码字长度比特流,对应数列CL2;SQ1是literal/length码字长度比特流,对应数列CL1;CCL数列就是记录SQ1和SQ2的码字长度比特流(或数列)。用HCLEN记录CCL数列的“项数”,本例中,HCLEN + 4 = 18;用HLIT记录CL1数列的“项数”,本例中,HLIT + 257 = 259;用HDIST记录CL2数列的“项数”,本例中,HDIST + 1 = 11;这三个变量分别用4bits、5bits、5bits编码,本例中,这三个变量分别是HCLEN(1110)、HLIT(00010)、HDIST(01010),提示,这三个编码仍然不需要逆序。

该压缩块是最后一个压缩块,所以块首部为一比特的1;该压缩块使用动态哈夫曼编码,所以是两比特的10;带上HLIT(00010)、HDIST(01010) 、HCLEN(1110),放到一个个字节中,这部分的比特流就是:

00010101   11001010    1

对应十六进制表示就是

15     CA

压缩工具的压缩结果如下图所示,

 和我们人工计算结果是相同的。将该比特流记为:

(不足一个字节的地方用“*”占位)

00010101     11001010     *******1

将该比特流和前面的比特流咬合在一起,

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

00010101  11001010

*******1

+++++++++++++++++++++++++++++++++++++++++=+++++++++++++++++++

1101000*

00001101  10000000 00100000  00001100  01000101 

11010001

01010101  11011110 00000000  11000110  00011101 00011100  10100101  11001010

00110011  00010000 01001011  01001001  01101000 11010101  10110000  10111101

11111000

01110001  01111111 01001110 

11101110

11100110  10101000 10110100  00101000  11001101 10011000  00100000  01111011

01111011  10111000 01000100  01100110  00100111 01100100  01010110  11000101

00000110  10101110 01100010  11001001  11010001 01001110  10111100  10100101

01010011  11101001 00001110  00011111  00011110 10101100  11010011  11111110

00010011  10010001 11000101  01110000  01010000 11110101  01010110  11101001

11101011  00000111

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

衔接处的十六进制值为D1,实际压缩结果如下图所示,

 与我们人工计算结果相同,所以整条比特流即,

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

00010101     11001010    11010001    00001101    10000000     00100000     00001100    01000101

11010001     01010101    11011110    00000000    11000110     00011101     00011100    10100101

11001010     00110011    00010000    01001011    01001001     01101000     11010101    10110000

10111101     11111000    01110001    01111111    01001110     11101110     11100110    10101000

10110100     00101000    11001101    10011000    00100000     01111011     01111011    10111000

01000100     01100110    00100111    01100100    01010110     11000101     00000110    10101110

01100010     11001001    11010001    01001110    10111100     10100101     01010011    11101001

00001110     00011111    00011110    10101100    11010011     11111110     00010011    10010001

11000101     01110000    01010000    11110101    01010110     11101001     11101011    00000111

+++++++++++++++++++++++++++++++++++++++++++++++++++++=+++++++++++

这才是一个完整的压缩块。如果带上gzip文件头和尾,就组成一个完整的gzip压缩文件了。

注意:不论几个压缩块,整条比特流中间任何位置都绝对不能有间断的地方。比如,两个压缩块之间隔了几个比特,这样的比特流是无法被正常解压的,因为解压缩工具不会意识到那是“间隔”的几个比特,只会对照码表继续解压,当发现无法解压时就会产生异常。这一点在HTTP流压缩中尤为重要。

整个动态哈夫曼编码过程结束。上面整个压缩过程都是我们人工按照压缩原理进行的,可以看到,通过逐步推导,人工压缩结果与压缩工具的压缩结果是完全相同的,这个过程证明我们的分析基本是正确的。以上整个人工压缩过程,就是gzip压缩的实际流程,压缩原理不过如此吧。

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 27
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值