(注:以下關于技術細節的描述是以 gzip 的公開源代碼為基礎的,如果需要完整的代碼,可以在 gzip 的官方網站 www.gzip.org下載。下面提到的每一個問題,都首先介紹最直觀簡單的解決方法,然后指出這種方法的弊端所在,最后介紹 gzip 采用的做法,這樣也許能使讀者對 gzip 看似復雜、不直觀的做法的意義有更好的理解。)
最 直觀的搜索方式是順序搜索:以待壓縮部分的第一個字節與窗口中的每一個字節依次比較,當找到一個相等的字節時,再比較后續的字節…… 遍歷了窗口后得出最長匹配。gzip 用的是被稱作哈希表的方法來實現較高效的搜索。哈希(hash是分散的意思,把待搜索的數據按照字節值分散到一個個中,搜索時再根據字節 值到相應的中去尋找。短語式壓縮的最短匹配為 3 個字節,gzip 3 個字節的值作為哈希表的索引,但 3 個字節共有 2 24 次方種取值,需要 16M 個桶,桶里存放的是窗口中的位置值,窗口的大小為 32K,所以每個桶至少要有大于兩個字節的空間,哈希表將大于 32M,作為 90 年代開發的程序,這個要求是太大了,而且隨著窗口的移動,哈希表里的數據會不斷過時,維護這么大的表,會降低程序的效率,gzip 定義哈希表為 2 15 次方(32K)個桶,并設計了一個哈希函數把 16M 種取值對應到 32K 個桶中,不同的值被對應到相同的桶中是不可避免的,哈希函數的任務是

1.使各種取值盡可能均勻地分布到各個桶中,避免許多不同的值集中到某些桶中,而另一些是空桶,使搜索的效率降低。

2.函數的計算盡可能地簡單,因為每次插入搜尋哈希表都要執行哈希函數,哈希函數的復雜度直接影響程序的執行效率,容易想到的哈希函數是取 3 個字節的左邊(或右邊)15 位二進制值,但這樣只要左邊(或右邊)2 個字節相同,就會被放到同一個桶中,而 2 個字節相同的概率是比較高的,不符合平均分布的要求。

gzip 采用的算法是:A(4,5) + A(6,7,8) ^ B(1,2,3) + B(4,5) + B(6,7,8) ^ C(1,2,3) + C(4,5,6,7,8) (說明:A 3 個字節中的第 1 個字節,B 指第 2 個字節,C 指第 3 個字節,A(4,5) 指第一個字節的第 4,5 位二進制碼,“^”是二進制位的異或操作,“+”連接而不是“^”優先于“+”)這樣使 3 個字節都盡量參與到最后的結果中來,而且每個結果值 h 都等于 ((1h << 5) ^ c)取右 15 位,計算也還簡單。
哈希表的具體實現也值得探討,因為無法預先知道每一個會存放多少個元素,所以最簡單的,會想到用鏈表來實現:哈希表里存放著每個桶的第一個 元素,每個元素除了存放著自身的值,還存放著一個指針,指向同一個桶中的下一個元素,可以順著指針鏈來遍歷該桶中的每一個元素,插入元素時,先用哈希函數 算出該放到第幾個桶中,再把它掛到相應鏈表的最后。

這個方案的缺點是頻繁地申請和釋放內存會降低運行速度;內存指針的存放占據了額外的內存開銷。

有更少內 存開銷和更快速的方法來實現哈希表,并且不需要頻繁的內存申請和釋放:gzip 在內存中申請了兩個數組,一個叫 head[],一個叫 pre[],大小都為 32K,根據當前位置 strstart 開始的 3 個字節,用哈希函數計算出在 head[] 中的位置 ins_h,然后把 head[ins_h] 中的值記入 pre[strstart],再把當前位置 strstart 記入 head[ins_h]

隨著壓縮的進行,head[]里記載著最近的可能的匹配的位置(如果有匹配的話,head[ins_h]不為 0),pre[]中的所有位置與原始數據的位置相對應,但每一個位置保存的值是前一個最近的可能的匹配的位置。

可能的匹配是指哈希函數計算出的 ins_h 相同。)順著 pre[] 中的指示找下去,直到遇到 0,可以得到所有匹配在原始數據中的位置,0 表示不再有更遠的匹配。
  接下來很自然地要觀察 gzip 具體是如何判斷哈希表中數據的過時,如何清理哈希表的,因為 pre[] 里只能存放 32K 個元素,所以這項工作是必須要做的。
   gzip 從原始文件中讀出兩個窗口大小的內容(共 64K 字節)到一塊內存中,這塊內存也是一個數組,稱作 Window[];申請 head[]pre[] 并清零;strstart 置為 0

