ZIP壓縮算法詳細分析及解壓實例解釋

最近本身實現了一個ZIP壓縮數據的解壓程序,以爲有必要把ZIP壓縮格式進行一下詳細總結,數據壓縮是一門通訊原理和計算機科學都會涉及到的學科,在通訊原理中,通常稱爲信源編碼,在計算機科學裏,通常稱爲數據壓縮,二者本質上沒啥區別,在數學家看來,都是映射。一方面在進行通訊的時候,有必要將待傳輸的數據進行壓縮,以減小帶寬需求;另外一方面,計算機存儲數據的時候,爲了減小磁盤容量需求,也會將文件進行壓縮,儘管如今的網絡帶寬愈來愈高,壓縮已經不像90年代初那個時候那麼迫切,但在不少場合下仍然須要,其中一個緣由是壓縮後的數據容量減少後,磁盤訪問IO的時間也縮短,儘管壓縮和解壓縮過程會消耗CPU資源,可是CPU計算資源增加得很快,可是磁盤IO資源卻變化得很慢,好比目前主流的SATA硬盤仍然是7200轉,若是把磁盤的IO壓力轉化到CPU上,整體上可以提高系統運行速度。壓縮做爲一種很是典型的技術,會應用到不少不少場合下,好比文件系統、數據庫、消息傳輸、網頁傳輸等等各種場合。儘管壓縮裏面會涉及到不少術語和技術,但無需擔憂,博主儘可能將其描述得通俗易懂。另外,本文涉及的壓縮算法很是主流而且十分精巧,理解了ZIP的壓縮過程,對理解其它相關的壓縮算法應該就比較容易了。程序員

 

一、引子算法

壓縮能夠分爲無損壓縮和有損壓縮,有損,指的是壓縮以後就沒法完整還原原始信息,可是壓縮率能夠很高,主要應用於視頻、話音等數據的壓縮,由於損失了一點信息,人是很難察覺的,或者說,也不必那麼清晰照樣能夠看能夠聽;無損壓縮則用於文件等等必須完整還原信息的場合,ZIP天然就是一種無損壓縮,在通訊原理中介紹數據壓縮的時候,每每是從信息論的角度出發,引出香農所定義的熵的概念,這方面的介紹實在太多,這裏換一種思路,從最原始的思想出發,爲了達到壓縮的目的,須要怎麼去設計算法。而ZIP爲咱們提供了至關好的案例。數據庫

儘管咱們不去探討信息論裏面那些複雜的概念,不過咱們首先仍是要從兩位信息論大牛談起。由於是他們奠定了今天大多數無損數據壓縮的核心,包括ZIP、RAR、GZIP、GIF、PNG等等大部分無損壓縮格式。這兩位大牛的名字分別是Jacob Ziv和Abraham Lempel,是兩位以色列人,在1977年的時候發表了一篇論文《A Universal Algorithm for Sequential Data Compression》,從名字能夠看出,這是一種通用壓縮算法,所謂通用壓縮算法,指的是這種壓縮算法沒有對數據的類型有什麼限定。不過論文我以爲不用仔細看了,由於博主做爲一名通訊專業的PHD,看起來也焦頭爛額,不過咱們後面能夠看到,它的思想仍是很簡單的,之因此看起來複雜,主要是由於IEEE的某些雜誌就是這個特色,須要從數學上去證實,這種壓縮算法到底有多優,好比針對一個各態歷經的隨機序列(不用追究什麼叫各態歷經隨機序列),通過這樣的壓縮算法後,是否能夠接近信息論裏面的極限(也就是前面說的熵的概念)等等,不過在理解其思想以前,我的認爲不必深究這些東西,除非你要發論文。這兩位大牛提出的這個算法稱爲LZ77,兩位大牛過了一年又提了一個相似的算法,稱爲LZ78,思想相似,ZIP這個算法就是基於LZ77的思想演變過來的,但ZIP對LZ77編碼以後的結果又繼續進行壓縮,直到難以壓縮爲止。除了LZ7七、LZ78,還有不少變種的算法,基本都以LZ開頭,如LZW、LZO、LZMA、LZSS、LZR、LZB、LZH、LZC、LZT、LZMW、LZJ、LZFG等等,很是多,LZW也比較流行,GIF那個動畫格式記得用了LZW。我也寫過解碼程序,之後有時間能夠再寫一篇,但感受跟LZ77這些相似,寫的必要性不大。編程

ZIP的做者是一個叫Phil Katz的人,這我的算是開源界的一個具備悲劇色彩的傳奇人物。雖然二三十年前,開源這個詞尚未如今這樣風起雲涌,可是總有一些具備黑客精神的牛人,心裏裏面充滿了自由,不管他處於哪一個時代。Phil Katz這我的是個牛逼程序員,成名於DOS時代,我我的也沒有經歷過那個時代,我是從Windows98開始接觸電腦的,只是從書籍中得知,那個時代網速很慢,撥號使用的是隻有幾十Kb(比特不是字節)的貓,56Kb其實是這種貓的最高速度,在ADSL出現以後,這種技術被迅速淘汰。當時記錄文件的也是硬盤,可是在電腦之間拷貝文件的是軟盤,這個東西我大一還用過,最高容量記得是1.44MB,這仍是200X年的軟盤,之前的軟盤容量具體多大就不知道了,Phil Katz上網的時候還不到1990年,WWW實際上就沒出現,瀏覽器固然是沒有的,當時上網幹嗎呢?基本就是相似於網管敲各類命令,這樣實際上也能夠聊天、上論壇不是嗎,傳個文件不壓縮的話確定死慢死慢的,因此壓縮在那個時代很重要。當時有個商業公司提供了一種稱爲ARC的壓縮軟件,可讓你在那個時代聊天更快,固然是要付費的,Phil Katz就感受到不爽,因而寫了一個PKARC,免費的,看名字知道是兼容ARC的,因而網友都用PKARC了,ARC那個公司天然就不爽,把哥們告上了法庭,說牽涉了知識產權等等,結果Phil Katz坐牢了。。。牛人就是牛人, 在牢裏面左思右想,決定整一個超越ARC的牛逼算法出來,牢裏面就是適合思考,用了兩週就整出來的,稱爲PKZIP,不只免費,並且此次還開源了,直接公佈源代碼,由於算法都不同了,也就不涉及到知識產權了,因而ZIP流行開來,不過Phil Katz這我的沒有從裏面賺到一分錢,仍是窮困潦倒,由於喝酒過多等衆多緣由,2000年的時候死在一個汽車旅館裏。英雄逝去,精神永存,如今咱們用UE打開ZIP文件,咱們能看到開頭的兩個字節就是PK兩個字符的ASCII碼。瀏覽器

 

二、一個案例的入門思考網絡

好了,Phil Katz在牢裏面到底思考了什麼?用什麼樣的算法來壓縮數據呢?咱們想一個簡單的例子:數據結構

生,容易。活,容易。生活,不容易。less

上面這句話假如不壓縮,若是使用Unicode編碼,每一個字會用2個字節表示。爲何是兩個字節呢?Unicode是一種國際標準,把常見各國的字符,好比英文字符、日文字符、韓文字符、中文字符、拉丁字符等等所有制定了一個標準,顯然,用2個字節能夠最多表示2^16=65536個字符,那麼65536就夠了嗎?生僻字實際上是不少的,好比光康熙字典裏面收錄的漢字就好幾萬,因此其實是不夠的,那麼是否是擴到4個字節?也能夠,這樣空間卻是變大了,能夠收錄更多字符,但一方面擴到4個字節就必定保證夠嗎?另外一方面,4個字節是否是太浪費空間了,就爲了那些通常狀況都不會出現的生僻字?因此,通常狀況下,使用2個字節表示,當出現生僻字的時候,再使用4個字節表示。這實際上就體現了信息論中數據壓縮基本思想,出現頻繁的那些字符,表示得短一些;出現稀少的,能夠表示得長些(反正通常狀況下也不會出現),這樣總體長度就會減少。除了Unicode,ASCII編碼是針對英文字符的編碼方案,用1個字節便可,除了這兩種編碼方案,還有不少地區性的編碼方案,好比GB2312能夠對中文簡體字進行編碼,Big5能夠對中文繁體字進行編碼。兩個文件若是都使用一種編碼方案,那是沒有問題的,不過考慮到國際化,仍是儘可能使用Unicode這種國際標準吧。不過這個跟ZIP沒啥關係,純屬背景介紹。優化

