zip 的压缩原理与实现 (4)

3.4 对码表进行第二次压缩。
  目前为止,码表中只需要保存各个节点经过 huffman 编码后的新编码的码长。共两棵树,l_tree: 256 个原始字节值加 29 个长度范围值加 1 个段落中止符,共 286 个节点,段落中止符用来在解压时标示一个段落的终结。d_tree: 30 个距离范围值。也就是说,共需要保存 286 + 30 = 316 个编码的码长。gzip 限制 huffman 树的最大层数为 15,这样,码长就有 0 - 15 共 16 种值,再加上前面介绍过的去重复机制使用的 3 种特殊值,共 19 种值,如果就这样保存码表的话,每个码长都需要 5 位,才能表示 19 种值。我们观察一下,316 个码长,一共只有 19 种值,码长值的重复是必然的,而且由于 huffman 树上每层的节点数不同,所以各个码长值的频率也不一样。所以还可以为这 19 种值再建 huffman 树,进行第二次编码。这棵树只有 19 个节点,限制它的层数为 0 - 7,可以用 3 个 bit 表示这 19 个节点的“长度”。这样,用新的“码长的编码”来保存 316 个码长,另需额外保存 3 * 19 = 57 bit,就可以解压出这 19 个“码长的编码”。(至于这 57 bit,就没有必要再作第 3 次编码了)


4. 解决了码表的问题,现在再回过头来看静态编码。
  静态编码是 gzip 预先设定的编码方案,它的码表是固定的。
  该如何合理设计这套编码?作为 huffman 编码的补助,它的耗时应尽量少,前面说过,lz77 输出一个分段之前,要比较 huffman 编码和静态编码的压缩结果,为了直接利用 lz77 输出时做的匹配长度范围、匹配距离范围的频率的统计,静态编码采用了同样的范围-附加码的方案,这样可以快速得到静态编码的压缩结果大小。
  静态编码的码长的分配是这样的:29 个长度范围中前 24 个范围的码长为 7,后 5 个范围的码长为8。原始字节值中 0 - 143 的码长为 8,144 - 255 的码长为 9。而 30 个距离范围的码长为 5。根据这些预先设定的码长建立静态的 l_tree 和 d_tree,编码也就产生了。结合前面提到的附加码位数的定义:
29 个长度范围的附加码位长:
{0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0};
30 个距离范围的附加码位长:
{0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13};
读者可以知道每一个值的实际码长。长度范围值和原始字节值建在一棵树上,节点多所以码长较长,30 个距离范围值只需要 5 位二进制数表示。短匹配的长度范围值位长较短,字节值 0 - 143 的位长中等,其他字节值和长匹配的长度范围值较长。这样的分配反映了 gzip 作者对“大多数”文件中各种值的频率的粗略估计。作为一个通用的压缩算法,无法预先知道一个文件的实际情况,不可能做精确的估计。
  进一步的思考:静态编码有必要吗?静态编码采用了和 huffman 编码相同的范围-附加码的方案,在码长的分配上不可能超过 huffman 编码,如果能“获胜”,那就是胜在不需要保存码表上,而前面分析过,码表是很小的,对压缩率没有多大影响,所以 gzip 设计的这个静态编码方案应该是可有可无的。

5. 关于堆排序算法。
  似乎已经解决了所有的难题,但是对于没有学过数据结构的读者,仍然有一个会对程序效率产生影响的问题需要关注,那就是“排序”。
  已经讲过,huffman 算法就是从一个节点序列中,不断找出两个最小的节点,为它们建一个父节点,值为这两个节点之和,然后从节点序列中去除这两个节点,加入它们的父节点到序列中,不断重复这样的步骤,直到节点序列中只剩下一个节点。如何快速地找出最小的元素呢?
  在普通的线性罗列的数据结构中,从 N 个元素中找出最小的元素的时间和 N 成正比,如果数据以我们所要介绍的“堆”的结构存储,时间和 lg N 成正比(注:lg 以 2 为底数,如 lg 256 = 8,lg 1024 = 10 ...)。 集合中的元素越多,堆排序算法的优势越突出,而且堆排序非常适合于在数据序列中不断地取走最小的元素并加入新的元素。

5.1 什么是堆?
  堆首先是一棵“完全二叉树”,即所有的叶子节点都在树的最低二层,最低一层的节点依次靠左排列的二叉树。如图:

                       完全二叉树
                         |
              +----------○----------+
              |                     |
      +-------○------+          +---○---+
      |              |          |       |
  +---○---+      +---○---+    +-○-+   +-○-+
  |       |      |       |    |   |   |   |
+-○-+   +-○-+  +-○-+   +-○-+  ■   ■   ■   ■
|   |   |   |  |   |   |   |
■   ■   ■   ■  ■   ■   ■   ■


  堆分大根堆和小根堆,大根堆的所有子节点都小于它的父节点,小根堆的所有子节点都大于它的父节点。下面就是一个小根堆:

                         小根堆
                          |
              +-----------2----------+
              |                      |
      +-------3------+          +----8---+
      |              |          |        |
  +---6---+      +---4---+    +-15-+   +-18-+
  |       |      |       |    |    |   |    |
