ZIP壓縮算法詳細分析及解壓實例解釋----其他關鍵點補充:deflate樹和deflate編碼、字符串匹配等

本文之前一篇壓縮算法中沒提到的一些關鍵知識點的補充ZIP壓縮算法詳細分析及解壓實例解釋

deflate樹及deflate編碼

我們知道交換哈夫曼樹的任意兩顆子樹依然還是哈夫曼樹,交換的過程雖然會改變葉子結點的huffman編碼,但是,不會改變huffman樹的帶權路徑和,也不會改變每個葉子結點的編碼長度。基於這一點,我們可以做個更特殊的變換,每一層,讓非葉子結點排在右邊,葉子結點排在非葉子結點的左邊。

假設以 6、9、10、11、15、16 建立一顆哈夫曼樹:

    

葉子節點對應的碼錶就是:

16  --> 00
15  --> 10
10  --> 010
11  --> 011
6   --> 110
9   --> 111

轉成deflate樹:

       

葉子節點對應的碼錶:

16  --> 00
15  --> 10
10  --> 100
11  --> 101
6   --> 110
9   --> 111

雖然元素的編碼變化了,但只要我們記錄如上這個碼錶,還是能把數據還原的。

前邊說過,deflate雖然改變了編碼,但是每個元素的編碼長度是不變的,這個時候,可以只記錄每個元素的編碼長度,就可以在解碼的時候把數據還原。現在,碼錶這麼記錄,每一層,從左往右記錄葉子結點的編碼長度,層次按從上到下。先記錄第2層(根節點爲第0層)的兩個葉子,再記錄第三次的4個葉子,碼錶如下:

16  --> 2
15  --> 2
10  --> 3
11  --> 3
6   -->  3
9   -->  3

對比下這兩種記錄法,會發現,下面這種碼錶記錄要比上面的碼錶記錄節省比特,2的二進制位10 ,3的二進制位11 ,總的比特位6*2=12。

而按照哈夫曼的碼錶長度爲2+2+3+3+3+3=16(15、14的編碼長度2,9、10、5、8的編碼長度爲3)。這並不是偶然,因爲一個元素的編碼的長度(10的編碼長度爲3)所佔的二進制比特位(10的編碼長度3,佔二進制2位)肯定小於等於編碼所佔的長度(10的編碼長度3)。

LZ77匹配相同字符串

爲一個串尋找匹配串需要進行大量的匹配工作,而且我們還需要爲很多很多個串尋找匹配串。在尋找匹配串的實現中使用哈希表來提高速度。
要達到的目標是,對於當前串,我們要在它之前的窗口中,尋找每一個匹配長度達到最小匹配的串,並找出匹配長度最長的串。最小匹配長度爲3,也就是說,兩個串,最少要前3個字節相同,才能算作匹配。爲什麼最小匹配長度爲3,將在後面說明。
插入的時候,使用這個插入串的前三個字節,計算出插入的“字典”位置,然後把插入串的開始位置保存在這個“字典”位置中。
如果有多個串,他們的前三個字節都相同,那麼他們的“字典”位置,也都是相同的,他們將被鏈成一條鏈,放在那個“字典”位置上。所以,如果一個串,查到了一個“字典”位置,也就查到了一個鏈,所有和它前三個字節相同的串,都在這個鏈上。

也就是說,當前串之前的所有匹配串被鏈在了一個鏈上,放在某個“字典”位置上。而當前串使用它的前三個字節,進行某種計算,就可以得到這個“字典”位置(得到了“字典”位置之後,它首先也把自己鏈入到這個鏈上),也就找到了鏈有它的所有匹配串的鏈,所以要找最長的匹配,也就是遍歷這個鏈上的每一個串,看和哪個串的匹配長度最大。

下面我們更具體的說明,尋找匹配串的實現。
我們前面所說的“字典”,是一個數組,叫做head[](爲什麼叫head,後面進行說明)。
我們前面所說的“字典”位置,放在一個叫做ins_h的變量中。
我們前面所說的鏈,是在一個叫做prev[]的數組中。

 