好了,回到咱們前面說的例子,一共有17個字符(包括標點符號),若是用普通Unicode表示,一共是17*2=34字節。可不能夠壓縮呢?全部人一眼均可以看出裏面出現了不少重複的字符,好比裏面出現了好屢次容易(其實是容易加句號三個字符)這個詞,第一次出現的時候用普通的Unicode,第二次出現的「容易。」則能夠用(距離、長度)表示,距離的意思是接下來的字符離前面重複的字符隔了幾個,長度則表示有幾個重複字符,上面的例子的第二個「容易。」就表示爲(5,3),就是距離爲5個字符,長度是3,在解壓縮的時候,解到這個地方的時候,往前跳5個字符,把這個位置的連續3個字符拷貝過來就完成了解壓縮,這實際上不就是指針的概念?沒有錯,跟指針很相似,不過在數據壓縮領域,通常稱爲字典編碼,爲何叫字典呢,當咱們去查一個字的時候,咱們老是先去目錄查找這個字在哪一頁,再翻到那一頁去看,指針不也是這樣,指針不就是內存的地址,要對一個內存進行操做,咱們先拿到指針,而後去那塊內存去操做。所謂的指針、字典、索引、目錄等等術語,不一樣的背景可能稱呼不一樣,但咱們要理解他們的本質。若是使用(5,3)這種表示方法,原來須要用6個字節表示,如今只須要記錄5和3便可。那麼,5和3怎麼記錄呢?一種方法天然仍是能夠用Unicode,那麼就至關於節省了2個字節,可是有兩個問題,第一個問題是解壓縮的時候怎麼知道是正常的5和3這兩個字符,仍是這只是一個特殊標記呢?因此前面還得加一個標誌來區分一下,到底接下來的Unicode碼是指普通字符,仍是指距離和長度,若是是普通Unicode,則直接查Unicode碼錶,若是是距離和長度,則往前面移動一段距離,拷貝便可。第二個問題,仍是壓縮程度不行,這麼一弄,感受壓縮不了多少,若是重複字符比較長那卻是比較划算,由於反正「距離+長度」就夠了,但好比這個例子,若是5和3前面加一個特殊字節,豈不是又是3個字節,那還不如不壓縮。咋辦呢?能不能對(5,3)這種整數進行再次壓縮?這裏就利用了咱們前面說的一個基本原則:出現的少的整數多編一些比特,出現的多的整數少編一些比特。那麼,好比三、四、五、六、七、八、9這些距離誰出現得多?誰出現的少呢?誰知道?動畫

壓縮以前固然不知道,不過掃描一遍不就知道了?好比,後面那個重複的字符串「容易。」按照前面的規則能夠表示爲(7,3),即離前面重複的字符串距離爲7,長度爲3。(7,3)指着前面跟本身同樣那個字符串。那麼,爲何不指着第一個「容易。」要指着第二個「容易。」呢?若是指着第一個,那就不是(7,3)了,就是(12,3)了。固然,表示爲(12,3)也能夠解壓縮,可是有一個問題,就是12這個值比7大,大了又怎麼了?咱們在生活中會發現一些廣泛規律,重複現象每每具備局部性。好比,你跟一我的說話,你說了一句話之後,每每很快會重複一遍,可是你不會隔了5個小時又重複這句話,這種特色在文件裏面也存在着,處處都是這種例子,好比你在編程的時候,你定義了一個變量int nCount,這個nCount通常你很快就會用到,不會離得很遠。咱們前面所說的距離表明了你隔了多久再說這句話,這個距離通常不大,既然如此,應該以離當前字符串距離最近的那個做爲記錄的依據(也就是指向離本身最近那個重複字符串),這樣的話,全部的標記都是一些短距離,好比都是三、四、五、六、7而不會是三、五、7八、965等等,若是大多數都是一些短距離,那麼這些短距離就能夠用短一些的比特表示,長一些的距離不太常見,則用一些長一些的比特表示。這樣, 整體的表示長度就會減小。好了,咱們前面獲得了(5,3)、(七、3)這種記錄重複的表示,距離有兩種:五、7;長度只有1種:3。咋編碼?越短越好。

既然表示的比特越短越好,3表示爲0、5表示爲十、7表示爲11,行不行?這樣(5,3),(7,3)就只須要表示爲100、110,這樣豈不是很短?貌似能夠,貌似很高效。

但解壓縮遇到10這兩個比特的時候,怎麼知道10表示5呢?這種表示方法是一個映射表,稱爲碼錶。咱們設計的上面這個例子的碼錶以下:

3-->0

5-->10

7-->11

這個碼錶也得傳過去或者記錄在壓縮文件裏才行啊,不然沒法解壓縮,但會不會記錄了碼錶之後總體空間又變大了,會不會起不到壓縮的做用?並且一個碼錶怎麼記錄?碼錶記錄下來也是一堆數據,是否是也須要編碼?碼錶是否能夠繼續壓縮?那豈不是又須要新的碼錶?壓縮會不會是一個永無止境的過程?做爲一個入門級的同窗,大概想到這兒就不容易想下去了。

 

三、ZIP中的LZ編碼思想

上面咱們說的重複字符串用指針標記記錄下來,這種方法就是LZ這兩我的提出來的,理解起來比較簡單。後面分析(5,3)這種指針標記應該怎麼編碼的時候,就涉及到一種很是普遍的編碼方式,Huffman編碼,Huffman大體和香農是一個時代的人,這種編碼方式是他在MIT讀書的時候提出來的。接下來,咱們來看看ZIP是怎麼作的。

以上面的例子,一個很簡單的示意圖以下:

能夠看出,ZIP中使用的LZ77算法和前面分析的相似,固然,若是仔細對比的話,ZIP中使用的算法和LZ提出來的LZ77算法其實仍是有差別的,不過我建議不用仔細去扣裏面的差別,思想基本是相同的,咱們後面會簡要分析一下二者的差別。LZ77算法通常稱爲「滑動窗口壓縮」,咱們前面說過,該算法的核心是在前面的歷史數據中尋找重複字符串,但若是要壓縮的文件有100MB,是否是從文件頭開始找?不是,這裏就涉及前面提過的一個規律,重複現象是具備局部性的,它的基本假設是,若是一個字符串要重複,那麼也是在附近重複,遠的地方就不用找了,所以設置了一個滑動窗口,ZIP中設置的滑動窗口是32KB,那麼就是往前面32KB的數據中去找,這個32KB隨着編碼不斷進行而往前滑動。固然,理論上講,把滑動窗口設置得很大,那樣就有更大的機率找到重複的字符串,壓縮率不就更高了?初看起來如此,找的範圍越大,重複機率越大,不過仔細想一想,可能會有問題,一方面,找的範圍越大,計算量會增大,不顧一切地增大滑動窗口,甚至不設置滑動窗口,那樣的軟件可能不可用,你想一想,如今這種方式,咱們在壓縮一個大文件的時候,速度都已經很慢了,若是增大滑動窗口,速度就更慢,從工程實現角度來講,設置滑動窗口是必須的;另外一方面,找的範圍越大,距離越遠,出現的距離不少,也不利於對距離進行進一步壓縮吧,咱們前面說過,距離和長度最好出現的值越少越好,那樣更好壓縮,若是出現的不少,如何記錄距離和長度可能也存在問題。不過,我相信滑動窗口設置得越大,最終的結果應該越好一些,不過應該不會起到特別大的做用,好比壓縮率提升了5%,但計算量增長了10倍,這顯然有些得不償失。

在第一個圖中,「容易。」是一個重複字符串,距離distance=5,字符串長度length=3。當對這三個字符壓縮完畢後,接下來滑動窗口向前移動3個字符,要壓縮的是「我...」這個字符串,但這個串在滑動窗口內沒找到,因此沒法使用distance+length的方式記錄。這種結果稱爲literal。literal的中文含義是原義的意思,表示沒有使用distance+length的方式記錄的那些普通字符。literal是否是就用原始的編碼方式,好比Unicode方式表示?ZIP裏不是這麼作的,ZIP把literal認爲也是一個數,儘管不能用distance+length表示,但不表明不能夠繼續壓縮。另外,若是「我」出如今了滑動窗口內,是否是就能夠用distance+length的方式表示?也不是,由於一個字出現重複,不值得用這種方式表示,兩個字呢?distance+length就是兩個整數,看起來也不必定值得,ZIP中確實認爲2個字節若是在滑動窗口內找到重複,也無論,只有3個字節以上的重複字符串,纔會用distance+length表示,重複字符串的長度越長越好,由於無論多長,都用distance+length表示就好了。

