GZIP压缩原理分析(14)——第五章 Deflate算法详解(五05) 预备知识(04) 前缀码、原始哈夫曼编码原理以及deflate所用哈夫曼编码的性质

1.前缀码

在一个字符集中,任何一个字符的编码都不是另一个字符编码的前缀,即前缀码。例如,有两个码字111与1111,那么这两个码字就不符合前缀码的规则,因为111是1111的前缀。放到二叉树里来讲,只用叶子节点编码的码字才是前缀码,如果同时使用中间节点和叶子节点编码,那结果就不是前缀码。因为压缩中经过编码的码字全部是前缀码,所以在对照码表解压的时候,碰到哪个码字就是哪个码字,不用担心出现某个字符的编码是另一个字符的编码的前缀的情况,该意识一定要具备。

 

关于前缀码,下面一段话摘自《算法导论》:“前缀码的作用是简化解码过程。由于没有码字是其他码字的前缀,编码文件的开始码字是无歧义的。我们可以简单的识别出开始码字,将其转换会原字符,然后对编码文件剩余部分重复这种解码过程。”

 

2.原始哈夫曼编码

哈夫曼设计了一个贪心算法来构造最优前缀码,被称为哈夫曼编码(Huffman code),其正确性证明依赖于贪心选择性质和最优子结构。哈夫曼编码可以很有效的压缩数据,具体压缩率依赖于数据本身的特性。这里我们先介绍几个概念:码字、码字长度、定长编码与变长编码。

 

每个字符可以用一个唯一的二进制串表示,这个二进制串称为这个字符的码字,这个二进制串的长度称为这个码字的码字长度。码字长度固定就是定长编码,码字长度不同则为变长编码。变长编码可以达到比定长编码好得多的压缩率,其思想是赋予高频字符(出现频率高的字符)短(码字长度较短)码字,赋予低频字符长码字。例如,我们用ASCII字符编辑一个文本文档,不论字符在整个文档中出现的频率,每个字符都要占用一个字节;如果我们使用变长编码的方式,每个字符因在整个文档中的出现频率不同导致码字长度不同,有的可能占用一个字节,而有的可能只占用一比特,这个时候,整个文档占用空间就会比较小了。当然,如果这个文本文档相当大,导致每个字符的出现频率基本相同,那么此时所谓变长编码在压缩方面的优势就基本不存在了(这点要十分明确,这是为什么压缩要分块的原因之一,后续源码分析会详细讲解)

 

哈夫曼编码会自底向上构造出一棵对应最优编码的二叉树,我们使用下面这个例子来说明哈夫曼树的构造过程。首先,我们已知在某个文本中有如下字符及其出现频率,

字符

a

b

c

d

e

f

出现频率

45

13

12

16

 9

 5

构造过程如下图所示:

GZIP压缩原理分析(14)——第五章 Deflate算法详解(五05) 预备知识(04) 前缀码、原始哈夫曼编码原理以及deflate所用哈夫曼编码的性质_第1张图片

图1到图6列除了整个哈夫曼树构造过程中的每个步骤。在一开始,每个字符都已经按照出现频率大小排好顺序,在后续的步骤中,每次都将频率最低的两棵树合并,然后用合并后的结果再次排序(注意,排序不是目的,目的是找到这时出现频率最低的两项,以便下次合并。gzip源码中并没有专门去排序,而是使用专门的数据结构把频率最低的两项找到即可)。叶子节点用矩形表示,每个叶子节点包含一个字符及其频率。中间节点用圆圈表示,包含其孩子节点的频率之和。中间节点指向左孩子的边标记为0,指向右孩子的边标记为1。一个字符的码字对应从根到其叶节点的路径上的边的标签序列。图1为初始集合,有六个节点,每个节点对应一个字符;图2到图5为中间步骤,图6为最终哈夫曼树。此时每个字符的编码都是前缀码。

 

3.Deflate所用的哈夫曼编码的性质

哈夫曼编码使用的数据结构就是二叉树。这里介绍一些阅读gzip源码时需要使用的一些哈夫曼树的性质,注意,deflate算法使用的哈夫曼树在原始哈夫曼树的基础上增加了一些独特的性质,专门为压缩/解压缩服务。本章只要了解这些性质并记住这里提出的问题即可。上文所述是原始哈夫曼树,与压缩使用的哈夫曼树还有一些区别。这部分内容主要参考博客http://blog.csdn.net/imquestion/article/details/16439,还有一部分是我自己总结的。我们参看下图来验证这些性质,注意:码字长度就是树的深度

