gzip压缩算法

 
如果你有时间的话,我建议你先不要看下面的内容,自己尝试通过读 gzip 源码,来了解它的压缩解压缩是如何实现的,这将会是一个非常有趣的智力游戏,千万不要错过。当一个又一个的谜被解开时,那感觉就像唐伯虎同志所说的, 慷慨然诺杯酒中 。(小唐的诗,除了另一个倒霉蛋曹雪芹外,好像不太被人提。)

   1 gzip 所使用压缩算法的基本原理

   gzip 对于要压缩的文件,首先使用 lz77 算法进行压缩,对得到的结果再使用 huffman 编码的方法进行压缩。所以我们分别对 lz77 huffman 编码的原理进行说明。

   1.1 ... 1.2 ...

   2 gzip 压缩算法实现方法

   2.1 LZ77 算法的 gzip 实现

   首先, gzip 从要压缩的文件中读入 64KB 的内容到一个叫 window 的缓冲区中。为了简单起见,我们以 32KB 以下文件的压缩为例做说明。对于我们这里使用 32KB 以下文件, gzip 将整个文件读入到 window 缓冲区中。然后使用一个叫 strstart 的变量在 window 数组中,从 0 开始一直向后移动。 strstart 在每一个位置上,都在它之前的区域中,寻找和当前 strstart 开始的串的头 3 个字节匹配的串,并试图从这些匹配串中找到最长的匹配串。

   如果当前的 strstart 开始的串,可以找到最少为 3 个字节的匹配串的话,当前的 strstart 开始的匹配长度那么长的串,将会被一个 < 匹配长度 , 到匹配串开头的距离 > 对替换。

   如果当前的 strstart 开始的串,找不到任何的最少为 3 个字节的匹配串的话,那么当前 strstart 的所在字节将不作改动。

   为了区分是一个 < 匹配长度 , 到匹配串开头的距离 > 对,还是一个没有被改动的字节,还需要为每一个没有被改动的字节或者 < 匹配长度 , 到匹配串开头的距离 > 对,另外再占用一
   位,来进行区分。这位如果为 1 ,表示是一个 < 匹配长度 , 到匹配串开头的距离 > 对,这位如果为 0 ,表示是一个没有被改动的字节。

   现在来说明一下,为什么最小匹配为 3 个字节。这是由于, gzip 中, < 匹配长度 , 到匹配串开头的距离 > 对中, " 匹配长度 " 的范围为 3-258 ,也就是 256 种可能值,需要 8bit 来保存。 " 到匹配串开头的距离 " 的范围为 0-32K ,需要 15bit 来保存。所以一个 < 匹配长度 , 到匹配串开头的距离 > 对需要 23 位,差一位 3 个字节。如果匹配串小于 3 个字节的话,使用 < 匹配长度 , 到匹配串开头的距离 > 对进行替换,不但没有压缩,反而还会增大。所以保存 < 匹配长度 , 到匹配串开头的距离 > 对所需要的位数,决定了最小匹配长度至少要为 3 个字节。

   下面我们就来介绍 gzip 如何实现寻找当前 strstart 开始的串的最长匹配串。

   如果每次为当前串寻找匹配串时,都要和之前的每个串的至少 3 个字节进行比较的话,那么比较量将是非常非常大的。为了提高比较速度, gzip 使用了哈希表。这是 gzip 实现 LZ77 的关键。这个哈希表是一个叫 head 的数组(后面我们将看到为什么这个缓冲区叫 head )。 gzip windows 中的每个串,使用串的头三个字节,也就是 strstart,strstart 1,strstart 2 ,用一个设计好的哈希函数来进行计算,得到一个插入位置 ins_h 。也就是用串的头三个字节来确定一个插入位置。然后把串的位置,也就是 strstart 的值,保存在 head 数组的第 ins_h 项中。我们马上就可以看到为什么要这样做。 head 数组在没有插入任何值时,全部为 0