這樣的話,一段字符串最終就能夠表示成literal、distance+length這兩種形式了。LZ系列算法的做用到此爲止,下面,Phil Katz考慮使用Huffman對上面的這些LZ壓縮後的結果進行二次壓縮。我的認爲接下來的過程纔是ZIP的核心,因此咱們要熟悉一下Huffman編碼。

 

四、ZIP中的Huffman編碼思想

上面LZ壓縮結果有三類(literal、distance、length),咱們拿出distance一類來舉例。distance表明重複字符串離前一個如出一轍的字符串之間的距離,是一個大於0的整數。如何對一個整數進行編碼呢?一種方法是直接用固定長度表示,好比採用計算機裏面描述一個4字節整數那樣去記錄,這也是能夠的,主要問題固然是浪費存儲空間,在ZIP中,distance這個數表示的是重複字符串之間的距離,顯然,通常而言,越小的距離,出現的數量可能越多,而越大的距離,出現的數量可能越少,那麼,按照咱們前面所說的原則,小的值就用較少比特表示,大的值就用較多比特表示,在咱們這個場景裏,distance固然也不會無限大,好比不會超過滑動窗口的最大長度,假如對一個文件進行LZ壓縮後,獲得的distance值爲:

三、六、四、三、四、三、四、三、5

這個例子裏,3出現了4次,4出現了3次,5出現了1次,6出現了1次。固然,不一樣的文件獲得的結果不一樣,這裏只是舉一個典型的例子,由於只有4種值,因此咱們沒有必要對其它整數編碼。只須要對這4個整數進行編碼便可。

那麼,怎麼設計一個算法,符合3的編碼長度最短?6的編碼長度最長這種直觀上可行的原則(咱們並無說這是理論上最優的方式)呢?

看起來彷佛很難想出來。咱們先來簡化一下,用固定長度表示。這裏有4個整數,只要使用2個比特表示便可。因而這樣表示就很簡單:

00-->3; 01-->4; 10-->5;  11-->6。

00、01這種編碼結果稱爲碼字,碼字的平均長度是2。上面這個對應關係即爲碼錶,在壓縮時,須要將碼錶放在最前面,後面的數字就用碼字表示,解碼時,先把碼錶記錄在內存裏,好比用一個哈希表記錄下來,解壓縮時,遇到00,就解碼爲3等等。

由於出現了9個數,因此所有碼字總長度爲18個比特。(咱們暫時不考慮記錄碼錶到底要佔多少空間)

想要編碼結果更短,由於3出現的最多,因此考慮把3的碼字縮短點,好比3是否是能夠用1個比特表示,這樣纔算縮短吧,由於0和1只是二進制的一個標誌,因此用0仍是1沒有本質區別,那麼,咱們暫定把3用比特0表示。那麼,四、五、6還能用0開頭的碼字表示呢?

這樣會存在問題,由於四、五、6的編碼結果若是以0開頭,那麼,在解壓縮的時候,遇到比特0,就不知道是表示3仍是表示四、五、6了,就沒法解碼,固然,彷佛理論上也不是不能夠,好比能夠日後解解看,好比假定0表示3的條件下日後解,若是無效則說明這個假設不對,但這種方式很容易出現兩個字符串的編碼結果是同樣的,這個誰來保證?因此,四、五、6都得以1開頭才行,那麼,按照這個原則,4用1個比特也不行,由於五、6要麼以0開頭,要麼以1開頭,就沒法編碼了,因此咱們將4的碼字增長至2個比特,好比10,因而咱們獲得了部分碼錶:

0-->3;10-->4。

按照這個道理,五、6既不能以0開頭,也不能以10開頭了,由於一樣存在沒法解碼的問題,因此5應該以11開頭,就定爲11行不行呢?也不行,由於6就不知道怎麼編碼了,6也不能以0開頭,也不能以十、11開頭,那就沒法表示了,因此,無可奈何,咱們必須把5擴展一位,好比110,那麼,6顯然就能夠用111表示了,反正也沒有其餘數了。因而咱們獲得了最終的碼錶:

0-->3;10-->4;110-->5;111-->6。

看起來,編碼結果只能是這樣了,咱們來算一下,碼字的總長度減小了沒有,原來的9個數是三、六、四、三、四、三、四、三、5,分別對應的碼字是:

0、1十一、十、0、十、0、十、0、110

算一下,總共16個比特,果真比前面那種方式變短了。咱們在前面的設計過程當中,是按照這些值出現次數由高到底的順序去找碼字的,好比先肯定3,再肯定四、五、6等等。按照一個碼字不能是另外一個碼字的前綴這一規則,逐步得到全部的碼字。這種編碼規則有一個專用術語,稱爲前綴碼。Huffman編碼基本上就是這麼作的,把出現頻率排個序,而後逐個去找,這個逐個去找的過程,就引入了二叉樹。不過Huffman的算法通常是從頻率由低到高排序,從樹的下面依次往上合併,不過本質上沒區別,理解思想便可。上面的結果能夠用一顆二叉樹表示爲下圖:

這棵樹也稱爲碼樹,其實就是碼錶的一種形式化描述,每一個節點(除了葉子節點)都會分出兩個分支,左分支表明比特0,右邊分支表明1,從根節點到葉子節點的一個比特序列就是碼字。由於只有葉子節點能夠是碼字,因此這樣也符合一個碼字不能是另外一個碼字的前綴這一原則。說到這裏,能夠說一下另外一個話題,就是一個映射表map在內存中怎麼存儲,沒有相關經驗的能夠跳過,map實現的是key-->value這樣的一個表,map的存儲通常有哈希表和樹形存儲兩類,樹形存儲就能夠採用上面這棵樹,樹的中間節點並無什麼含義,葉子節點的值表示value,從根節點到葉子節點上分支的值就是key,這樣比較適合存儲那些key由多個不等長字符組成的場合,好比key若是是字符串,那麼把二叉樹的分支擴展不少,成爲多叉樹,每一個分支就是a,b,c,d這種字符,這棵樹也就是Trie樹,是一種很好使的數據結構。利用樹的遍歷算法,就實現了一個有序Map。

好了,咱們理解了Huffman編碼的思想,咱們來看看distance的實際狀況。ZIP中滑動窗口大小固定爲32KB,也就是說,distance的值範圍是1-32768。那麼,經過上面的方式,統計頻率後,就獲得32768個碼字,按照上面這種方式能夠構建出來。因而咱們會遇到一個最大的問題,那就是這棵樹太大了,怎麼記錄呢?

好了,我的認爲到了ZIP的核心了,那就是碼樹應該怎麼縮小,以及碼樹怎麼記錄的問題。

 

五、ZIP中Huffman碼樹的記錄方式

分析上面的例子,看看這個碼錶:

0-->3;10-->4;110-->5;111-->6。

咱們以前提過,0和1就是二進制的一個標誌,互換一下其實根本不影響編碼長度,因此,下面的碼錶實際上是同樣的:

1-->3;00-->4;010-->5;011-->6。

1-->3;01-->4;000-->5;001-->6。

0-->3;11-->4;100-->5;101-->6。

。。。。。

這些均可以,並且編碼長度徹底同樣,只是碼字不一樣而已。

對比一下第一個和第二個例子,對應的碼樹是這個樣子:

也就是說,咱們把碼樹的任意節點的左右分支旋轉(0、1互換),也能夠稱爲樹的左右互換,其實不影響編碼長度,也就是說,這些碼錶其實都是同樣好的,使用哪一個均可以。

這個規律暗示了什麼信息呢?暗示了碼錶能夠怎麼記錄呢?Phil Katz當年在牢裏也深深地思考了這一問題。

爲了體會Phil Katz當時的心情,咱們有必要盯着這兩棵樹思考幾分鐘:怎麼把一顆樹用最少的比特記錄下來?

