1. 程式人生 > >GZIP壓縮原理分析(31)——第五章 Deflate演算法詳解(五22) 動態哈夫曼編碼分析(11)構建哈夫曼樹(03)

GZIP壓縮原理分析(31)——第五章 Deflate演算法詳解(五22) 動態哈夫曼編碼分析(11)構建哈夫曼樹(03)

*構建distance樹

現在已經知道壓縮會在壓縮結果中儲存葉子節點深度資訊(即碼字長度)從而讓解壓方間接得到碼錶,但是問題來了,構造樹的資訊只包括碼字長度,可解壓方怎麼知道這個碼字長度是哪個原碼的(注意,“原碼”與“原始碼”的差別,前者是指原始資料,後者是指程式碼)?有什麼方法可以讓解壓方以最簡單的方式知道碼字長度和原碼的關係?帶著這些問題,我們先來分析distance樹。

查詢緩衝區最大32KB(實際的程式碼實現中比這個要稍微小一些),也就是說,distance最大值就是32768,而且不能為0,所以distance的範圍就是閉區間[1, 32768]。哈夫曼編碼是根據字元出現頻率自底向上構建哈夫曼樹的,所以如果要構建distance樹,那就要在[1, 32768]的範圍內,根據不同的distance出現頻率來構建一棵哈夫曼樹,這棵樹的葉子節點就是出現在當前待壓縮資料中的distance值。假設待壓縮資料比較大,那麼其中的很多字串都可以用長度距離對兒替換,也就是說,在[1, 32768]這個範圍內的很多distance值都會成為長度距離對兒中的那個distance,如果讓這些distance值全部參加哈夫曼樹的構建,那這棵樹要達到何種規模!舉個例子,假設[1, 32768]範圍內有一千個distance值成為了長度距離對兒中的那個“距離”,讓這一千個distance值作為哈夫曼樹的葉子節點,這棵哈夫曼樹得有多大!為了避免這種情況的發生,壓縮使用了一種特殊的處理方法,簡化哈夫曼樹的規模,同時優化壓縮流程。

壓縮將範圍[1, 32768]細化為三十個規模不等的區間,每個區間容納不同數量的distance值(這些distance值是連續的),併為這些區間編號。如下圖所示,

圖中Code列就是區間編號,簡稱區間碼(十進位制),範圍是閉區間[0, 29];Distance列是該區間所容納的distance值範圍;Extra bits列暫時不管,後面介紹。前四個區間,即區間碼範圍為閉區間[0, 3]的區間,都有且只有一個distance值;再往後,每個區間所容納的distance個數逐漸增加。注意從區間碼16開始,往後的每個區間所容納的distance個數,都是128的倍數,這不是巧合,而是“刻意為之”,後面原始碼分析我們會看到這種倍數關係到底能帶來什麼好處!

有了這三十個區間,就可以避免直接對distance編碼,因為壓縮只會對這三十個區間的區間碼進行編碼。拿著LZ77之後的結果,判斷結果中的distance值都分別落在了哪個區間,然後對這些區間的區間碼進行哈夫曼編碼即可。由於只有三十個區間,所以就算這三十個區間同時參與哈夫曼編碼,那對應哈夫曼樹也不會太大,更何況參與編碼的區間如果達不到三十個的時候呢。