字典插入

當前字節爲第 strstart 個字節。通過第strstart,strstart+1,strstart+2,這三個字節,使用一個設計好的哈希函數算出ins_h,也就是插入的位置。然後將當前字節的位置,即strstart,保存在head[ins_h]中。
注意由 strstart,strstart+1,strstart+2,這三個字節(也就是strstart開始處的串的頭三個字節,也就是當前字節和之後的兩個字節)確定了ins_h。head[ins_h]中保存的又是strstart,也就是這個串開始的位置。

當前串的前三個字節,使用哈希函數算出ins_h,這時如果head[ins_h]的值不爲空的話,那麼head[ins_h]中的值,便是之前保存在這裏的另一個串的位置,並且這個串的前三個字節算出的ins_h,和當前串的前三個字節算出的ins_h相同。也就是說有可能有匹配。如果head[ins_h]的值爲空的話,那麼肯定沒有匹配。

對於相同的三個字節,通過哈希函數得到的ins_h必然是相同的。
而不同的三個字節,通過哈希函數有可能得到同一個ins_h,不過這並不要緊,
當發現head[ins_h]不空後,也就是說有可能有匹配串的話,會對鏈上的每一個串進行真正的串的比較。

head[ins_h] 中的值,有兩個作用。一個作用,是一個前三個字節計算結果爲ins_h的串的位置。另一個作用,是一個在prev[]數組中的索引,用這個索引在prev[]中,將找到前一個前三個字節計算結果爲ins_h的串的位置。即prev[head[ins_h]]的值(不爲空的話)爲前一個前三個字節計算結果爲ins_h的串的位置。

prev[]的值,也有兩個作用。一個作用是一個前三個字節計算結果爲ins_h的串的位置。另一個作用是一個在prev[]數組中的索引,用這個索引在prev[]中,將找到前一個前三個字節計算結果爲ins_h的串的位置。即prev[]的值(不爲空的話)爲前一個三個字節計算結果爲ins_h的串的位置。直到prev[]爲空,表示鏈結束。

我們來舉一個例子,串,
0abcd abce,abcf_abcg
當處理到abcg的a時,由abcg的abc算出ins_h。
這時的head[ins_h]中爲 11,即串"abcf abcg"的開始位置。
這時的prev[11]中爲 6,即串"abce abcf abcg"的開始位置。
這時的prev[6]中爲 1,即串"abcd abce abcf abcg"的開始位置。
這時的prev[1]中爲 0。表示鏈結束了。
我們看到所有頭三個字母爲abc的串,被鏈在了一起,從head可以一直找下去,直到找到0。

衝突鏈建立

每次處理當前串的時候,首先用當前串的前三個字節計算出ins_h,然後,就要把當前的串也插入到相應的鏈中,也就是把當前的串的位置,保存到 head[ins_h] 中,而此時,head[ins_h] 中(不空的話)爲前一個串的開始位置。所以這時候需要把前一個串的位置,也就是原來的head[ins_h]放入鏈中。於是把現在的head[ins_h]的值,用當前串的位置做索引,保存到 prev[] 中。然後再把 head[ins_h] 賦值爲當前串的位置。

如果當前串的位置爲strstart的話,那麼也就是
prev[strstart] = head[ins_h];
head[ins_h] = strstart;
就這樣,每次把一個串的位置加入到鏈中,鏈就形成了。
現在我們也就知道了,前三個字節計算得到同一ins_h的所有的串被鏈在了一起,head[ins_h]爲鏈頭,prev[]數組中放着的更早的串的位置。head數組和prev數組的名字,也正反應了他們的作用。

衝突鏈的特點

越向前(prev)與當前處理位置之間的距離越大。比如,當前處理串,算出了ins_h,而且head[ins_h]中的值不空,那麼head[ins_h]就是離當前處理串距離最近的一個可能的匹配串,並且順着prev[]向前所找到的串,越來距離越遠。