Phil Katz當時思考的邏輯我猜是這樣的,既然這些樹的壓縮程度都同樣,那乾脆使用最特殊的那棵樹,反正壓縮程度都同樣,只要ZIP規定了這棵樹的特殊性,那麼我記錄的信息就能夠最少,這種特殊化的思想在後面還會看到。不一樣的樹固然有不一樣的特色,好比數據結構裏面常見的平衡樹也是一類特殊的樹,他選的樹就是左邊那棵,這棵樹有一個特色,越靠左邊越淺,越往右邊越深,是這些樹中最不平衡的樹。ZIP裏的壓縮算法稱爲Deflate算法,這棵樹也稱爲Deflate樹,對應的解壓縮算法稱爲Inflate,Deflate的大體意思是把輪胎放氣了,意爲壓縮;Inflate是給輪胎打氣的意思,意爲解壓。那麼,Deflate樹的特殊性又帶來什麼了?

揭曉答案吧,Phil Katz認爲換來換去只有碼字長度不變,若是規定了一類特殊的樹,那麼就只須要記錄碼字長度便可。好比,一個有效的碼錶是0-->3;10-->4;110-->5;111-->6。但只須要記錄這個對應關係便可:

3  4  5  6

1  2  3  3

也就是說,把一、二、三、3記錄下來,解壓一邊照着左邊那棵樹的形狀構造一顆樹,而後只須要一、二、三、3這個信息天然就知道是0、十、1十、111。這就是Phil Katz想出來的ZIP最核心的一點:這棵碼樹用碼字長度序列記錄下來便可。

固然,只把一、二、三、3這個序列記錄下來還不行,好比不知道111對應5仍是對應6?

因此,構造出樹來只是知道了有哪些碼字了,可是這些碼字到底對應哪些整數仍是不知道。

Phil Katz因而又出現了一個想法:記錄一、二、三、3仍是記錄一、三、二、3,或者三、三、二、1,其實都能構造出這棵樹來,那麼,爲何不按照一個特殊的順序記錄呢?這個順序就是整數的大小順序,好比上面的三、四、五、6是整數大小順序排列的,那麼,記錄的順序就是一、二、三、3。而不是二、三、三、1。

好了,根據一、二、三、3這個信息構造出了碼字,這些碼字對應的整數一個比一個大,假如咱們知道編碼前的整數就是三、四、五、6這四個數,那就能對應起來了,不過究竟是哪四個仍是不知道啊?這個整數能夠表示距離啊,距離不知道怎麼去解碼LZ?

Phil Katz又想了,既然distance的範圍是1-32768,那麼就按照這個順序記錄。上面的例子1和2沒有,那就記錄長度0。因此記錄下來的碼字長度序列爲:

0、0、一、二、三、三、0、0、0、0、0、。。。。。。。。。。。。

這樣就知道構造出來的碼字對應哪一個整數了吧,但由於distance可能的值不少(32768個),但實際出現的每每很少,中間會出現不少0(也就是根本就沒出現這個距離),不過這個問題卻是能夠對連續的0作個特殊標記,這樣是否是就好了呢?還有什麼問題?

咱們仍是要站在時代的高度來看待這個問題。咱們明白,每一個distance確定對應惟一一個碼字,使用Huffman編碼能夠獲得全部碼字,可是由於distance可能很是多,雖然通常不會有32768這麼多,但對一個大些的文件進行LZ編碼,distance上千仍是很正常的,因此這棵樹很大,計算量、消耗的內存都容易超越了那個時代的硬件條件,那麼怎麼辦呢?這裏再次體現了Phil Katz對Huffman編碼掌握的深度,他把distance劃分紅多個區間,每一個區間當作一個整數來看,這個整數稱爲Distance Code。當一個distance落到某個區間,則至關因而出現了那個Code,多個distance對應於一個Distance Code,Distance雖然不少,但Distance Code能夠劃分得不多,只要咱們對Code進行Huffman編碼,獲得Code的編碼後,Distance Code再根據必定規則擴展出來。那麼,劃分多少個區間?怎麼劃分區間呢?咱們分析過,越小的距離,出現的越多;越大的距離,出現的越少,因此這種區間劃分不是等間隔的,而是愈來愈稀疏的,相似於下面的劃分:

一、二、三、4這四個特殊distance不劃分,或者說1個Distance就是1個區間;5,6做爲一個區間;七、8做爲一個區間等等,基本上,區間的大小都是一、二、四、八、1六、32這麼遞增的,越日後,涵蓋的距離越多。爲何這麼分呢?首先天然是距離越小出現頻率越高,因此距離值小的時候,劃分密一些,這樣至關於一個放大鏡,能夠對小的距離進行更精細地編碼,使得其編碼長度與其出現次數儘可能匹配;對於距離較大那些,由於出現頻率低,因此能夠適當放寬一些。另外一個緣由是,只要知道這個區間Code的碼字,那麼對於這個區間裏面的全部distance,後面追加相應的多個比特便可,好比,17-24這個區間的Huffman碼字是110,由於17-24這個區間有8個整數,因而按照下面的規則便可得到其distance對應的碼字:

17-->110 000

18-->110 001

19-->110 010

20-->110 011

21-->110 100

22-->110 101

23-->110 110

24-->110 111

這樣計算複雜度和內存消耗是否是很小了,由於須要進行Huffman編碼的整數一下字變少了,這棵樹不會多大,計算起來時間和空間複雜度下降,擴展起來也比較簡單。固然,從理論上來講,這樣的編碼方式實際上將編碼過程分爲了兩級,並非理論上最優的,把全部distance看成一個大空間去編碼纔可能獲得最優結果,不過仍是那句話,工程實現的限制,在壓縮軟件實現上,咱們不能用壓縮率做爲衡量一個算法優劣的惟一指標,其實耗費的時間和空間一樣是指標,因此須要看綜合指標。不少其餘軟件也同樣,擴展性、時間空間複雜度、穩定性、移植性、維護的方便性等等是工程上很重要的東西。我沒有看過RAR是如何壓縮的,有多是在相似的地方進行了改進,若是如此,那也是站在巨人的肩膀上,並且硬件條件不一樣,進行一些改進也並不奇怪。

具體來講,Phil Katz把distance劃分爲30個區間,以下圖:

這個圖是我從David Salomon的《Data Compression The Complete Reference》這本書(第四版)中拷貝出來的,下面的有些圖也是,若是須要對數據壓縮進行全面的瞭解,這本書幾乎是最全的了,強烈推薦。

固然,你要問爲何是30個區間,我也沒分析過,也許是複雜度和壓縮率通過試驗以後的一種折中吧。

其中,左邊的Code表示區間的編號,是0-29,共30個區間,這只是個編號,沒有特別的含義,但Huffman就是對0-29這30個Code進行編碼的,獲得區間的碼字;

bits表示distance的碼字須要在Code的碼字基礎上擴展幾位,好比0就表示不擴展,最大的13表示要擴展13位,所以,最大的區間包含的distance數量爲8192個。

Distance一列則表示這個區間涵蓋的distance範圍。

理解了碼樹如何有效記錄,以及如何縮小碼樹的過程,我以爲就理解了ZIP的精髓。

 

六、ZIP中literal和length的壓縮方式

說完了distance,LZ編碼結果還有兩類:literal和length。這兩類也利用了相似於distance的方式進行壓縮。

前面分析過,literal表示未匹配的字符,咱們前面之因此拿漢字來舉例,徹底是爲了便於理解,ZIP之因此是通用壓縮,它其實是針對字節做爲基本字符來編碼的,因此一個literal至多有256種可能。

length表示重複字符串長度,length=1固然不會出現,由於一個字符不值得用distance+length去記錄,重複字符串固然越長越好,Phil Katz(下面仍是簡稱PK了,拷貝太麻煩)認爲,length=2也不值得用這種方式記錄,仍是過短了,因此PK把length最小值認爲是3,必須3個以上字符的字符串出現重複才用distance+length記錄。那麼,最大的length是多少呢?理論上固然能夠很長很長,好比一個文件就是連續的0,這個重複字符串長度其實接近於這個文件的實際長度。可是PK把length的範圍作了限制,限定length的個數跟literal同樣,也只有256個,由於PK認爲,一個重複字符串達到了256個已經很長了,機率很是小;另外,其實哪怕超過了256,我仍是認爲是一段256再加上另一段,增長一個distance+length就好了嘛,並不影響結果。並且這樣作,我想一樣也考慮了硬件條件吧。