那問題來了,這三十個區間中,大部分的區間容納的distance都不止一個,如果LZ77之後的結果中有很多不同的distance值都落到了同一個區間,怎麼辦?不是隻對區間碼進行哈夫曼編碼麼,現在咋辦,怎麼用這一個區間碼把該區間下所有的distance值區分出來?上圖中的Extra bits列就是用來解決這個問題的。Extra bits列表示“擴充套件位”,該列的值表示擴充套件位長度,仔細觀察可以發現,擴充套件位長度與該區間容納distance值個數是對應的(強調一下,其實沒必要說吧~)。比如區間碼4,該區間有兩個distance,所以擴充套件位只有一位;區間碼16,該區間有一百二十八個distance值,所以擴充套件位有七位。每個區間中的distance值都是連續的,所以擴充套件位表示的值從零開始到該擴充套件位能表示的最大值就可以將該區間中的每個distance編碼。比如區間碼5,擴充套件位長度是一,該區間容納兩個distance,所以distance_7的擴充套件值是0,distance_8的擴充套件值是1;區間碼9,擴充套件位長度是三,該區間容納八個distance值,所以distance_25的擴充套件值是二進位制“000”,distance_26的擴充套件值是二進位制“001”,distance_27的擴充套件值是二進位制“010”……distance_31的擴充套件值是二進位制“110”,distance_32的擴充套件值是二進位制“111”。各區間容納的distance值數量不同,換句話說就是各個區間的疏密程度不同。Distance值越小,區間劃分越密;distance值越大,區間劃分越稀疏。這還是與我們很早之前提到的那個“隱含假設”有關,相同的內容總是扎堆兒出現,越是小的distance值,其出現頻率也就越高;出現頻率越高的內容,其碼字長度就應該儘可能的短。區間碼的碼字長度會因待壓縮內容不同而不同,會依據實際情況變化;擴充套件值的長度是固定的,只會因所屬區間不同而變化,遵照“隱含假設”所述,distance值越小,出現頻率越高,所以擴充套件位長度越短,並且區間劃分越密集。

哈夫曼編碼是針對區間碼進行的,因為編碼結果是字首碼,所以經過哈夫曼編碼後的區間碼碼字各不相同,都是唯一的,所以,用“區間碼碼字+擴充套件值”就可以唯一標識一個distance碼字。比如區間4和區間9的哈夫曼編碼碼字分別為二進位制“0010”和“0111”,distance_5的擴充套件值是二進位制的“0”, distance_27的擴充套件值是二進位制“010”,所以distance_5最終作為壓縮結果的碼字就是distance_5 = 0010 0,distance_27最終作為壓縮結果的碼字是distance_27 = 0111 010。這就是合成distance碼字的規則,壓縮/解壓縮雙方都知道這個規則。

Distance值的合成規則、擴充套件值的計算規則、distance區間碼範圍表,壓縮/解壓縮雙方都知道,現在解壓縮一方不知道的就是如何構造哈夫曼樹從而把區間碼碼錶得到。如果得到了區間碼碼錶,再按照擴充套件值計算規則在解壓縮本地把各區間的擴充套件值計算出來,就能夠根據合成規則得到distance碼錶,對著distance碼錶就能把所有distance解碼。我們已經知道壓縮結果中儲存的用於構建哈夫曼樹的資訊是碼字長度,而哈夫曼編碼又是針對區間碼進行的,所以現在問題就聚焦在壓縮結果怎麼記錄區間碼的碼字長度上。

區間碼由[0, 29]這三十個連續的整陣列成,假設一個有三十個元素的陣列,這些區間碼就是這個陣列的下標,而陣列元素就是下標對應的區間碼碼字長度,這樣就可以把碼字長度記錄下來了。壓縮結果記錄區間碼碼字長度的方式基本就是這樣,只不過沒有“陣列”這個實體,只是用的陣列的“原理”。例如有如下碼字長度序列,

0、0、1、2、3、3、0、0、0、0、0……(一共三十個數)

這幾個數就是碼字長度,而這幾個數的排列順序就是陣列下標,陣列下標就是對應的區間碼。第0個數是0,相當於陣列下標是0,陣列元素也是0,所以區間碼0的碼字長度是0;第1個數是0,相當於陣列下標是1,陣列元素是0,所以區間碼1的碼字長度是0;第2個數是1,相當於陣列下標是2,陣列元素是1,所以區間碼2的碼字長度是1;第3個數是2,相當於陣列下標是3,陣列元素是2,所以區間碼3的碼字長度是2;第4個數是3,相當於陣列下標是4,陣列元素是3,所以區間碼4的碼字長度是3;第5個數是3,相當於陣列下標是5,陣列元素是3,所以區間碼5的碼字長度是3,以此類推,一直到第29個數,也就是區間碼29的碼字長度。這就是壓縮結果記錄區間碼碼字長度的方法。

