基於GZIP壓縮算法的模擬實現

  1. ZIP壓縮的歷史
    1977年,兩位以色列人Jacob Ziv和Abraham Lempel,發表了一篇論文《A Universal Algorithm forSequential Data Compression》,一種通用的數據壓縮算法,所謂通用壓縮算法指的是這種壓縮算法沒有對數據的類型有什麼限定,該算法奠基了今天大多數無損數據壓縮的核心,爲了紀念兩位科學家,該算法被稱爲LZ77,過了一年他們又提了一個類似的算法,稱爲LZ78。ZIP這個算法就是基於LZ77的思想演變過來的,但ZIP對LZ77編碼之後的結果又繼續進行壓縮,直到難以壓縮爲止。在LZ77、LZ78基礎上變種的算法很多,基本都以LZ開頭,如LZW、LZO、LZMA、LZSS、LZR、LZB、LZH、LZC、LZT、LZMW、LZJ、LZFG等等。

2.GZIP壓縮算法的原理
GZIP壓縮算法經歷了兩個階段,第一個階段使用改進的LZ77壓縮算法對上下文中的重複語句進行壓縮,第二階段,採用huffman編碼思想對第一階段壓縮完成的數據進行字節上的壓縮,從而實現對數據的高效壓縮存儲。

3.LZ77壓縮算法:LZ77是一種基於字典的算法,它將長字符串(也稱爲短語)編碼成短小的標記,用小標記代替字典中的短語,從而達到壓縮的的。
通俗來講就是將文件中重複的字符串換成<距離,長度>對,從而達到壓縮的目的。距離2個字節,長度1個字節,但所能表示的最大匹配爲255 + 3 = 258.
因爲我們是從最小3個字節開始匹配的。
例:aaaaabbbbfgnkmpbfgno
壓縮後:aaaaabbbbfgnkmp74o。 4,7表示在距離當前位置7個字符前存在4個重複字符。

①1或2個字符重複,不用替換。
②當重複長度大於等於3時,替換。

3.1如此一來就引入幾個問題?匹配字符串時採用暴力破解還是其他方法。不用想就知道暴力破解不可取,於是我們採用了hash表,將匹配的最小條件(3個字符算出的hash值和3個字符的首字符的下標)插入到hash表,此時又引入一個問題?三個字符總共可以組成種取值(即16M = 256 * 256 * 256),桶的個數需要個,而索引大小佔2個字節,總共桶佔32M字節,是一個非常大的開銷。隨着窗口的移動,表中的數據會不斷過時,維護這麼大的表,會降低程序運行的效率。

3.2 於是我們取hash桶的個數wsize = 2^15個,此時又引入查找緩衝區和先行緩衝區的概念。哈希表由一整塊連續的內存構成,分爲兩個部分。

在這裏插入圖片描述
hash表
在這裏插入圖片描述
prev指向該字典整個內存的起始位置,head = prev + WSIZE,內存是連續的,所以prev和head可以看作兩個數組,即prev[]和head[]。
head數組用來保存三個字符串首字符的索引位置,head的索引爲三個字符通過哈希函數計算的哈希值。而prev就是來解決衝突的。
在這裏插入圖片描述
在這裏插入圖片描述
說明:當pos超過WSIZE時,在插入函數中如果直接使用pos肯定會越界,因此需要與上WMASK,即_prev[pos & WMASK] = _head[hashAddr],但是該語句可能會破壞匹配鏈,讓匹配鏈構成環而造成死循環,該情況如何處理?
設置一個最長匹配次數,比如:255,匹配了255次也沒有匹配到,放棄本次匹配。

3.3 滑動窗口
MIN_LOOKAHEAD = 258爲最小先行匹配單元,如果小於它,那麼可能出現匹配不到258個字符的情況。
隨着滑動窗口的不斷移動,右側窗口中的數據不足MIN_LOOKAHEAD時怎麼辦?在壓縮時,如果文件沒有讀到結尾,爲了保證最大匹配,必須保持look_ahead中至少有MIN_LOOKAHEAD的源數據。那麼此時就會搬移數據,將右窗的數據搬移到左窗,然後更新hash表,hash的值小於wsize則置爲0,大於wsize則減去wsize。

3.4 解壓縮
我們是如何將<距離,長度>對和原字符解析出來,最簡單的方法是再創建一個標記文件,0表示原字符,1表示遇到<長度,距離>對。
注意:在解壓縮時,每更新一個字符都需要手動更新緩衝區,不然會出錯。
例:a b c d a b c d a e f 壓縮後–> a b c d 4 5 e f
如果不及時更新緩衝區,解壓到abcd後, 第二個abcd在緩衝區而不在文件,那麼第五個字符a解碼不出來。