初看有點奇怪的在於,將literal和length兩者合二爲一,什麼意思呢?就是對這兩種整數(literal本質上是一個字節)共用一個Huffman碼錶,一下子解釋爲何。PK對Huffman的理解我以爲達到了爐火純青的地步,前面已經看到,後面還會看到。他認爲Huffman編碼的輸入反正說白了就是一個集合的元素就行,不管這個元素是啥,因此多個集合看作一個集合看成Huffman編碼的輸入沒啥問題。literal用整數0-255表示,256是一個結束標誌,解碼之後結果是256表示解碼結束;從257開始表示length,因此257這個數表示length=3,258這個數表示length=4等等,但PK也不是一直這麼一一對應,和distance同樣,也是把length(總共256個值)劃分爲29個區間,其結果以下圖:

其中的含義和distance相似,再也不贅述,因此literal/length這個Huffman編碼的輸入元素一共285個,其中256表示解碼結束標誌。爲何要把兩者合二爲一呢?由於當解碼器接收到一個比特流的時候,首先能夠按照literal/length這個碼錶來解碼,若是解出來是0-255,就表示未匹配字符,若是是256,那天然就結束,若是是257-285之間,則表示length,把後面擴展比特加上造成length後,後面的比特流確定就表示distance,所以,實際上經過一個Huffman碼錶,對各種狀況進行了統一,而不是經過加一個什麼標誌來區分究竟是literal仍是重複字符串。

好了,理解了上面的過程,就理解了ZIP壓縮的第二步,第一步是LZ編碼,第二步是對LZ編碼後結果(literal、distance、length)進行的再編碼,由於literal/length是一個碼錶,我稱其爲Huffman碼錶1,distance那個碼錶稱爲Huffman碼錶2。前面咱們已經分析了,Huffman碼樹用一個碼字長度序列表示,稱爲CL(Code Length),記錄兩個碼錶的碼字長度序列分別記爲CL一、CL2。碼樹記錄下來,對literal/length的編碼比特流稱爲LIT比特流;對distance的編碼比特流稱爲DIST比特流。

按照上面的方法,LZ的編碼結果就變成四塊:CL一、CL二、LIT比特流、DIST比特流。CL一、CL2是碼字長度的序列,這個序列說白了就是一堆正整數,所以,PK繼續深挖,認爲這個序列還應該繼續壓縮,也就是說,對碼錶進行壓縮。

 

七、ZIP中對CL進行再次壓縮的方法

這裏仍然沿用Huffman的想法,由於CL也是一堆整數,那麼固然能夠再次應用Huffman編碼。不過在這以前,PK對CL序列進行了一點處理。這個處理也是很精巧的。

CL序列表示一系列整數對應的碼字長度,對於literal/length來講,總共有0-285這麼多符號,因此這個序列長度爲286,每一個符號都有一個碼字長度,固然,這裏面可能會出現大段連續的0,由於某些字符或長度不存在,尤爲是對英文文本編碼的時候,非ASCII字符就根本不會出現,length較大的值出現機率也很小,因此出現大段的0是很正常的;對於distance也相似,也可能出現大段的0。PK因而先進行了一下游程編碼。在說什麼是遊程編碼以前,咱們談談PK對CL序列的認識。

literal/length的編碼符號總共286個(回憶:256個Literal+1個結束標誌+29個length區間),distance的編碼符號總共30個(回憶:30個區間),因此這顆碼樹不會特別深,Huffman編碼後的碼字長度不會特別長,PK認爲最長不會超過15,也就是樹的深度不會超過15,這個是不是理論證實我尚未分析,有興趣的同窗能夠分析一下。所以,CL1和CL2這兩個序列的任意整數值的範圍是0-15。0表示某個整數沒有出現(好比literal=0x12, length Code=8, distance Code=15等等)。

什麼叫遊程呢?就是一段徹底相同的數的序列。什麼叫遊程編碼呢?提及來原理更簡單,就是對一段連續相同的數,記錄這個數一次,緊接着記錄出現了多少個便可。David的書中舉了這個例子,好比CL序列以下:

4, 4, 4, 4, 4, 3, 3, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2
那麼,遊程編碼的結果爲:

4, 16, 01(二進制), 3, 3, 3, 6, 16, 11(二進制), 16, 00(二進制), 17,011(二進制), 2, 16, 00(二進制)
這是什麼意思呢?由於CL的範圍是0-15,PK認爲重複出現2次過短就不用遊程編碼了,因此遊程長度從3開始。用16這個特殊的數表示重複出現三、四、五、6個這樣一個遊程,分別後面跟着00、0一、十、11表示(實際存儲的時候須要低比特優先存儲,須要把比特倒序來存,博文的一些例子有時候會忽略這點,實際寫程序的時候必定要注意,不然會獲得錯誤結果)。因而4,4,4,4,4,這段遊程記錄爲4,16,01,也就是說,4這個數,後面還會連續出現了4次。6,16,11,16,00表示6後面還連續跟着6個6,再跟着3個6;由於連續的0出現的可能不少,因此用1七、18這兩個特殊的數專門表示0遊程,17後面跟着3個比特分別記錄長度爲3-10(總共8種可能)的遊程;18後面跟着7個比特表示11-138(總共128種可能)的遊程。17,011(二進制)表示連續出現6個0;18,0111110(二進制)表示連續出現62個0。總之記住,0-15是CL可能出現的值,16表示除了0之外的其它遊程;1七、18表示0遊程。由於二進制實際上也是個整數,因此上面的序列用整數表示爲:

4, 16, 1, 3, 3, 3, 6, 16, 3, 16, 0, 17, 3, 2, 16, 0

咱們又看到了一串整數,這串整數的值的範圍是0-18。這個序列稱爲SQ(Sequence的意思)。由於有兩個CL一、CL2,因此對應的有兩個SQ一、SQ2。

針對SQ一、SQ2,PK用了第三個Huffman碼錶來對這兩個序列進行編碼。經過統計各個整數(0-18範圍內)的出現次數,按照相同的思路,對SQ1和SQ2進行了Huffman編碼,獲得的碼流記爲SQ1 bits和SQ2 bits。同時,這裏又須要記錄第三個碼錶,稱爲Huffman碼錶3。同理,這個碼錶也用相同的方法記錄,也等效爲一個碼長序列,稱爲CCL,由於至多有0-18個,PK認爲樹的深度至多爲7,因而CCL的範圍是0-7。

當獲得了CCL序列後,PK決定再也不折騰,對這個序列用普通的3比特定長編碼記錄下來便可,即000表明0,111表明7。但實際上還有一點小折騰,就是最後這個序列若是所有記錄,那就須要19*3=57個比特,PK認爲CL序列裏面CL範圍爲0-15,特殊的幾個值是1六、1七、18,若是把CCL序列位置置換一下,把1六、1七、18這些放前面,那麼這個CCL序列就極可能最後面跟着一串0(由於CL=14,15這些極可能沒有),因此最後還引入了一個置換,其示意圖以下,分別表示置換前的CCL序列和置換後的CCL。能夠看出,1六、1七、18對應的CCL被放到了前面,這樣若是尾部出現了一些0,就只須要記錄CCL長度便可,後面的0不記錄。能夠繼續節省一些比特,不過這個例子尾部置換後只有1個0:

不過粗看起來,這個置換效果並很差,我一開始接觸這個置換的時候,我以爲應該按照1六、1七、1八、0、一、二、三、。。。這樣的順序來存儲,若是按照我理解的,那麼置換後的結果以下:

二、四、0、四、五、五、一、五、0、六、0、0、0、0、0、0、0、0、0

這樣後面的一大串0直接截斷,比PK的方法更短。但PK卻按照上面的順序。我老是認爲,我以爲牛人可能出錯了的時候,每每是我本身錯了,因此我又仔細想了一下,上面的順序特色比較明顯,直觀上看,PK認爲CL爲0和中間的值出現得比較多(放在了前面),但CL比較小的和比較大的出現得比較少(一、1五、二、14這些放在了後面,你看,後面交叉着放),在文件比較小的時候,這種方法效果不算好,上面就是一個典型的例子,但文件比較大了之後,CL一、CL2碼樹比較大,碼字長度廣泛比較長,大部分極可能接近於中間值,那麼這個時候PK的方法可能就體現出優點了。不得不說,對一個算法或者數據結構的優化程度,簡直徹底取決於程序員對那個東西細節的理解的深度。當我仔細研究了ZIP壓縮算法的過程以後,我對PK這種深夜埋頭左思右想的大牛佩服得五體投地。