GZIP压缩原理分析(14)——第五章 Deflate算法详解(五05) 预备知识(04) 前缀码、原始哈夫曼编码原理以及deflate所用哈夫曼编码的性质_第2张图片

a.  如果有n个叶子节点,那么整棵树的总节点个数为2n-1。以上图为例,有6个叶子节点,而总节点共有11个。注意,这个地方与gzip源码中几个数组的定义不同,源码中是2n+1,原因后续分析,这里提一下,后续分析时要留心;

b.  整棵树最左边叶子节点的码字为0(码字长度视情况而定);

c.   码字长度相同(即树深相同)的叶子节点,它们的码字是连续的,而且右面的总比左面的大1。从上图中可以看出,c、b、d节点在同一层,c的码字为100,b的码字为101,符合该性质,但是d的码字为111,不符合该性质。如果能将d节点与14节点交换,那就符合该性质了。实际上,这就是deflate中所用哈夫曼编码与原始哈夫曼编码不同的地方,前者构建哈夫曼树的过程在后者的构建上增加了一些条件。这个地方留作一个问题,我们后续分析源码时详细讨论,这里要留心。

 

此时我们已经初步看到deflate中所用哈夫曼编码与原始哈夫曼编码的不同,现在我们使用一棵deflate中所用的标准的哈夫曼树来分析以下的性质,如下图所示

GZIP压缩原理分析(14)——第五章 Deflate算法详解(五05) 预备知识(04) 前缀码、原始哈夫曼编码原理以及deflate所用哈夫曼编码的性质_第3张图片

d.  树深为(n+1)时,该层最左面的叶子节点(即本层码字值最小的那个叶子节点)的值为,树深为n时,n层最右面的叶子节点(即这一层码字值最大的叶子节点)的值+1,并且要变长一位(即左移一位)。以上图为例,i的码字是01,f的码字是100,符合该性质;a的码字是11100,但是a的上一层没有叶子节点,不能用该性质?我们先接着看下面的性质;

e.  树深为n这层,最右面的叶子节点(即该层码字值最大的叶子节点)的值为最左面的叶子节点的值(即该层码字值最小的叶子节点)加上该层所含叶子节点的个数减一。以上图为例,f的码字是100,e的码字是110,该层共有叶子节点三个,符合该性质;a的码字为11100,d的码字为11111,该层共有叶子节点四个,符合该性质;

f.  前两条性质可以合成本条性质,即,树深为(n+1)时,该层最左面的叶子节点的值为,树深为n的这一层最左面的叶子节点的值加上该层所有叶子节点的个数,然后变长一位(即左移一位)。以上图为例,h的码字为00,改层有叶子节点两个,f的码字为100,所以00+10(2的二进制表示)=10,将10左移一位就是100,即f,符合该性质。实际上,该性质在源码中对应的代码就是code = (code + bl_count[bits-1])<< 1,后面我们详细分析这句话。看到这里,上面第四条性质的问题就可以解决了,f的码字是100,f所在的这层有三个叶子节点,那么再往下一层,这层没有叶子节点,但是我们可以假设一个不存在的叶子节点,这个不存在的叶子节点的编码要用f去计算,即,code = (100+11)<<1,11就是二进制的3,所以code就是1110,即这个不存在的叶子节点的编码就是1110,用它去计算a,继续套用这个公式,code = (1110+0)<<1,所以code就是11100,与性质是符合的!!!

g. 节点n的左子节点是2n,右子节点是2n+1;

 

其实deflate中使用的哈夫曼编码就是“范式哈夫曼编码”,范式哈夫曼编码的相关介绍如下:“范式哈夫曼编码最早由Schwartz提出,它是哈夫曼编码的一个子集。其中心思想是使用某些强制的约定,仅通过很少的数据便能重构出哈夫曼编码树的结构。其中一种很重要的约定是数字序列属性(numerical sequence property),它要求相同长度的码字是连续整数的二进制描述。例如,假设码字长度为4的最小值为0010,那么其它长度为4的码字必为0011, 0100, 0101, ...;另一个约定:为了尽可能的利用编码空间,长度为i第一个码字f(i)能从长度为i-1的最后一个码字得出, 即:f(i) = 2(f(i-1)+1)。假定长度为4的最后一个码字为1001,那么长度为5的第一个码字便为10100。最后一个约定:码字长度最小的第一个编码从0开始。通过上述约定,解码器能根据每个码字的长度恢复出整棵哈夫曼编码树的结构。”从上面的性质可以看出,我们这里使用的就是范式哈夫曼编码。

 

你可能感兴趣的:(GZIP压缩)