4.huffman壓縮
通過前面LZ77變形思想對源數據進行語句的重複壓縮之後,語句層面的重複性已經解決,但並不代表壓縮效果已經達到最佳,字節層面可能也有大量重複的。
如:"BCDCDDBDDCA"一個字節佔8個比特位,那如果能對所有字節找到小於8個比特位的編碼,然後用找到的編碼對源文件中對應字節重新進行改寫,也可以讓源文件更小。
若此時我們找到的編碼 A:111 B:110 C:10 D:0
此時編碼 110 10 0 10 0 0 110 0 0 10 111,很顯然文件變小了。

4.1 如何構建huffman樹
從二叉樹的根結點到二叉樹中所有葉結點的路徑長度與相應權值的乘積之和爲該二叉樹的帶權路徑長度WPL。
在這裏插入圖片描述
上述四棵樹的帶權路徑長度分別爲:
WPLa = 1 * 2 + 3 * 2 + 5 * 2 + 7 * 2 = 32
WPLb = 1 * 2 + 3 * 3 + 5 * 3 + 7 * 1 = 33
WPLc = 7 * 3 + 5 * 3 + 3 * 2 + 1 * 1 = 43
WPLd = 1 * 3 + 3 * 3 + 5 * 2 + 7 * 1 = 29
把帶權路徑最小的二叉樹稱爲Huffman樹
構造huffman樹及獲取編碼的詳細步驟請參考鏈接:構造huffman樹

4.2 利用huffman編碼對源文件進行壓縮

  1. 統計源文件中每個字符出現的次數
  2. 以字符出現的次數爲權值創建huffman樹
  3. 通過huffman樹獲取每個字符對應的huffman編碼
  4. 讀取源文件,對源文件中的每個字符使用獲取的huffman編碼進行改寫,將改寫結果寫到壓縮文件中,直到文件結束。

4.3 壓縮文件格式
壓縮文件中只保存壓縮之後的數據可以嗎?
答案是不行的,因爲在解壓縮時,沒有辦法進行解壓縮。比如:10111011 00101001 11000111 01011,只有壓縮數據是沒辦法進行解壓縮的,因此壓縮文件中除了要保存壓縮數據,還必須保存解壓縮需要用到的信息:
1.源文件的後綴
2.字符次數對的總行數
5. 字符以及字符出現次數(爲簡單期間,每個字符放置一行)
6. 壓縮數據

4.4 解壓縮
1.從壓縮文件中獲取源文件的後綴
2. 從壓縮文件中獲取字符次數的總行數
3. 獲取每個字符出現的次數
4. 重建huffman樹
5. 解壓縮
4.5 實現過程中遇到的問題
①需要用unsigned char,表示0–255
②解壓縮時遇到換行符時需要多讀取一行。
③打開文件方式需使用二進制,注意windows下換行是/r/n,以及獲取文本文件和二進制文件的結尾都是不一樣的。

最後,壓縮比率分析。
筆者初學,又沒有接觸過測試,所以也是採用了笨辦法,一個文件一個文件測試。測試用例和樣本複雜度都不高,僅供參考。
①對於一般的文本文件(txt),LZ77壓縮比率大概爲60%–80%,而再次huffman壓縮後,源文件較小(2-3k以下),會大於源文件,源文件(5k以上),huffman壓縮後略小於源文件。而官方GZIP壓縮率大在60%以下。
②對於圖片文件(png,jpg),在文件小於200k時,壓縮效果大概在80%–100%,當大於500k時,壓縮後的文件比源文件大
③對於MP3文件10M以下時,大概在85%–100%,大於10M壓縮會變大,官方GZIP壓縮率小於75%。

筆者在測試的時候發現一個現象,那就是我們自己實現的壓縮算法有以下特點:
①LZ77對小文件壓縮率更佳。原因:我們在壓縮時採用了標記位來區分原字符和壓縮字符。若一個文件8M,最壞情況下,壓縮後9M = 8M + 1M(標記位)。
②huffman對大文件壓縮率更佳。原因:我們在壓縮文件中寫入了字符及其出現次數(比如 B:200代表B出現了200次),顯然,字符越多越划算。

存在一個bug:源文件字符種類只有一種的時候壓縮失敗。原因:huffman樹只有根節點的情況獲取不到編碼。

當然,GZIP不是這樣的,基本思想一致,但在其做了許多優化。參考博客

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