前面我們依次分析了“對誰進行哈夫曼編碼”、“如何合成實際的distance碼字”以及“壓縮結果如何記錄構建樹資訊”的問題,現在我們介紹構建哈夫曼樹以及得到區間碼的哈夫曼編碼碼字的具體流程。

壓縮只對區間碼進行哈夫曼編碼,也就是說,哈夫曼樹的葉子節點最多也就三十個。落在區間中的distance數量就是該區間的區間碼出現頻率,利用出現頻率自底向上構建哈夫曼樹,構建過程和預備知識中的描述相同。此時構建的哈夫曼樹是原始的哈夫曼樹,如下圖所示的哈夫曼樹是有可能出現的,

我們說過,壓縮使用的哈夫曼編碼是正規化哈夫曼編碼,這樣的樹顯然不符合正規化哈夫曼編碼的各個性質,不能用來編碼。大家是否還記得這張圖,

雖然原始哈夫曼樹有可能(不排除構建的哈夫曼樹恰好就是正規化哈夫曼樹)不符合正規化哈夫曼樹的性質,但各個葉子節點的深度,也就是碼字長度,這兩棵樹都是相同的!壓縮真正構建的哈夫曼樹其實就是原始的哈夫曼樹,這棵樹的作用只有一個,就是計算各個葉子節點的深度,也就是計算出現在壓縮結果中的那些區間碼的碼字長度(三十個區間,有可能僅有幾個區間會參與哈夫曼編碼,因為沒有distance落入其他區間)。

得到了碼字長度,就可以利用正規化哈夫曼編碼的性質來計算各個葉子節點的編碼。正規化哈夫曼編碼的性質是固定的,樹的形狀也就基本固定了,保證樹的右邊的深度始終不小於左邊即可,所以,利用碼字長度計算碼字的過程根本用不著再把正規化哈夫曼樹構建一遍,直接根據性質就可以把碼字計算出來。比如對區間碼2、4、5、6、7、8編碼,這幾個區間碼的碼字長度分別是1、3、3、3、4、4,碼字長度為1的只有一個,碼字長度為3的有三個,碼字長度為4的有兩個。公式“code = (code + bl_count[bits-1])<< 1”用來計算每層最左邊那個節點的碼字,code初始化為0,所以區間碼2的碼字是二進位制“0”,區間碼4的碼字是二進位制“100”,區間碼7的碼字是二進位制“1110”;碼字長度為3的碼字有三個,第一個是“100”,第二個就是區間碼5的碼字,為“100+1 = 101”,第三個就是區間碼6的碼字,為“101+1 = 110”;碼字長度為4的有兩個,第一個是“1110”,第二個就是區間碼8的碼字,為“1110+1 = 1111”(這個例子不用深究,到原始碼分析時自然明白)。從這個過程中我們還可以看到一個細節,就是同一深度下,或者說同一碼字長度下,最左邊的葉子節點對應的區間碼越小,越往右,葉子節點對應的區間碼越大。

前面說了一大堆,其實構建哈夫曼樹以及記錄構建樹資訊的方法非常簡單,只要保證這幾個要點即可:

i.     壓縮只針對區間碼進行哈夫曼編碼;

ii.    實際的distance碼字由“區間碼碼字+擴充套件值”合成;

iii.   壓縮結果只記錄區間碼的碼字長度,各碼字長度的排列序號就是該碼字長度對應的區間碼;

iv.   原始哈夫曼樹各節點深度與正規化哈夫曼樹各節點深度相同;

v.    原始哈夫曼樹只用來計算碼字長度;

vi.   得到了碼字長度就可以直接利用正規化哈夫曼編碼的性質計算碼字,此時不用再構建樹。

現在,我們為字串“As mentioned above,there a(3,4)many kinds of wireless system(3,20)(4,42)than cellular.”中的distance編碼。Distance值為4、20、42,對應區間碼分別為3、8、10,每個區間碼只出現了一次,構造哈夫曼樹為