匹配串處理

我們說過了所有字節開始的串,都將被插入“字典”。對於確定了的匹配串,匹配串中的每個字節開始的串,仍要被插入“字典”,以便後面串可以和他們進行匹配。

惰性匹配(lazy match)

對於當前字節開始的串,尋找到了最長匹配之後並不立即決定使用這個串進行替換。而是看看這個匹配長度是否滿意,如果匹配長度不滿意,而下一個字節開始的串也有匹配串的話,那麼就找到下一個字節開始的串的最長匹配,看看是不是比現在這個長。這就是惰性匹配。如果比現在這個長的話,將不使用現在的這個匹配。如果比現在這個短的話,將確定使用現在的這個匹配。

我們來舉個例子,串
abc bcde abcde
處理到第10字節時,也就是"abcde"的a時,找到最長匹配的情況如下,[]所括部分。
abc bcde [abc]de
這時,再看看下一個字節,也就是第11字節的情況,也就是'abcde"的b,找到最長匹配的情況如下,[]所括部分。

0abc bcde a[bcde]
發現第二次匹配的匹配長度大,就不使用第一次的匹配串。我們也看到了如果使用第一次匹配的話,將錯過更長的匹配串。
在滿足惰性匹配的前提條件下,惰性匹配不限制次數,一次惰性匹配發現了更長的匹配串之後,仍會再進行惰性匹配,如果這次惰性匹配,發現了更長的匹配串,那麼上一次的惰性匹配找到的匹配串就不用了。

窗口的實現

實際中,窗口大小爲 WSIZE一般是32KB。

內存中有一個叫window[]的緩衝區,大小爲2個窗口的大小,也就是64KB。文件的內容將被讀到這個window[]中,我們在window[]上進行LZ77部分的處理,得到結果將放在其他緩衝區中。對window[]中的內容,從開始處開始,一個字節一個字節的向後處理。有一個指針叫strstart(其實是個索引),指向當前處理字節,噹噹前處理字節開始的串沒有匹配時,不做改動的輸出當前處理字節,strstart向後移動一個字節。當前處理字節開始的串找到了匹配時,輸出(匹配長度,相隔距離)對,strstart向後移動匹配長度個字節。我們把strstart到window[]結束的這部分內容,叫做 lookahead buffer。在一個變量lookahead中保存着lookahead buffer所剩的字節數。lookahead最開始被初始化爲整個讀入內容的大小,隨着處理的進行,strstart不斷後移,超前查看緩衝區不斷減小,lookahead的值也不斷的減小。

我們只能在當前處理字節之前的32KB的範圍內尋找匹配串。由於處理是在2個窗口大小,也就是64KB大小的緩衝區中進行的,所以匹配鏈上的串與當前串之間的距離是很有可能超過32KB的。當前串計算ins_h,發現head[ins_h]值不爲空時(head[ins_h]爲一個串的開始位置),在prev衝突鏈表上向前查找時限制只能查找最遠32K範圍內數據。


每處理一個字節都要判斷 lookahead < MIN_LOOKAHEAD ,也就是window中還沒有處理的字節是否還夠MIN_LOOKAHEAD ,如果不夠的話,就會導致 fill_window(),從文件中讀內容到window[]中。
不管是大於64KB的文件,還是小於64KB的文件,隨着處理的進行,最終都要到文件的結束,在接近文件結束的時候,都會出現 lookahead < MIN_LOOKAHEAD ,對於這種情況,fill_window() 讀文件,就再讀不出文件內容了,於是fill_window()會設置一個標誌eofile,表示文件就要結束了,之後肯定會接着遇到 lookahead < MIN_LOOKAHEAD ,不過由於設置了 eofile 標誌,就不會再去試圖讀文件到window[]中了。
壓縮開始之前會進行初始化,先從文件中讀入64KB的內容到window[]中,窗口大小爲32KB,也就是讀入2窗的內容到window[]中。我們把第一窗的內容叫做w1_32k,第二窗的內容叫做w2_32k。