+-8-+   +-9-+  +-5-+   +-5-+  16  20   19   20
|   |   |   |  |   |   |   |
9   9  11  13  6   8   6   6

5.2 堆如何在内存中存储?
  堆存放在一个数组中,存放的顺序是:从根开始,依次存放每一层从左至右的节点。
5.3 如何寻找任意节点的子节点和父节点?
  数组中第 k 个元素,它的左子节点是第 2k 个元素,右子节点是第 2k + 1 个元素。它的父节点是┖ k/2 ┚(注:┖ X ┚表示小于等于 X 的最大整数)。
5.4 如何建立堆?
  先把 n 个元素依次放入数组中,令变量 k = ┖ n/2 ┚,这时第 k 个元素是最后一个元素的父节点,从第 k 个元素的两个子节点中找出较小的一个与 k 元素比较,如果小于 k 元素,就和 k 元素交换一下位置,换位后的原先的 k 元素再和新的子节点比较(如果有子节点的话),直到它不再小于新的子节点或没有子节点。令 k = k - 1。再重复上面的做法直到 k < 1,一个堆就建成了。
5.5 如何从堆中找出第二个最小的元素?
  把堆中第一个元素(最小的元素)存放到其他地方,把第 n 个元素(最后一个)放到第一个的位置,再用前面的方法和下层节点交换直到它放到合适的位置,这时数组仍然是一个堆,第一个元素是最小的节点,数组的最后一个有效节点是第 n - 1 个元素。
  花费的时间和交换的次数成正比,最大的可能的交换次数是: 堆的层数 - 1 =┏ lg (元素数 + 1) ┒- 1(注:┏ X ┒表示大于等于 X 的最小整数)。
  现在可以看到,堆之所以采用完全二叉树的形式,是为了树的层数尽可能少。
  而抽出最后一个元素放到树根,而不是抽出第二层的元素,是为了维持完全二叉树的结构!
5.6 如何加入新的元素到堆中?
  把第一个元素存放到其他地方,把新的元素放到第一个的位置,再用前面的方法和下层节点交换,直到它被放到合适的位置,此时数组中仍然是一个堆。

6. 建 huffman 树和编码的算法:
  如果现在有 n 个待编码的节点,按照原始数值从小到大存放在数组 tree[n] 中,那么,将要建立的 huffman 树总共会有 2n -1 节点,包括叶子节点和非叶子节点。申请一块内存,大小是能放下 huffman 树的所有节点,先把 n 个待编码节点放入这块内存的左端,然后用“堆排序”算法先把它们建成一个堆。
  然后不断用“堆排序”算法取出频率最小的节点,把它们从右到左、从小到大排放在内存块的右端,每当取出两个节点,给它们生成一个父节点,频率等于它们之和,加入堆中。这样直到堆中只剩下一个根节点,这时,内存中从左到右存储的是频率从大到小的所有节点,一棵 huffman 树其实也就建成了,层数小的节点在前,层数大的节点在后,每一层的节点又是按频率从大到小依次排列。
  申请两个数组:bl_count[],bl_base[]。置根节点的码长为 0,从左至右,所有节点的码长(len)为它的父节点的码长 + 1,如果是叶子节点,bl_count[len]++,得到了每一层上的叶子节点数目。令变量 code = 0,然后根据 bl_count[] 生成 bl_base[]:码长 len 从 1 开始递增,bl_base[len] = code = (code + bl_count[len - 1]) << 1,得到了每一层上第一个叶子节点的编码。
  现在所有待编码节点都被赋予了码长,遍历待编码节点,根据它们的码长得到它们的编码:序号 n 递增,tree[n].code = bl_base[ tree[n].len ] ++。
  注意:我们前面讨论码表的时候说过,gzip 对 huffman 编码进行了改进,只需要得到每一个叶子节点(待编码节点)的码长,就可以进行编码,而不需要关心它的父节点的编码是什么。而保存码表时,只需要保存码长。

动态 huffman 压缩和解压的整个流程:
压缩:
  lz77 的压缩过程中输出未匹配的单双字节,和匹配,并统计各字节值和匹配长度范围、匹配距离范围的频率,根据这些频率建立两棵 huffman 树:ltree、dtree,得到这两棵树上所有节点的长度和编码。
  统计这两棵树节点长度的使用频率,对各节点长度建立 huffman 树:bl_tree,得到 bl_tree 的长度和编码。
  存储 bl_tree 的节点长度数组。
  再用 bl_tree 的编码存储 ltree、dtree 的节点长度数组。
  再用 ltree 的编码存储各字节值和匹配长度范围(及附加码)的流;用 dtree 的编码存储匹配距离范围(及附加码)的流。
解压:
  先根据 bl_tree 的节点长度数组得到 bl_tree 的编码。
  再用这些编码得到 ltree、dtree 的节点长度数组,进而得到 ltree、dtree 的编码。
  再根据 ltree、dtree 的编码及附加码的定义,得到 lz77 的输出的原始结果:各字节值和匹配长度的流,匹配距离的流。

你可能感兴趣的:(zip)