為了簡便起見,這棵哈夫曼樹是按照正規化哈夫曼性質構建的,也就是實際的編碼就會按照這棵樹來進行。有沒有覺得不太對勁?正規化哈夫曼也好,原始哈夫曼也罷,葉子節點的深度是相同的,但是這裡明顯不對勁啊,按照我們前面的分析,區間碼3應該和區間碼8在同一層,為什麼現在居然和區間碼10在同一層呢?!而且為什麼偏偏3和10的碼字長度是二?這其實和原始碼中的build_tree過程以及pqremove巨集有關,我們後續的原始碼分析章節會看到,這裡只是給大家提前鋪墊一下。

從這棵樹中我們可以得到這三個區間碼的哈夫曼編碼(二進位制):

8     < ------------ >    0

3     < ------------ >    10

10   < ------------ >    11

現在根據distance區間碼錶對以上區間碼內部的distance值進行擴充套件以及合成,

區間碼extra bits     distance

8                3            17-24

3                0            4

10              4            33-48

合成的過程非常簡單,先將區間碼碼字輸出,緊接著輸出擴充套件位編碼即可。這裡要注意一個問題,在接收壓縮結果的這塊記憶體上,以一個位元組為例,區間碼碼字在該位元組的低位,而擴充套件位編碼在該位元組的高位。如下圖所示,

當然,實際情況中,一個distance碼字有可能佔用不了一個位元組,也有可能一個位元組都不夠用,只要知道實際記憶體中,區間碼碼字在低位而擴充套件位編碼在高位即可。

記憶體中儲存的區間碼碼字與從樹上算出的結果是不同的,是其按位逆序的結果。比如,上面區間碼3的碼字是二進位制“10”,但是放到記憶體作為壓縮輸出結果時,就是“01”,即“10”按位逆序的結果。這個過程在原始碼分析時會看到實際的程式碼操作。

記憶體中儲存擴充套件位編碼的方式就是我們平時閱讀的方式,這一點與區間碼不同。比如,某擴充套件碼是“100”,那麼其作為壓縮輸出結果在記憶體中的儲存方式仍然是“100”。

對區間碼8進行擴充套件以及合成,distance值20

區間碼碼字          extra bits     distance       記憶體中的code

     0                       000               17                   000 0

     0                       001               18                   001 0

     0                       010               19                   010 0

     0                       011               20                   011 0

     0                       100               21                   100 0

     0                       101               22                   101 0

     0                       110               23                   110 0

     0                       111               24                   111 0

對區間碼3進行擴充套件以及合成,distance值:4,

區間碼碼字          extra bits     distance        記憶體中的code

     10                      無                4                       01

對區間碼10進行擴充套件以及合成,distance值42

區間碼碼字          extra bits     distance        記憶體中的code

     11                     0000              33                  0000 11

     11                     0001              34                  0001 11

     11                     0010              35                  0010 11

     11                     0011              36                  0011 11

     11                     0100              37                  0100 11

     11                     0101              38                  0101 11

     11                     0110              39                  0110 11

     11                     0111              40                  0111 11

     11                     1000              41                  1000 11

     11                     1001              42                  1001 11

     11                     1010              43                  1010 11

     11                     1011              44                  1011 11

     11                     1100              45                  1100 11

     11                     1101              46                  1101 11

     11                     1110              47                  1110 11

     11                     1111              48                  111111

那麼到現在為止,LZ77結果中所有distance的碼字就都得到了!如下表所示,

Distance 碼錶

Distance 值

Distance 碼字(作為壓縮結果)

4

01

20

011 0

42

1001 11

用碼字替換實際的distance值,LZ77之後的那個字串現在就是,

“As mentioned above,there a(3,01)many kinds ofwireless system(3,0110)(4,100111) than cellular.”。

別忘了,我們還要記錄構建哈夫曼樹的資訊,三十個區間碼,只有3、8、10的碼字長度不為零,那麼整個數列就是,

“0、0、0、2、0、0、0、0、1、0、2、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0”。這就是distance碼字長度列。