到此爲止,ZIP壓縮算法的結果已經完畢。這個算法命名爲Deflate算法。總結一下其編碼流程爲:

 

八、Deflate壓縮數據格式

ZIP的格式實際上就是Deflate壓縮碼流外面套了一層文件相關的信息,這裏先介紹Deflate壓縮碼流格式。其格式爲:

Header:3個比特,第一個比特若是是1,表示此部分爲最後一個壓縮數據塊;不然表示這是.ZIP文件的某個中間壓縮數據塊,但後面還有其餘數據塊。這是ZIP中使用分塊壓縮的標誌之一;第二、3比特表示3個選擇:壓縮數據中沒有使用Huffman、使用靜態Huffman、使用動態Huffman,這是對LZ77編碼後的literal/length/distance進行進一步編碼的標誌。咱們前面分析的都是動態Huffman,其實Deflate也支持靜態Huffman編碼,靜態Huffman編碼原理更爲簡單,無需記錄碼錶(由於PK本身定義了一個固定的碼錶),但壓縮率不高,因此大多數狀況下都是動態Huffman。

HLIT:5比特,記錄literal/length碼樹中碼長序列(CL1)個數的一個變量。後面CL1個數等於HLIT+257(由於至少有0-255總共256個literal,還有一個256表示解碼結束,但length的個數不定)。

HDIST:5比特,記錄distance碼樹中碼長序列(CL2)個數的一個變量。後面CL2個數等於HDIST+1。哪怕沒有1個重複字符串,distance都爲0也是一個CL。

HCLEN:4比特,記錄Huffman碼錶3中碼長序列(CCL)個數的一個變量。後面CCL個數等於HCLEN+4。PK認爲CCL個數不會低於4個,即便對於整個文件只有1個字符的狀況。

接下來是3比特編碼的CCL,一共HCLEN+4個,用以構造Huffman碼錶3;

接下來是對CL1(碼長)序列通過遊程編碼(SQ1:縮短的整數序列)後,並對SQ1繼續用Huffman編碼後的比特流。包含HLIT+257個CL1,其解碼碼錶爲Huffman碼錶3,用以構造Huffman碼錶1;

接下來是對CL2(碼長)序列通過遊程編碼(SQ2:縮短的整數序列)後,並對SQ2繼續用Huffman編碼後的比特流。包含HDIST+1個CL2,其解碼碼錶爲Huffman碼錶3,用於構造Huffman碼錶2;

總之,上面的數據都是爲了構造LZ解碼須要的2個Huffman碼錶。

接下來纔是通過Huffman編碼的壓縮數據,解碼碼錶爲Huffman碼錶1和碼錶2。
最後是數據塊結束標誌,即literal/length這個碼錶輸入符號位256的編碼比特。
對倒數第一、2內容塊進行解碼時,首先利用Huffman碼錶1進行解碼,若是解碼所得整數位於0-255之間,表示literal未匹配字符,接下來仍然利用Huffman碼錶1解碼;若是位於257-285之間,表示length匹配長度,以後須要利用Huffman碼錶2進行解碼獲得distance偏移距離;若是等於256,表示數據塊解碼結束。

 

九、ZIP文件格式解析

 上面各節對ZIP的原理進行了分析,這一節咱們來看一個實際的例子,爲了更好地描述,咱們儘可能把這個例子舉得簡單一些。下面是我隨便從一本書拷貝出來的一段較短的待壓縮的英文文本數據:

As mentioned above,there are many kinds of wireless systems other than cellular.

這段英文文本長度爲80字節。通過ZIP壓縮後,其內容以下:

能夠看到,第一、2字節就是PK。看着怎麼比原文還長,這怎麼叫壓縮?實際上,這裏面大部份內容是ZIP的文件標記開銷,真正壓縮的內容(也就是咱們前面提到的Deflate數據,劃線部分都是ZIP文件開銷)其實確定要比原文短(不然ZIP不會啓用壓縮),咱們這個例子是個短文本,但對於更長的文本而言,那ZIP文件整體長度確定是要短於原始文本的。上面的這個ZIP文件,能夠看到好幾個以PK開頭的區域,也就是不一樣顏色的劃線區域,這些其實都是ZIP文件自己的開銷。

因此,咱們首先來看一看ZIP的格式,其格式定義爲:

[local file header 1]
[file data 1]
[data descriptor 1]
..........
[local file header n]
[file data n]
[data descriptor n]
[archive decryption header]
[archive extra data record]
[central directory]
[zip64 end of central directory record]
[zip64 end of central directory locator]
[end of central directory record]
local file header+file data+data descriptor這是一段ZIP壓縮數據,在一個ZIP文件裏,至少有一段,至多那就很差說了,假如你要壓縮的文件一共有10個,那這個地方至少會有10段,ZIP對每一個文件進行了獨立壓縮,RAR在此進行了改進,將多個文件聯合起來進行壓縮,提升了壓縮率。local file header的格式以下:

可見,起始的4個字節就是0x50(P)、0x4B(K)、0x0三、0x04,由於是低字節優先,因此Signature=0x03044B50.接下來的內容按照上面的格式解析,十分簡單,這個區域在上面ZIP數據的那個圖裏面是紅色劃線區域,以後則是壓縮後的Deflate數據。在文件的尾部,還有ZIP尾部數據,上面這個例子包含了central directory和end of central directory record,通常這兩部分也是必須的。central directory以0x50、0x4B、0x0一、0x02開頭;end of central directory record以0x50、0x4B、0x0五、0x06開頭,其含義比較簡單,分別對應於上面ZIP數據那個圖的藍色和綠色部分,下面是二者的格式:

end of central directory record格式:

這幾張圖是我從網上找的,寫得比較清晰。對於其中的含義,解釋起來也比較簡單,我分析的結果以下:注意ZIP採用的低字節優先,在一個字節裏面低位優先,須要反過來看。

Local File Header: (38B,304b)
00001010110100101100000000100000 (signature)
0000000000010100 (version:20)
0000000000000000 (generalBitFlag)
0000000000001000 (compressionMethod:8)
0100110110001110 (lastModTime:19854)
0100010100100101 (lastModDate:17701)
01010100101011010100001100111100 (CRC32)
00000000000000000000000001001000 (compressedSize:72)
00000000000000000000000001010000 (uncompressedSize:80)
0000000000001000 (filenameLength:8)
0000000000000000 (extraFieldLength:0)
0010101010100110110011100010111001110100001011100001111000101110 (fileName:Test.txt)
 (extraField)


Central File Header: (54B,432b)
00001010110100101000000001000000 (signature)
0000000000010100 (versionMadeBy:20)
0000000000010100 (versionNeeded:20)
0000000000000000 (generalBitFlag)
0000000000001000 (compressionMethod:8)
0100110110001110 (lastModTime:19854)
0100010100100101 (lastModDate:17701)
01010100101011010100001100111100 (CRC32)
00000000000000000000000001001000 (compressedSize:72)
00000000000000000000000001010000 (uncompressedSize:80)
0000000000001000 (filenameLength:8)
0000000000000000 (extraFieldLength:0)
0000000000000000 (fileCommenLength:0)
0000000000000000 (diskNumberStart)
0000000000000001 (internalFileAttr)
10000001100000000000000000100000 (externalFileAttr)
00000000000000000000000000000000 (relativeOffsetLocalHeader)
0010101010100110110011100010111001110100001011100001111000101110 (fileName:Test.txt)
 (extraField)
 (fileComment)


end of Central Directory Record: (22B,176b)
00001010110100101010000001100000 (signature)
0000000000000000 (numberOfThisDisk:0)
0000000000000000 (numberDiskCentralDirectory:0)
0000000000000001 (EntriesCentralDirectDisk:1)
0000000000000001 (EntriesCentralDirect:1)
00000000000000000000000000110110 (sizeCentralDirectory:54)
00000000000000000000000001101110 (offsetStartCentralDirectory:110)
0000000000000000 (fileCommentLength:0)
 (fileComment)

Local File Header Length:304
Central File Header Length:432
End Central Directory Record Length:176