然后 gzip 邊搜索邊插入,搜索時通過計算 ins_h,檢查 head[] 中是否有匹配,如果有匹配,判斷 strstart head[] 中的位置是否大于 1 個窗口的大小,如果大于 1 個窗口的大小,就不到 pre[] 中去搜索了,因為 pre[] 中保存的位置更遠了,如果不大于,就順著 pre[] 的指示到 Window[] 中逐個匹配位置開始,逐個字節與當前位置的數據比較,以找出最長匹配,pre[] 中的位置也要判斷是否超出一個窗口,如遇到超出一個窗口的位置或者 0 就不再找下去,找不到匹配就輸出當前位置的單個字節到另外的內存(輸出方法在后文中會介紹),并把 strstart 插入哈希表,strstart 遞增,如果找到了匹配,就輸出匹配位置和匹配長度這兩個數字到另外的內存中,并把 strstart 開始的,直到 strstart + 匹配長度 為止的所有位置都插入哈希表,strstart += 匹配長度。插入哈希表的方法為:
pre[strstart % 32K] = head[ins_h];
head[ins_h] = strstart;
可 以看出,pre[] 是循環利用的,所有的位置都在一個窗口以內,但每一個位置保存的值不一定是一個窗口以內的。

在搜索時,head[] pre[] 中的位置值對應到 pre[] 時也要 % 32K。當 Window[] 中的原始數據將要處理完畢時,要把 Window[] 中后一窗的數據復制到前一窗,再讀取 32K 字節的數據到后一窗,strstart -= 32K,遍歷 head[],值小于等于 32K 的,置為 0,大于 32K 的,-= 32Kpre[] head[] 一樣處理。然后同前面一樣處理新一窗的數據。
  分析:現在可以 看到,雖然 3 個字節有 16M 種取值,但實際上一個窗口只有 32K 個取值需要插入哈希表,由于短語式重復的存在,實際只有 < 32K 種取值插入哈希表的 32K 中,而且哈希函數又符合平均分布的要求,所以哈希表中實際存在的沖突一般不會多,對搜索效率的影響不大。可以預計,在一般情況下,每 個中存放的數據,正是我們要找的。

哈希表在各種搜索算法中,實現相對的比較簡單,容易理解,平均搜索速度最快,哈希函數的設計是搜索速度的關 鍵,只要符合平均分布計算簡單,就常常能成為諸種搜索算法中的首選,所以哈希表是最流行的一種搜索算法。

但在某些特殊情況下,它也有缺點,比 如:1.當鍵碼 k 不存在時,要求找出小于 k 的最大鍵碼或大于 k 的最小鍵碼,哈希表無法有效率地滿足這種要求。2.哈希表的平均搜索速度是建立在概率論的基礎上的,因為事先不能預知待搜索的數據集合,我們只能信 賴搜索速度的平均值,而不能保證搜索速度的上限。在同人類性命攸關的應用中(如醫療或宇航領域),將是不合適的。

這些情況及其他一些特殊情 況下,我們必須求助其他平均速度較低,但能滿足相應的特殊要求的算法。(見《計算機程序設計藝術》第3卷 排序與查找)。幸而在窗口中搜索匹配字節串不屬于特殊情況。

時間與壓縮率的平衡:
gzip
定義了幾種可供選擇的 level,越低的 level 壓縮時間越快但壓縮率越低,越高的 level 壓縮時間越慢但壓縮率越高。
不同的 level 對下面四個變量有不同的取值:

nice_length
max_chain
max_lazy
good_length

nice_length
: 前面說過,搜索匹配時,順著 pre[] 的指示到 Window[] 中逐個匹配位置開始,找出最長匹配,但在這過程中,如果遇到一個匹配的長度達到或超過 nice_length,就不再試圖尋找更長的匹配。最低的 level 定義 nice_length 8,最高的 level 定義 nice_length 258(即一個字節能表示的最大短語匹配長度 3 + 255)。

max_chain
:這個值規定了順著 pre[] 的指示往前回溯的最大次數。最低的 level 定義 max_chain 4,最高的 level 定義 max_chain 4096。當 max_chain nice_length 有沖突時,以先達到的為準。