gzip壓縮演算法: gzip 所使用壓縮演算法的基本原理1 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編碼。
我們可以看到,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編碼替換成它對應的符號,並輸出。