壓縮不斷進行,直到 lookahead < MIN_LOOKAHEAD,也就是處理到了64KB內容的接近結束部分,也就是如果再處理,超前查看緩衝區中的內容就可能不夠了。由於 lookahead < MIN_LOOKAHEAD ,將執行 fill_window()。

fill_window() 判斷是否壓縮已經進行到了2窗內容快用完了,該把新的內容放進來了。如果是的話就把第二窗的內容 w2_32k,複製到第一窗中,第一窗中的內容就被覆蓋掉了,然後對match_start,strstart之類的索引做修正。緊接着更新匹配鏈的鏈頭數組head[],從頭到尾過一遍,如果這個頭中保存的串的位置,在w2_32k中,就對這個串的位置做修正,如果這個頭中保存的串的位置,在w1_32k中,就不要了,設爲空,因爲第一窗的內容我們已經覆蓋掉了。最後更新prev[]數組,從頭到尾過一遍,如果某項的內容,在w2_32k中,就做修正。如果這項的內容,在w1_32k中,就不要了,設爲空。清理完後,fill_window()從文件中再讀出一窗內容,也就是讀出32KB的內容,複製到第二個窗中。就這樣,一窗窗的處理,直到整個文件結束。

如果小於64KB的文件,初始化的時候,會首先從文件中讀64KB的內容到window[]中。對於小於64KB的文件,整個文件都被讀入到window[]中。在window[]上進行LZ77的處理,從開始直到文件結束。

靜態Huffman編碼與動態Huffman編碼

靜態Huffman編碼就是使用自己預先定義好了一套編碼進行壓縮,解壓縮的時候也使用這套編碼,這樣不需要傳遞用來生成樹的信息。
動態Huffman編碼就是使用統計好的各個符號的出現次數,建立Huffman樹,產生各個符號的Huffman編碼,用這產生的Huffman編碼進行壓縮,這樣需要傳遞生成樹的信息。
對於靜態樹來說,不需要傳遞用來生成樹的那部分信息。動態樹需要傳遞這個信息。而當文件比較小的時候,傳遞生成樹的信息得不償失,反而會使壓縮文件變大。也就是說對於文件比較小的時候,就可能會出現使用靜態Huffman編碼比使用動態Huffman編碼,生成的塊小。

其他

(1)、爲什麼最小匹配長度爲3個字節。這是由於 (匹配長度,相隔距離)對中,"匹配長度"的範圍爲3-258,也就是256種可能值,需要8bit來保存。"相隔距離"的範圍爲0-32K,需要15bit來保存。所以一個(匹配長度,相隔距離)對需要23位,差一位3個字節。如果匹配串小於3個字節的話,使用(匹配長度,相隔距離)對進行替換,不但沒有壓縮,反而還會增大。所以保存(匹配長度,相隔距離)對所需要的位數,決定了最小匹配長度至少要爲3個字節。

(2)、最大匹配長度爲258的原因是,綜合各種因素,決定用8位來保存匹配長度,8位的最大值爲255。實際中,我們在(匹配長度,相隔距離)對中的“匹配長度”保存的是,實際匹配長度-最小匹配長度(3),所以255對應的實際匹配長度爲258。在進行匹配時,會對匹配長度進行判斷,保證到達最大匹配長度時,匹配就停止。也就是說,即使有兩個串的相同部分超過了最大匹配長度,也只匹配到最大匹配長度。

(3)、保存相隔距離所用的位數和窗口大小是互相決定的,綜合兩方面各種因素,確定了窗口大小,也就確定了保存相隔距離所使用的位數。

(4)、LZ77壓縮輸出:
l_buf[] 中的每個字節是一個沒有匹配的字節或者是一個匹配的對中的匹配長度-3。
d_buf[] 中的每個unsigned short,是一個匹配的對中的相隔距離。
flag_buf[] 中每位是一個標誌,用來指示l_buf[]中相應字節是沒有匹配的字節,還是一個匹配的對中的匹配長度-3。


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章