摘自:http://blog.csdn.net/ghevinn/article/details/45747465
gzip 所使用壓縮算法的基本原理
gzip 對於要壓縮的文件,首先使用LZ77算法的一個變種進行壓縮,對得到的結果再使用Huffman編碼的方法(實際上gzip根據情況,選擇使用靜態Huffman編碼或者動態Huffman編碼,詳細內容在實現中說明)進行壓縮。所以明白了LZ77算法和Huffman編碼的壓縮原理,也就明白了gzip的壓縮原理。我們來對LZ77算法和Huffman編碼做一個簡單介紹。
1.1 LZ77算法簡介
這一算法是由Jacob Ziv 和 Abraham Lempel 於 1977 年提出,所以命名為 LZ77。
1.1.1 LZ77算法的壓縮原理
如果文件中有兩塊內容相同的話,那么只要知道前一塊的位置和大小,我們就可以確定后一塊的內容。所以我們可以用(兩者之間的距離,相同內容的長度)這樣一對信息,來替換后一塊內容。由於(兩者之間的距離,相同內容的長度)這一對信息的大小,小於被替換內容的大小,所以文件得到了壓縮。
下面我們來舉一個例子。
有一個文件的內容如下
http://jiurl.yeah.net http://jiurl.nease.net
其中有些部分的內容,前面已經出現過了,下面用()括起來的部分就是相同的部分。
http://jiurl.yeah.net (http://jiurl.)nease(.net)
我們使用 (兩者之間的距離,相同內容的長度) 這樣一對信息,來替換后一塊內容。
http://jiurl.yeah.net (22,13)nease(23,4)
(22,13)中,22為相同內容塊與當前位置之間的距離,13為相同內容的長度。
(23,4)中,23為相同內容塊與當前位置之間的距離,4為相同內容的長度。
由於(兩者之間的距離,相同內容的長度)這一對信息的大小,小於被替換內容的大小,所以文件得到了壓縮。
1.1.2 LZ77使用滑動窗口尋找匹配串
LZ77算法使用"滑動窗口"的方法,來尋找文件中的相同部分,也就是匹配串。我們先對這里的串做一個說明,它是指一個任意字節的序列,而不僅僅是可以在文本文件中顯示出來的那些字節的序列。這里的串強調的是它在文件中的位置,它的長度隨着匹配的情況而變化。
LZ77從文件的開始處開始,一個字節一個字節的向后進行處理。一個固定大小的窗口(在當前處理字節之前,並且緊挨着當前處理字節),隨着處理的字節不斷的向后滑動,就象在陽光下,飛機的影子滑過大地一樣。對於文件中的每個字節,用當前處理字節開始的串,和窗口中的每個串進行匹配,尋找最長的匹配串。窗口中的每個串指,窗口中每個字節開始的串。如果當前處理字節開始的串在窗口中有匹配串,就用(之間的距離,匹配長度) 這樣一對信息,來替換當前串,然后從剛才處理完的串之后的下一個字節,繼續處理。如果當前處理字節開始的串在窗口中沒有匹配串,就不做改動的輸出當前處理字節。
處理文件中第一個字節的時候,窗口在當前處理字節之前,也就是還沒有滑到文件上,這時窗口中沒有任何內容,被處理的字節就會不做改動的輸出。隨着處理的不斷向后,窗口越來越多的滑入文件,最后整個窗口滑入文件,然后整個窗口在文件上向后滑動,直到整個文件結束。
1.1.3 使用LZ77算法進行壓縮和解壓縮
為了在解壓縮時,可以區分“沒有匹配的字節”和“(之間的距離,匹配長度)對”,我們還需要在每個“沒有匹配的字節”或者“(之間的距離,匹配長度)對”之前,放上一位,來指明是“沒有匹配的字節”,還是“(之間的距離,匹配長度)對”。我們用0表示“沒有匹配的字節”,用1表示“(之間的距離,匹配長度)對”。
實際中,我們將固定(之間的距離,匹配長度)對中的,“之間的距離”和“匹配長度”所使用的位數。由於我們要固定“之間的距離”所使用的位數,所以我們才使用了固定大小的窗口,比如窗口的大小為32KB,那么用15位(2^15=32K)就可以保存0-32K范圍的任何一個值。實際中,我們還將限定最大的匹配長度,這樣一來,“匹配長度”所使用的位數也就固定了。
實際中,我們還將設定一個最小匹配長度,只有當兩個串的匹配長度大於最小匹配長度時,我們才認為是一個匹配。我們舉一個例子來說明這樣做的原因。比如,“距離”使用15位,“長度”使用8位,那么“(之間的距離,匹配長度)對”將使用23位,也就是差1位3個字節。如果匹配長度小於3個字節的話,那么用“(之間的距離,匹配長度)對”進行替換的話,不但沒有壓縮,反而會增大,所以需要一個最小匹配長度。
壓縮:
從文件的開始到文件結束,一個字節一個字節的向后進行處理。用當前處理字節開始的串,和滑動窗口中的每個串進行匹配,尋找最長的匹配串。如果當前處理字節開始的串在窗口中有匹配串,就先輸出一個標志位,表明下面是一個(之間的距離,匹配長度) 對,然后輸出(之間的距離,匹配長度) 對,然后從剛才處理完的串之后的下一個字節,繼續處理。如果當前處理字節開始的串在窗口中沒有匹配串,就先輸出一個標志位,表明下面是一個沒有改動的字節,然后不做改動的輸出當前處理字節,然后繼續處理當前處理字節的下一個字節。
解壓縮:
從文件開始到文件結束,每次先讀一位標志位,通過這個標志位來判斷下面是一個(之間的距離,匹配長度) 對,還是一個沒有改動的字節。如果是一個(之間的距離,匹配長度)對,就讀出固定位數的(之間的距離,匹配長度)對,然后根據對中的信息,將匹配串輸出到當前位置。如果是一個沒有改動的字節,就讀出一個字節,然后輸出這個字節。
我們可以看到,LZ77壓縮時需要做大量的匹配工作,而解壓縮時需要做的工作很少,也就是說解壓縮相對於壓縮將快的多。這對於需要進行一次壓縮,多次解壓縮的情況,是一個巨大的優點。
1.2 Huffman編碼簡介
1.2.1 Huffman編碼的壓縮原理
我們把文件中一定位長的值看作是符號,比如把8位長的256種值,也就是字節的256種值看作是符號。我們根據這些符號在文件中出現的頻率,對這些符號重新編碼。對於出現次數非常多的,我們用較少的位來表示,對於出現次數非常少的,我們用較多的位來表示。這樣一來,文件的一些部分位數變少了,一些部分位數變多了,由於變小的部分比變大的部分多,所以整個文件的大小還是會減小,所以文件得到了壓縮。
1.2.2 Huffman編碼使用Huffman樹來產生編碼
要進行Huffman編碼,首先要把整個文件讀一遍,在讀的過程中,統計每個符號(我們把字節的256種值看作是256種符號)的出現次數。然后根據符號的出現次數,建立Huffman樹,通過Huffman樹得到每個符號的新的編碼。對於文件中出現次數較多的符號,它的Huffman編碼的位數比較少。對於文件中出現次數較少的符號,它的Huffman編碼的位數比較多。然后把文件中的每個字節替換成他們新的編碼。
建立Huffman樹:
把所有符號看成是一個結點,並且該結點的值為它的出現次數。進一步把這些結點看成是只有一個結點的樹。
每次從所有樹中找出值最小的兩個樹,為這兩個樹建立一個父結點,然后這兩個樹和它們的父結點組成一個新的樹,這個新的樹的值為它的兩個子樹的值的和。如此往復,直到最后所有的樹變成了一棵樹。我們就得到了一棵Huffman樹。
通過Huffman樹得到Huffman編碼:
這棵Huffman樹,是一棵二叉樹,它的所有葉子結點就是所有的符號,它的中間結點是在產生Huffman樹的過程中不斷建立的。我們在Huffman樹的所有父結點到它的左子結點的路徑上標上0,右子結點的路徑上標上1。
現在我們從根節點開始,到所有葉子結點的路徑,就是一個0和1的序列。我們用根結點到一個葉子結點路徑上的0和1的序列,作為這個葉子結點的Huffman編碼。
我們來看一個例子。
有一個文件的內容如下
abbbbccccddde
我們統計一下各個符號的出現次數,
a b c d e
1 4 4 3 1
建立Huffman樹的過程如圖:
通過最終的Huffman樹,我們可以得到每個符號的Huffman編碼。
a 為 110
b 為 00
c 為 01
d 為 10
e 為 111
我們可以看到,Huffman樹的建立方法就保證了,出現次數多的符號,得到的Huffman編碼位數少,出現次數少的符號,得到的Huffman編碼位數多。
各個符號的Huffman編碼的長度不一,也就是變長編碼。對於變長編碼,可能會遇到一個問題,就是重新編碼的文件中可能會無法如區分這些編碼。
比如,a的編碼為000,b的編碼為0001,c的編碼為1,那么當遇到0001時,就不知道0001代表ac,還是代表b。出現這種問題的原因是a的編碼是b的編碼的前綴。
由於Huffman編碼為根結點到葉子結點路徑上的0和1的序列,而一個葉子結點的路徑不可能是另一個葉子結點路徑的前綴,所以一個Huffman編碼不可能為另一個Huffman編碼的前綴,這就保證了Huffman編碼是可以區分的。
1.2.3 使用Huffman編碼進行壓縮和解壓縮
為了在解壓縮的時候,得到壓縮時所使用的Huffman樹,我們需要在壓縮文件中,保存樹的信息,也就是保存每個符號的出現次數的信息。
壓縮:
讀文件,統計每個符號的出現次數。根據每個符號的出現次數,建立Huffman樹,得到每個符號的Huffman編碼。將每個符號的出現次數的信息保存在壓縮文件中,將文件中的每個符號替換成它的Huffman編碼,並輸出。
解壓縮:
得到保存在壓縮文件中的,每個符號的出現次數的信息。根據每個符號的出現次數,建立Huffman樹,得到每個符號的Huffman編碼。將壓縮文件中的每個Huffman編碼替換成它對應的符號,並輸出。