当某处的当前串的三个字节确定了一个 ins_h ,并把当时当前串的位置也就是当时的 strstart 保存在了 head[ins_h] 中。之后另一处,当另一处的当前串的头三个字节,再为那三个字节时,再使用那个哈希函数来计算,由于是同样的三个字节,同样的哈希函数,得到的 ins_h 必然和前面得到的 ins_h 是相同的。于是就会发现 head[ins_h] 不为 0 。这就说明了,有一个头三个字节和自己相同的串把自己的位置保存在了这里,现在 head[ins_h] 中保存的值,也就是那个串的开始位置,我们就可以找到那个串,那个串至少前 3 个字节和当前串的前 3 个字节相同(稍后我们就可以看到这种说法不准确,这里是为了说明方便),我们可以找到那个串,做进一步比较,看到底能有多长的匹配。

   我们现在来说明一下,相同的三个字节,通过哈希函数得到的 ins_h 必然是相同的。而不同的三个字节,通过哈希函数有没有可能得到同一个 ins_h ,我没有对这个哈希函数做研究,并不清楚,不过一般的哈希函数都是这样的,所以极大可能这里的也会是这种情况,即不同的三个字节,通过哈希函数有可能得到同一个 ins_h ,不过这并不要紧,我们发现有可能是匹配串之后,还会进行串的比较。

   一个文件中,可能有很多个串的头三个字节都是相同的,也就是说他们计算得到的 ins_h 都是相同的,如何能保证找到他们中的每一个串呢? gzip 使用一个链把他们链在一起。 gzip 每次把当前串的位置插入 head 的当前串头三个字节算出的 ins_h 处时,都会首先把原来的 head[ins_h] 的值,保存到一个叫 prev 的数组中,保存的位置就在现在的 strstart 处。这样当以后某处的当前串计算出 ins_h ,发现 head[ins_h] 不空时,就可以到 prev[ head[ins_h] ] 中找到更前一个的头三个字节相同的串的位置。对此我们举例说明。

   例,串
   0abcdabceabcfabcg
   ^^^^^^^^^^^^^^^^^
   01234567890123456

   整个串被压缩程序处理之后。

   abc 算出 ins_h
   这时的 head[ins_h] 中为 13, "abcg" 的开始位置。
   这时 prev[13] 中为 9 ,即 "abcfabcg" 的开始位置。
   这时 prev[9] 中为 5 ,即 "abceabcfabcg" 的开始位置。
   这时 prev[5] 中为 1 ,即 "abcdabceabcfabcg" 的开始位置。
   这时 prev[1] 中为 0

   我们看到所有头三个字母为 abc 的串,被链在了一起,从 head 可以一直找下去,直到找到 0

   现在我们也就知道了,三个字节通过哈希函数计算得到同一 ins_h 的所有的串被链在了一起, head[ins_h] 为链头, prev 数组中放着的更早的串。这也就是 head prev 名称的由
   来。

   gzip 寻找匹配串的另外一个值得注意的实现是,延迟匹配。会进行两次尝试。比如当前串为 str, 那么 str 发生匹配以后,并不发生压缩,还会对 str 1 串进行匹配,然后看哪种
   匹配效果好。

   例子 ...
从这个例子中我们就看到了做另外一次尝试的原因。如果碰到的一个匹配就使用了的话,可能错过更长匹配的机会。现在做两次会有所改善。

   ...

   2.2 问题讨论

   我在这里对 gzip 压缩算法做出了一些说明,是希望可以和对 gzip 或者压缩解压缩感兴趣的朋友进行交流。
   我对 gzip 的了解要比这里说的更多一些,也有更多的例子。如果哪位朋友愿意对下面的问题进行研究,以及其他压缩解压缩的问题进行研究,来这里 http://jiurl.cosoft.org.cn/forum/ 和我交流的话,我也愿意就我知道的内容进行更多的说明。

   下面是几个问题

   这种匹配算法,即用 3 个字节 ( 最小匹配 ) 来计算一个整数,是否比用串比较来得高效,高效到什么程度。

   哈希函数的讨论。不同的三个字节,是否可能得到同一个 ins_h ins_h 和计算它的三个字节的关系。

   几次延迟尝试比较好?

   用延迟,两次尝试是否对压缩率的改善是非常有限的?

   影响 lz77 压缩率的因素。

   压缩的极限。

   2.3 ...

   3 gzip 源码分析

   main() 中调用函数 treat_file()
   treat_file() 中打开文件,调用函数 zip() 。注意这里的 work 的用法,这是一个函数指针。
   zip() 中输出 gzip 文件格式的头,调用 bi_init ct_init lm_init
   其中在 lm_init 中将 head 初始化清 0 。初始化 strstart 0 。从文件中读入 64KB 的内容到 window 缓冲区中。
   由于计算 strstart=0 时的 ins_h ,需要 0,1,2 这三个字节和哈希函数发生关系,所以在 lm_init 中,预读 0,1 两个字节,并和哈希函数发生关系。

   然后 lm_init 调用 deflate()
   deflate() gzip LZ77 的实现主要 deflate() 中。
 

你可能感兴趣的:(C/C++)