可見,開銷總的長度爲38+54+22=114字節,整個文件長度爲186字節,所以Deflate壓縮數據長度爲72字節(576比特)。儘管這裏看起來只是從80字節壓縮到72字節,那是由於這是一段短文本,重複字符串出現較少,但若是文本較長,那壓縮率就會增長,這裏只是舉個例子。

下面對其中的關鍵部分,也就是Deflate壓縮數據進行解析。

 

10,Deflate解碼過程實例分析

咱們按照ZIP格式把Deflate壓縮數據(72字節)提取出來,以下(每行8字節):

1010100001010011100010111011000000000001000001000011000010100010
1000101110101010011110110000000001100011101110000011100010100101
0101001111001100000010001101001010010010000101101010101100001101
1011110100011111100011101111111001110010011101110110011100010101
0010110100010100101100110001100100000100110111101101111000011101
0010001001100110111001000010011001101010101000110110000001110101
0100011010010011100010110111001000111101101001011100101010010111
0111000011111000011110000011010111001011011111111100100010001001
1010001100001110000010101010111101101010100101111101011111100000

Deflate格式除了上面的介紹,也能夠參考RFC1951,解析以下:

Header:101, 第一個比特是1,表示此部分爲最後一個壓縮數據塊;後面的兩個比特01表示採用動態哈夫曼、靜態哈夫曼、或者沒有編碼的標誌,01表示採用動態Huffman;在RFC1951裏面是這麼說明的:

00 - no compression

01 - compressed with fixed Huffman codes

10 - compressed with dynamic Huffman codes

11 - reserved (error)

注意,這裏須要按照低比特在先的方式去看,不然會誤覺得是靜態Huffman。

接下來:
HLIT:01000,記錄literal/length碼樹中碼長序列個數的一個變量,表示HLIT=2(低位在前),說明後面存在HLIT + 257=259個CL1,CL1即0-258被編碼後的長度,其中0-255表示Literal,256表示無效符號,25七、258分別表示Length=三、4(length從3開始)。所以,這裏實際上只出現了兩種重複字符串的長度,即3和4。回顧這個圖能夠更清楚:

繼續:
HDIST:01010,記錄distance碼樹中碼長序列個數的一個變量,表示HDIST=10,說明後面存在HDIST+1=11個CL2,CL2即Distance Code=0-10被編碼的長度。

繼續:

HCLEN:0111,記錄Huffman碼樹3中碼長序列個數的一個變量,表示HCLEN=14(1110二進制),即說明緊接着跟着HCLEN+4=18個CCL,前面已經分析過,CCL記錄了一個Huffman碼錶,這個碼錶能夠用一個碼長序列表示,根據這個碼長序列能夠獲得碼錶。因而接下來咱們把後面的18*3=54個比特拷貝出來,上面的碼流目前解析爲下面的結果:

101(Header) 01000(HLIT) 01010(HDIST) 0111(HCLEN)
000 101 110 110 000 000 000 010 000 010 000 110 000 101 000 101 000 101 (CCL)
110101010011110110000000001100011101110000011100010100101
0101001111001100000010001101001010010010000101101010101100001101
1011110100011111100011101111111001110010011101110110011100010101
0010110100010100101100110001100100000100110111101101111000011101
0010001001100110111001000010011001101010101000110110000001110101
0100011010010011100010110111001000111101101001011100101010010111
0111000011111000011110000011010111001011011111111100100010001001
1010001100001110000010101010111101101010100101111101011111100000

標準的CCL長度爲19(回憶一下:CCL範圍爲0-18,按照整數大小排序記錄各自的碼字長度),所以最後一個補0。獲得序列:

000 101 110 110 000 000 000 010 000 010 000 110 000 101 000 101 000 101 000

其長度分別爲(低位在前):
0、五、三、三、0、0、0、二、0、二、0、三、0、五、0、五、0、五、0
前面已經分析過,這個CCL序列其實是通過一次置換操做獲得的,須要進行相反的置換,置換後爲:

三、五、五、五、三、二、二、0、0、0、0、0、0、0、0、0、0、五、3
這個就是對應於0-18的碼字長度序列。
根據Deflate樹的構造方式,獲得下面的碼錶(Huffman碼錶3):

00      <-->   5
01      <-->   6
100     <-->  0
101     <-->  4
110     <-->  18
11100   <-->1
11101   <-->2
11110   <-->3
11111   <-->17

接下來就是CL1序列,按照前面的指示,一共有259個,分別對應於literal/length:0-258對應的碼字長度序列,咱們隊跟着CCL後面的比特按照上面得到的碼錶進行逐步解碼,在解碼以前,實際上並不知道CL1的比特流長度有多少,須要根據259這個數字來斷定,解完了259個整數,代表解析CL1完畢:

101(Header) 01000(HLIT) 01010(HDIST) 0111(HCLEN)
000 101 110 110 000 000 000 010 000 010 000 110 000 101 000 101 000 101 (CCL)

110(18)1010100(7比特,記錄連續的11-138個0,此處一共0010101b=21,即記錄21+11=32個0)

11110(3)110(18)0000000(7比特,記錄連續的11-138個0,此處爲全0,即記錄0+11=11個0)

01(6)100(0)01(6)110(18)1110000(7比特,記錄連續的11-138個0,此處爲111b=7,即記錄7+11=18個0)

01(6)110(18)0010100(7比特,記錄連續的11-138個0,此處爲10100b=20,即記錄20+11=31個0)

101(4)01(6)01(6)00(5)11110(3)01(6)100(0)00(5)00(5)100(0)01(6)101(4)

00(5)101(4)00(5)100(0)100(0)00(5)101(4)101(4)01(6)01(6)01(6)100(0)

00(5)110(18)1101111(7比特,記錄連續的11-138個0,此處爲1111011b=123,即記錄123+11=134個0)

統計一下,上面已經解了32+11+18+31+134+30=256個數了,由於總共259個,還差三個:

01(6)00(5)01(6)

好了,CL1比特流解析完畢了,獲得的CL1碼長序列爲:

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 0 0 0 0 0 0 0
0 0 0 0 6 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 4 6 6 5 3 6 0 5 5 0 6 4 5 4 5 0 0 5 4 4 6 6 6
0 5 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6 5 6

總共259個,每行40個。根據這個序列,一樣按照Deflate樹構造方法,獲得literal/length碼錶(Huffman碼錶1)爲:

000     --> (System.Char)(看前面的CL1序列,空格對應的ASCII爲0x20=32,碼字長度3,即上面序列中第一個3)
001     -->e(System.Char)
0100    -->a(System.Char)
0101    -->l(System.Char)
0110    -->n(System.Char)
0111    -->s(System.Char)
1000    -->t(System.Char)
10010   -->d(System.Char)
10011   -->h(System.Char)
10100   -->i(System.Char)
10101   -->m(System.Char)
10110   -->o(System.Char)
10111   -->r(System.Char)
11000   -->y(System.Char)
11001   -->3(System.Int32)(看前面的CL1序列,對應257,碼字長度5)
110100  -->,(System.Char)
110101  -->.(System.Char)
110110  -->A(System.Char)
110111  -->b(System.Char)
111000  -->c(System.Char)
111001  -->f(System.Char)
111010  -->k(System.Char)
111011  -->u(System.Char)
111100  -->v(System.Char)
111101  -->w(System.Char)
111110  -->-1(System.Int32)(看前面的CL1序列,對應256,碼字長度6)
111111  -->4(System.Int32)(看前面的CL1序列,對應258,碼字長度6)

能夠看出,碼錶裏存在兩個重複字符串長度3和4,當解碼結果爲-1(上面進行了處理,即256),或者說遇到111110的時候,表示Deflate碼流結束。

按照一樣的道理,對CL2序列進行解析,前面已經知道HDIST=10,即有11個CL2整數序列:

11111(17)000(3比特,記錄連續的3-10個0,此處爲0,即記錄3個0)

11101(2)11111(17)100(3比特,記錄連續的3-10個0,此處爲001b=1,即記錄4個0)

11100(1)100(0)11101(2)

已經結束,總共11個。

因而CL2序列爲:

0 0 0 2 0 0 0 0 1 0 2

分別記錄的是distance碼爲0-10的碼字長度,根據下面的對應關係,須要進行擴展:

好比,第1個碼長2記錄的是Code=3的長度,即Distance=4對應的碼字爲:

10      -->4(System.Int32)

第1個碼長1記錄的是Code=8的長度(碼字爲0,擴展三位000-111),即Distance=17-24對應的碼字爲(注意,低比特優先):

0 000    -->17(System.Int32)
0 100    -->18(System.Int32)
0 010    -->19(System.Int32)
0 110    -->20(System.Int32)
0 001    -->21(System.Int32)
0 101    -->22(System.Int32)
0 011    -->23(System.Int32)
0 111    -->24(System.Int32)

注意,擴展的時候仍是低比特優先。

最後1個碼長2記錄的是Code=10的長度(實際上是碼字:11,擴展四位0000-1111),即Distance=33-48對應的碼字爲:

11 0000  -->33(System.Int32)
11 1000  -->34(System.Int32)
11 0100  -->35(System.Int32)
11 1100  -->36(System.Int32)
11 0010  -->37(System.Int32)
11 1010  -->38(System.Int32)
11 0110  -->39(System.Int32)
11 1110  -->40(System.Int32)
11 0001  -->41(System.Int32)
11 1001  -->42(System.Int32)
11 0101  -->43(System.Int32)
11 1101  -->44(System.Int32)
11 0011  -->45(System.Int32)
11 1011  -->46(System.Int32)
11 0111  -->47(System.Int32)
11 1111  -->48(System.Int32)

至此爲止,Huffman碼錶一、Huffman碼錶2已經還原出來,接下來是對LZ壓縮所獲得的literal、distance、length進行解碼,目前剩餘的比特流以下,先按照Huffman碼錶1解碼,若是解碼結果是長度(>256),則接下來按照Huffman碼錶2解碼,逐步解碼便可:

[As ]:110110(A)0111(s)000(空格)

[mentioned ]:10101(m)001(e)0110(n)1000(t)10100(i)10110(o)0110(n)001(e)10010(d)000(空格)

[above,]:0100(a)110111(b)10110(o)111100(v)001(e)110100(,)

[there ]:1000(t)10011(h)001(e)10111(r)001(e)000(空格)

[are ]:0100(a)11001(長度3,表示下一個須要用Huffman解碼)10(Distance=4,即重複字符串爲re空格)

[many ]:10101(m)0100(a)0110(n)11000(y)000(空格)

[kinds ]:111010(k)10100(i)0110(n)10010(d)0111(s)000(空格)

[of ]:10110(o)111001(f)000(空格)

[wireless ]:111101(w)10100(i)10111(r)001(e)0101(l)001(e)0111(s)0111(s)000(空格)

[systems o]:0111(s)11000(y)0111(s)1000(t)001(e)10101(m)11001(長度指示=3,接下來根據distance解碼)0110(Distance=20,即重複字符串爲s o)

[ther ]:111111(長度指示=4,接下來根據distance解碼)111001(Distance=42,即重複字符串爲ther)000(空格)

[than ]:1000(t)10011(h)0100(a)0110(n)000(空格)

[cellular.]:111000(c)001(e)0101(l)0101(l)111011(u)0101(l)0100(a)10111(r)110101(.)

[256,結束標誌]111110(結束標誌)0000(字節補齊的0)

因而解壓縮結果爲:

As mentioned above,there are many kinds of wireless systems other than cellular.

再來回顧咱們的解碼過程:

譯碼過程:
一、根據HCLEN獲得截尾信息,並參照固定置換表,根據CCL比特流獲得CCL整數序列;
二、根據CCL整數序列構造出等價於CCL的二級Huffman碼錶3;
三、根據二級Huffman碼錶3對CL一、CL2比特流進行解碼,獲得SQ1整數序列,SQ2整數序列;
四、根據SQ1整數序列,SQ2整數序列,利用遊程編碼規則獲得等價的CL1整數序列、CL2整數序列;
五、根據CL1整數序列、CL2整數序列分別構造兩個一級Huffman碼錶:literal/length碼錶、distance碼錶;
六、根據兩個一級Huffman碼錶對後面的LZ壓縮數據進行解碼獲得literal/length/distance流;
七、根據literal/length/distance流按照LZ規則進行解碼。

Deflate碼流長度總共爲72字節=576比特,其中:

3比特Header;

5比特HLIT;

5比特HDIST;

4比特HCLEN;

54比特CCL序列碼流;

133比特CL1序列碼流;

34比特CL2序列碼流;

338比特LZ壓縮後的literal/length/distance碼流。

十一、ZIP的其它說明

上面各個環節已經詳細分析了ZIP壓縮的過程以及解碼流程,經過對一個實例的解壓縮過程分析,能夠完全地掌握ZIP壓縮和解壓縮的原理和過程。還有一些狀況須要說明:

(1)上面的算法複雜度主要在於壓縮一端,由於須要統計literal/length/distance,建立動態Huffman碼錶,相反解壓只須要還原碼錶後,逐比特解析便可,這也是壓縮軟件的一個典型特色,解壓速度遠快於壓縮速度。

(2)上面咱們分析了動態Huffman,對於LZ壓縮後的literal/length/distance,也能夠採用靜態Huffman編碼,這主要取決於ZIP在壓縮中看哪一種方式更節省空間,靜態Huffman編碼不須要記錄碼錶,由於這個碼錶是固定的,在RFC1951裏面也有說明。對於literal/length碼錶來講,須要對0-285進行編碼,其碼錶爲:

對於Distance來講,須要對Code=0-29的數進行編碼,則直接採用5比特表示。Distance和動態Huffman同樣,在此基礎上進行擴展。

(3)ZIP中使用的LZ77算法是一種改進的LZ77。主要區別有兩點:

1)標準LZ77在找到重複字符串時輸出三元組(length, distance, 下一個未匹配的字符)(有興趣能夠關注LZ77那篇論文);Deflate在找到重複字符串時僅輸出雙元組(length, distance)。
2)標準LZ77使用」貪婪「的方式解析,尋找的都是最長匹配字符串。Deflate中不徹底如此。David Salomon的書裏給了一個例子:

對於上面這個例子,標準LZ77在滑動窗口中查找最長匹配字符串,找到的是"the"與前面的there的前三個字符匹配,這種貪婪解析方式邏輯簡單,但編碼效率不必定最高。Deflate則不急於輸出,跳過t繼續日後查看,發現"th ne"這5個字符存在重複字符串,所以,Deflate算法會選擇將t做爲未匹配字符輸出,而對後面的匹配字符串用(length, distance)編碼輸出。顯然,這樣就提升了壓縮效率,由於標準的LZ77找到的重複字符串長度爲3,而Deflate找到的是5。換句話說,Deflate算法並非簡單的尋找最長匹配後輸出,而是會權衡幾種可行的編碼方式,用其中最高效的方式輸出。

 

十二、總結

本篇博文對ZIP中使用的壓縮算法進行了詳細分析,從一個簡單地例子出發,一步步地分析了PK設計Deflate算法的思路。最後,經過一個實際例子,分析了其解壓縮流程。總的來看,ZIP的核心在於如何對LZ壓縮後的literal、length、distance進行Huffman編碼,以及如何以最小空間記錄Huffman碼錶。整個過程充滿了對數據結構尤爲是樹的深刻優化利用。按照上面的分析,若是要對ZIP進行進一步改進,能夠考慮的地方也有很多,典型的有:

(1)擴大LZ編碼的滑動窗口的大小;

(2)將Huffman編碼改進爲算術編碼等壓縮率更高的方法,畢竟,Huffman的碼字長度必須爲整數,這就從理論上限制了它的壓縮率只能接近於理論極限,但難以達到。我記得在JPEG圖像編碼領域,之前的JPEG採用了DCT變換編碼+Huffman的方式,如今JPEG2000將其改成小波變換+算數編碼,因此數據壓縮也能夠嘗試相似的思路;

(3)將多個文件進行合併壓縮,ZIP中,不一樣的文件壓縮過程沒有關係,獨立進行,若是將它們合併起來一塊兒進行壓縮,壓縮率能夠獲得進一步提升。

 

描述分析有誤的地方,敬請指正。針對數據壓縮相關的話題,後續會對HBase列壓縮等等進行分析,看看ZIP這種文件壓縮和HBase這種數據庫數據壓縮的區別和聯繫。

相關文章
相關標籤/搜索