日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

ZIP壓縮算法詳細(xì)分析及解壓實(shí)例解釋

 gljin_cn 2014-09-15

原文出處: esingchan   歡迎分享原創(chuàng)到伯樂頭條

最近自己實(shí)現(xiàn)了一個(gè)ZIP壓縮數(shù)據(jù)的解壓程序,覺得有必要把ZIP壓縮格式進(jìn)行一下詳細(xì)總結(jié),數(shù)據(jù)壓縮是一門通信原理和計(jì)算機(jī)科學(xué)都會(huì)涉及到的學(xué)科,在通信原理中,一般稱為信源編碼,在計(jì)算機(jī)科學(xué)里,一般稱為數(shù)據(jù)壓縮,兩者本質(zhì)上沒啥區(qū)別,在數(shù)學(xué)家看來,都是映射。

一方面在進(jìn)行通信的時(shí)候,有必要將待傳輸?shù)臄?shù)據(jù)進(jìn)行壓縮,以減少帶寬需求;另一方面,計(jì)算機(jī)存儲(chǔ)數(shù)據(jù)的時(shí)候,為了減少磁盤容量需求,也會(huì)將文件進(jìn)行壓縮,盡管現(xiàn)在的網(wǎng)絡(luò)帶寬越來越高,壓縮已經(jīng)不像90年代初那個(gè)時(shí)候那么迫切,但在很多場合下仍然需要,其中一個(gè)原因是壓縮后的數(shù)據(jù)容量減小后,磁盤訪問IO的時(shí)間也縮短,盡管壓縮和解壓縮過程會(huì)消耗CPU資源,但是CPU計(jì)算資源增長得很快,但是磁盤IO資源卻變化得很慢,比如目前主流的SATA硬盤仍然是7200轉(zhuǎn),如果把磁盤的IO壓力轉(zhuǎn)化到CPU上,總體上能夠提升系統(tǒng)運(yùn)行速度。

壓縮作為一種非常典型的技術(shù),會(huì)應(yīng)用到很多很多場合下,比如文件系統(tǒng)、數(shù)據(jù)庫、消息傳輸、網(wǎng)頁傳輸?shù)鹊雀黝悎龊?。盡管壓縮里面會(huì)涉及到很多術(shù)語和技術(shù),但無需擔(dān)心,博主盡量將其描述得通俗易懂。另外,本文涉及的壓縮算法非常主流并且十分精巧,理解了ZIP的壓縮過程,對理解其它相關(guān)的壓縮算法應(yīng)該就比較容易了。

1、引子

壓縮可以分為無損壓縮和有損壓縮,有損,指的是壓縮之后就無法完整還原原始信息,但是壓縮率可以很高,主要應(yīng)用于視頻、話音等數(shù)據(jù)的壓縮,因?yàn)閾p失了一點(diǎn)信息,人是很難察覺的,或者說,也沒必要那么清晰照樣可以看可以聽;無損壓縮則用于文件等等必須完整還原信息的場合,ZIP自然就是一種無損壓縮,在通信原理中介紹數(shù)據(jù)壓縮的時(shí)候,往往是從信息論的角度出發(fā),引出香農(nóng)所定義的熵的概念,這方面的介紹實(shí)在太多,這里換一種思路,從最原始的思想出發(fā),為了達(dá)到壓縮的目的,需要怎么去設(shè)計(jì)算法。而ZIP為我們提供了相當(dāng)好的案例。

盡管我們不去探討信息論里面那些復(fù)雜的概念,不過我們首先還是要從兩位信息論大牛談起。因?yàn)槭撬麄兊旎私裉齑蠖鄶?shù)無損數(shù)據(jù)壓縮的核心,包括ZIP、RAR、GZIP、GIF、PNG等等大部分無損壓縮格式。這兩位大牛的名字分別是Jacob Ziv和Abraham Lempel,是兩位以色列人,在1977年的時(shí)候發(fā)表了一篇論文《A Universal Algorithm for Sequential Data Compression》,從名字可以看出,這是一種通用壓縮算法,所謂通用壓縮算法,指的是這種壓縮算法沒有對數(shù)據(jù)的類型有什么限定。不過論文我覺得不用仔細(xì)看了,因?yàn)椴┲髯鳛橐幻ㄐ艑I(yè)的PHD,看起來也焦頭爛額,不過我們后面可以看到,它的思想還是很簡單的,之所以看起來復(fù)雜,主要是因?yàn)镮EEE的某些雜志就是這個(gè)特點(diǎn),需要從數(shù)學(xué)上去證明,這種壓縮算法到底有多優(yōu),比如針對一個(gè)各態(tài)歷經(jīng)的隨機(jī)序列(不用追究什么叫各態(tài)歷經(jīng)隨機(jī)序列),經(jīng)過這樣的壓縮算法后,是否可以接近信息論里面的極限(也就是前面說的熵的概念)等等,不過在理解其思想之前,個(gè)人認(rèn)為沒必要深究這些東西,除非你要發(fā)論文。這兩位大牛提出的這個(gè)算法稱為LZ77,兩位大牛過了一年又提了一個(gè)類似的算法,稱為LZ78,思想類似,ZIP這個(gè)算法就是基于LZ77的思想演變過來的,但ZIP對LZ77編碼之后的結(jié)果又繼續(xù)進(jìn)行壓縮,直到難以壓縮為止。除了LZ77、LZ78,還有很多變種的算法,基本都以LZ開頭,如LZW、LZO、LZMA、LZSS、LZR、LZB、LZH、LZC、LZT、LZMW、LZJ、LZFG等等,非常多,LZW也比較流行,GIF那個(gè)動(dòng)畫格式記得用了LZW。我也寫過解碼程序,以后有時(shí)間可以再寫一篇,但感覺跟LZ77這些類似,寫的必要性不大。

ZIP的作者是一個(gè)叫Phil Katz的人,這個(gè)人算是開源界的一個(gè)具有悲劇色彩的傳奇人物。雖然二三十年前,開源這個(gè)詞還沒有現(xiàn)在這樣風(fēng)起云涌,但是總有一些具有黑客精神的牛人,內(nèi)心里面充滿了自由,無論他處于哪個(gè)時(shí)代。Phil Katz這個(gè)人是個(gè)牛逼程序員,成名于DOS時(shí)代,我個(gè)人也沒有經(jīng)歷過那個(gè)時(shí)代,我是從Windows98開始接觸電腦的,只是從書籍中得知,那個(gè)時(shí)代網(wǎng)速很慢,撥號使用的是只有幾十Kb(比特不是字節(jié))的貓,56Kb實(shí)際上是這種貓的最高速度,在ADSL出現(xiàn)之后,這種技術(shù)被迅速淘汰。當(dāng)時(shí)記錄文件的也是硬盤,但是在電腦之間拷貝文件的是軟盤,這個(gè)東西我大一還用過,最高容量記得是1.44MB,這還是200X年的軟盤,以前的軟盤容量具體多大就不知道了,Phil Katz上網(wǎng)的時(shí)候還不到1990年,WWW實(shí)際上就沒出現(xiàn),瀏覽器當(dāng)然是沒有的,當(dāng)時(shí)上網(wǎng)干嘛呢?基本就是類似于網(wǎng)管敲各種命令,這樣實(shí)際上也可以聊天、上論壇不是嗎,傳個(gè)文件不壓縮的話肯定死慢死慢的,所以壓縮在那個(gè)時(shí)代很重要。當(dāng)時(shí)有個(gè)商業(yè)公司提供了一種稱為ARC的壓縮軟件,可以讓你在那個(gè)時(shí)代聊天更快,當(dāng)然是要付費(fèi)的,Phil Katz就感覺到不爽,于是寫了一個(gè)PKARC,免費(fèi)的,看名字知道是兼容ARC的,于是網(wǎng)友都用PKARC了,ARC那個(gè)公司自然就不爽,把哥們告上了法庭,說牽涉了知識產(chǎn)權(quán)等等,結(jié)果Phil Katz坐牢了。。。牛人就是牛人, 在牢里面冥思苦想,決定整一個(gè)超越ARC的牛逼算法出來,牢里面就是適合思考,用了兩周就整出來的,稱為PKZIP,不僅免費(fèi),而且這次還開源了,直接公布源代碼,因?yàn)樗惴ǘ疾灰粯恿耍簿筒簧婕暗街R產(chǎn)權(quán)了,于是ZIP流行開來,不過Phil Katz這個(gè)人沒有從里面賺到一分錢,還是窮困潦倒,因?yàn)楹染七^多等眾多原因,2000年的時(shí)候死在一個(gè)汽車旅館里。英雄逝去,精神永存,現(xiàn)在我們用UE打開ZIP文件,我們能看到開頭的兩個(gè)字節(jié)就是PK兩個(gè)字符的ASCII碼。

2、一個(gè)案例的入門思考

好了,Phil Katz在牢里面到底思考了什么?用什么樣的算法來壓縮數(shù)據(jù)呢?我們想一個(gè)簡單的例子:

生,容易?;睿菀?。生活,不容易。

上面這句話假如不壓縮,如果使用Unicode編碼,每個(gè)字會(huì)用2個(gè)字節(jié)表示。為什么是兩個(gè)字節(jié)呢?Unicode是一種國際標(biāo)準(zhǔn),把常見各國的字符,比如英文字符、日文字符、韓文字符、中文字符、拉丁字符等等全部制定了一個(gè)標(biāo)準(zhǔn),顯然,用2個(gè)字節(jié)可以最多表示2^16=65536個(gè)字符,那么65536就夠了嗎?生僻字其實(shí)是很多的,比如光康熙字典里面收錄的漢字就好幾萬,所以實(shí)際上是不夠的,那么是不是擴(kuò)到4個(gè)字節(jié)?也可以,這樣空間倒是變大了,可以收錄更多字符,但一方面擴(kuò)到4個(gè)字節(jié)就一定保證夠嗎?另一方面,4個(gè)字節(jié)是不是太浪費(fèi)空間了,就為了那些一般情況都不會(huì)出現(xiàn)的生僻字?所以,一般情況下,使用2個(gè)字節(jié)表示,當(dāng)出現(xiàn)生僻字的時(shí)候,再使用4個(gè)字節(jié)表示。這實(shí)際上就體現(xiàn)了信息論中數(shù)據(jù)壓縮基本思想,出現(xiàn)頻繁的那些字符,表示得短一些;出現(xiàn)稀少的,可以表示得長些(反正一般情況下也不會(huì)出現(xiàn)),這樣整體長度就會(huì)減小。除了Unicode,ASCII編碼是針對英文字符的編碼方案,用1個(gè)字節(jié)即可,除了這兩種編碼方案,還有很多地區(qū)性的編碼方案,比如GB2312可以對中文簡體字進(jìn)行編碼,Big5可以對中文繁體字進(jìn)行編碼。兩個(gè)文件如果都使用一種編碼方案,那是沒有問題的,不過考慮到國際化,還是盡量使用Unicode這種國際標(biāo)準(zhǔn)吧。不過這個(gè)跟ZIP沒啥關(guān)系,純屬背景介紹。

好了,回到我們前面說的例子,一共有17個(gè)字符(包括標(biāo)點(diǎn)符號),如果用普通Unicode表示,一共是17*2=34字節(jié)??刹豢梢詨嚎s呢?所有人一眼都可以看出里面出現(xiàn)了很多重復(fù)的字符,比如里面出現(xiàn)了好多次容易(實(shí)際上是容易加句號三個(gè)字符)這個(gè)詞,第一次出現(xiàn)的時(shí)候用普通的Unicode,第二次出現(xiàn)的“容易。”則可以用(距離、長度)表示,距離的意思是接下來的字符離前面重復(fù)的字符隔了幾個(gè),長度則表示有幾個(gè)重復(fù)字符,上面的例子的第二個(gè)“容易。”就表示為(5,3),就是距離為5個(gè)字符,長度是3,在解壓縮的時(shí)候,解到這個(gè)地方的時(shí)候,往前跳5個(gè)字符,把這個(gè)位置的連續(xù)3個(gè)字符拷貝過來就完成了解壓縮,這實(shí)際上不就是指針的概念?沒有錯(cuò),跟指針很類似,不過在數(shù)據(jù)壓縮領(lǐng)域,一般稱為字典編碼,為什么叫字典呢,當(dāng)我們?nèi)ゲ橐粋€(gè)字的時(shí)候,我們總是先去目錄查找這個(gè)字在哪一頁,再翻到那一頁去看,指針不也是這樣,指針不就是內(nèi)存的地址,要對一個(gè)內(nèi)存進(jìn)行操作,我們先拿到指針,然后去那塊內(nèi)存去操作。所謂的指針、字典、索引、目錄等等術(shù)語,不同的背景可能稱呼不同,但我們要理解他們的本質(zhì)。如果使用(5,3)這種表示方法,原來需要用6個(gè)字節(jié)表示,現(xiàn)在只需要記錄5和3即可。那么,5和3怎么記錄呢?一種方法自然還是可以用Unicode,那么就相當(dāng)于節(jié)省了2個(gè)字節(jié),但是有兩個(gè)問題,第一個(gè)問題是解壓縮的時(shí)候怎么知道是正常的5和3這兩個(gè)字符,還是這只是一個(gè)特殊標(biāo)記呢?所以前面還得加一個(gè)標(biāo)志來區(qū)分一下,到底接下來的Unicode碼是指普通字符,還是指距離和長度,如果是普通Unicode,則直接查Unicode碼表,如果是距離和長度,則往前面移動(dòng)一段距離,拷貝即可。第二個(gè)問題,還是壓縮程度不行,這么一弄,感覺壓縮不了多少,如果重復(fù)字符比較長那倒是比較劃算,因?yàn)榉凑熬嚯x+長度”就夠了,但比如這個(gè)例子,如果5和3前面加一個(gè)特殊字節(jié),豈不是又是3個(gè)字節(jié),那還不如不壓縮。咋辦呢?能不能對(5,3)這種整數(shù)進(jìn)行再次壓縮?這里就利用了我們前面說的一個(gè)基本原則:出現(xiàn)的少的整數(shù)多編一些比特,出現(xiàn)的多的整數(shù)少編一些比特。那么,比如3、4、5、6、7、8、9這些距離誰出現(xiàn)得多?誰出現(xiàn)的少呢?誰知道?

壓縮之前當(dāng)然不知道,不過掃描一遍不就知道了?比如,后面那個(gè)重復(fù)的字符串“容易?!卑凑涨懊娴囊?guī)則可以表示為(7,3),即離前面重復(fù)的字符串距離為7,長度為3。(7,3)指著前面跟自己一樣那個(gè)字符串。那么,為什么不指著第一個(gè)“容易?!币钢诙€(gè)“容易。”呢?如果指著第一個(gè),那就不是(7,3)了,就是(12,3)了。當(dāng)然,表示為(12,3)也可以解壓縮,但是有一個(gè)問題,就是12這個(gè)值比7大,大了又怎么了?我們在生活中會(huì)發(fā)現(xiàn)一些普遍規(guī)律,重復(fù)現(xiàn)象往往具有局部性。比如,你跟一個(gè)人說話,你說了一句話以后,往往很快會(huì)重復(fù)一遍,但是你不會(huì)隔了5個(gè)小時(shí)又重復(fù)這句話,這種特點(diǎn)在文件里面也存在著,到處都是這種例子,比如你在編程的時(shí)候,你定義了一個(gè)變量int nCount,這個(gè)nCount一般你很快就會(huì)用到,不會(huì)離得很遠(yuǎn)。我們前面所說的距離代表了你隔了多久再說這句話,這個(gè)距離一般不大,既然如此,應(yīng)該以離當(dāng)前字符串距離最近的那個(gè)作為記錄的依據(jù)(也就是指向離自己最近那個(gè)重復(fù)字符串),這樣的話,所有的標(biāo)記都是一些短距離,比如都是3、4、5、6、7而不會(huì)是3、5、78、965等等,如果大多數(shù)都是一些短距離,那么這些短距離就可以用短一些的比特表示,長一些的距離不太常見,則用一些長一些的比特表示。這樣, 總體的表示長度就會(huì)減少。好了,我們前面得到了(5,3)、(7、3)這種記錄重復(fù)的表示,距離有兩種:5、7;長度只有1種:3。咋編碼?越短越好。

既然表示的比特越短越好,3表示為0、5表示為10、7表示為11,行不行?這樣(5,3),(7,3)就只需要表示為100、110,這樣豈不是很短?貌似可以,貌似很高效。

但解壓縮遇到10這兩個(gè)比特的時(shí)候,怎么知道10表示5呢?這種表示方法是一個(gè)映射表,稱為碼表。我們設(shè)計(jì)的上面這個(gè)例子的碼表如下:

3–>0

5–>10

7–>11

這個(gè)碼表也得傳過去或者記錄在壓縮文件里才行啊,否則無法解壓縮,但會(huì)不會(huì)記錄了碼表以后整體空間又變大了,會(huì)不會(huì)起不到壓縮的作用?而且一個(gè)碼表怎么記錄?碼表記錄下來也是一堆數(shù)據(jù),是不是也需要編碼?碼表是否可以繼續(xù)壓縮?那豈不是又需要新的碼表?壓縮會(huì)不會(huì)是一個(gè)永無止境的過程?作為一個(gè)入門級的同學(xué),大概想到這兒就不容易想下去了。

3、ZIP中的LZ編碼思想

上面我們說的重復(fù)字符串用指針標(biāo)記記錄下來,這種方法就是LZ這兩個(gè)人提出來的,理解起來比較簡單。后面分析(5,3)這種指針標(biāo)記應(yīng)該怎么編碼的時(shí)候,就涉及到一種非常廣泛的編碼方式,Huffman編碼,Huffman大致和香農(nóng)是一個(gè)時(shí)代的人,這種編碼方式是他在MIT讀書的時(shí)候提出來的。接下來,我們來看看ZIP是怎么做的。

以上面的例子,一個(gè)很簡單的示意圖如下:

可以看出,ZIP中使用的LZ77算法和前面分析的類似,當(dāng)然,如果仔細(xì)對比的話,ZIP中使用的算法和LZ提出來的LZ77算法其實(shí)還是有差異的,不過我建議不用仔細(xì)去扣里面的差異,思想基本是相同的,我們后面會(huì)簡要分析一下兩者的差異。LZ77算法一般稱為“滑動(dòng)窗口壓縮”,我們前面說過,該算法的核心是在前面的歷史數(shù)據(jù)中尋找重復(fù)字符串,但如果要壓縮的文件有100MB,是不是從文件頭開始找?不是,這里就涉及前面提過的一個(gè)規(guī)律,重復(fù)現(xiàn)象是具有局部性的,它的基本假設(shè)是,如果一個(gè)字符串要重復(fù),那么也是在附近重復(fù),遠(yuǎn)的地方就不用找了,因此設(shè)置了一個(gè)滑動(dòng)窗口,ZIP中設(shè)置的滑動(dòng)窗口是32KB,那么就是往前面32KB的數(shù)據(jù)中去找,這個(gè)32KB隨著編碼不斷進(jìn)行而往前滑動(dòng)。當(dāng)然,理論上講,把滑動(dòng)窗口設(shè)置得很大,那樣就有更大的概率找到重復(fù)的字符串,壓縮率不就更高了?初看起來如此,找的范圍越大,重復(fù)概率越大,不過仔細(xì)想想,可能會(huì)有問題,一方面,找的范圍越大,計(jì)算量會(huì)增大,不顧一切地增大滑動(dòng)窗口,甚至不設(shè)置滑動(dòng)窗口,那樣的軟件可能不可用,你想想,現(xiàn)在這種方式,我們在壓縮一個(gè)大文件的時(shí)候,速度都已經(jīng)很慢了,如果增大滑動(dòng)窗口,速度就更慢,從工程實(shí)現(xiàn)角度來說,設(shè)置滑動(dòng)窗口是必須的;另一方面,找的范圍越大,距離越遠(yuǎn),出現(xiàn)的距離很多,也不利于對距離進(jìn)行進(jìn)一步壓縮吧,我們前面說過,距離和長度最好出現(xiàn)的值越少越好,那樣更好壓縮,如果出現(xiàn)的很多,如何記錄距離和長度可能也存在問題。不過,我相信滑動(dòng)窗口設(shè)置得越大,最終的結(jié)果應(yīng)該越好一些,不過應(yīng)該不會(huì)起到特別大的作用,比如壓縮率提高了5%,但計(jì)算量增加了10倍,這顯然有些得不償失。

在第一個(gè)圖中,“容易?!笔且粋€(gè)重復(fù)字符串,距離distance=5,字符串長度length=3。當(dāng)對這三個(gè)字符壓縮完畢后,接下來滑動(dòng)窗口向前移動(dòng)3個(gè)字符,要壓縮的是“我…”這個(gè)字符串,但這個(gè)串在滑動(dòng)窗口內(nèi)沒找到,所以無法使用distance+length的方式記錄。這種結(jié)果稱為literal。literal的中文含義是原義的意思,表示沒有使用distance+length的方式記錄的那些普通字符。literal是不是就用原始的編碼方式,比如Unicode方式表示?ZIP里不是這么做的,ZIP把literal認(rèn)為也是一個(gè)數(shù),盡管不能用distance+length表示,但不代表不可以繼續(xù)壓縮。另外,如果“我”出現(xiàn)在了滑動(dòng)窗口內(nèi),是不是就可以用distance+length的方式表示?也不是,因?yàn)橐粋€(gè)字出現(xiàn)重復(fù),不值得用這種方式表示,兩個(gè)字呢?distance+length就是兩個(gè)整數(shù),看起來也不一定值得,ZIP中確實(shí)認(rèn)為2個(gè)字節(jié)如果在滑動(dòng)窗口內(nèi)找到重復(fù),也不管,只有3個(gè)字節(jié)以上的重復(fù)字符串,才會(huì)用distance+length表示,重復(fù)字符串的長度越長越好,因?yàn)椴还芏嚅L,都用distance+length表示就行了。

這樣的話,一段字符串最終就可以表示成literal、distance+length這兩種形式了。LZ系列算法的作用到此為止,下面,Phil Katz考慮使用Huffman對上面的這些LZ壓縮后的結(jié)果進(jìn)行二次壓縮。個(gè)人認(rèn)為接下來的過程才是ZIP的核心,所以我們要熟悉一下Huffman編碼。

4、ZIP中的Huffman編碼思想

上面LZ壓縮結(jié)果有三類(literal、distance、length),我們拿出distance一類來舉例。distance代表重復(fù)字符串離前一個(gè)一模一樣的字符串之間的距離,是一個(gè)大于0的整數(shù)。如何對一個(gè)整數(shù)進(jìn)行編碼呢?一種方法是直接用固定長度表示,比如采用計(jì)算機(jī)里面描述一個(gè)4字節(jié)整數(shù)那樣去記錄,這也是可以的,主要問題當(dāng)然是浪費(fèi)存儲(chǔ)空間,在ZIP中,distance這個(gè)數(shù)表示的是重復(fù)字符串之間的距離,顯然,一般而言,越小的距離,出現(xiàn)的數(shù)量可能越多,而越大的距離,出現(xiàn)的數(shù)量可能越少,那么,按照我們前面所說的原則,小的值就用較少比特表示,大的值就用較多比特表示,在我們這個(gè)場景里,distance當(dāng)然也不會(huì)無限大,比如不會(huì)超過滑動(dòng)窗口的最大長度,假如對一個(gè)文件進(jìn)行LZ壓縮后,得到的distance值為:

3、6、4、3、4、3、4、3、5

這個(gè)例子里,3出現(xiàn)了4次,4出現(xiàn)了3次,5出現(xiàn)了1次,6出現(xiàn)了1次。當(dāng)然,不同的文件得到的結(jié)果不同,這里只是舉一個(gè)典型的例子,因?yàn)橹挥?種值,所以我們沒有必要對其它整數(shù)編碼。只需要對這4個(gè)整數(shù)進(jìn)行編碼即可。

那么,怎么設(shè)計(jì)一個(gè)算法,符合3的編碼長度最短?6的編碼長度最長這種直觀上可行的原則(我們并沒有說這是理論上最優(yōu)的方式)呢?

看起來似乎很難想出來。我們先來簡化一下,用固定長度表示。這里有4個(gè)整數(shù),只要使用2個(gè)比特表示即可。于是這樣表示就很簡單:

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

00、01這種編碼結(jié)果稱為碼字,碼字的平均長度是2。上面這個(gè)對應(yīng)關(guān)系即為碼表,在壓縮時(shí),需要將碼表放在最前面,后面的數(shù)字就用碼字表示,解碼時(shí),先把碼表記錄在內(nèi)存里,比如用一個(gè)哈希表記錄下來,解壓縮時(shí),遇到00,就解碼為3等等。

因?yàn)槌霈F(xiàn)了9個(gè)數(shù),所以全部碼字總長度為18個(gè)比特。(我們暫時(shí)不考慮記錄碼表到底要占多少空間)

想要編碼結(jié)果更短,因?yàn)?出現(xiàn)的最多,所以考慮把3的碼字縮短點(diǎn),比如3是不是可以用1個(gè)比特表示,這樣才算縮短吧,因?yàn)?和1只是二進(jìn)制的一個(gè)標(biāo)志,所以用0還是1沒有本質(zhì)區(qū)別,那么,我們暫定把3用比特0表示。那么,4、5、6還能用0開頭的碼字表示呢?

這樣會(huì)存在問題,因?yàn)?、5、6的編碼結(jié)果如果以0開頭,那么,在解壓縮的時(shí)候,遇到比特0,就不知道是表示3還是表示4、5、6了,就無法解碼,當(dāng)然,似乎理論上也不是不可以,比如可以往后解解看,比如假定0表示3的條件下往后解,如果無效則說明這個(gè)假設(shè)不對,但這種方式很容易出現(xiàn)兩個(gè)字符串的編碼結(jié)果是一樣的,這個(gè)誰來保證?所以,4、5、6都得以1開頭才行,那么,按照這個(gè)原則,4用1個(gè)比特也不行,因?yàn)?、6要么以0開頭,要么以1開頭,就無法編碼了,所以我們將4的碼字增加至2個(gè)比特,比如10,于是我們得到了部分碼表:

0–>3;10–>4。

按照這個(gè)道理,5、6既不能以0開頭,也不能以10開頭了,因?yàn)橥瑯哟嬖跓o法解碼的問題,所以5應(yīng)該以11開頭,就定為11行不行呢?也不行,因?yàn)?就不知道怎么編碼了,6也不能以0開頭,也不能以10、11開頭,那就無法表示了,所以,迫不得已,我們必須把5擴(kuò)展一位,比如110,那么,6顯然就可以用111表示了,反正也沒有其他數(shù)了。于是我們得到了最終的碼表:

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

看起來,編碼結(jié)果只能是這樣了,我們來算一下,碼字的總長度減少了沒有,原來的9個(gè)數(shù)是3、6、4、3、4、3、4、3、5,分別對應(yīng)的碼字是:

0、111、10、0、10、0、10、0、110

算一下,總共16個(gè)比特,果然比前面那種方式變短了。我們在前面的設(shè)計(jì)過程中,是按照這些值出現(xiàn)次數(shù)由高到底的順序去找碼字的,比如先確定3,再確定4、5、6等等。按照一個(gè)碼字不能是另一個(gè)碼字的前綴這一規(guī)則,逐步獲得所有的碼字。這種編碼規(guī)則有一個(gè)專用術(shù)語,稱為前綴碼。Huffman編碼基本上就是這么做的,把出現(xiàn)頻率排個(gè)序,然后逐個(gè)去找,這個(gè)逐個(gè)去找的過程,就引入了二叉樹。不過Huffman的算法一般是從頻率由低到高排序,從樹的下面依次往上合并,不過本質(zhì)上沒區(qū)別,理解思想即可。上面的結(jié)果可以用一顆二叉樹表示為下圖:

這棵樹也稱為碼樹,其實(shí)就是碼表的一種形式化描述,每個(gè)節(jié)點(diǎn)(除了葉子節(jié)點(diǎn))都會(huì)分出兩個(gè)分支,左分支代表比特0,右邊分支代表1,從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)的一個(gè)比特序列就是碼字。因?yàn)橹挥腥~子節(jié)點(diǎn)可以是碼字,所以這樣也符合一個(gè)碼字不能是另一個(gè)碼字的前綴這一原則。說到這里,可以說一下另一個(gè)話題,就是一個(gè)映射表map在內(nèi)存中怎么存儲(chǔ),沒有相關(guān)經(jīng)驗(yàn)的可以跳過,map實(shí)現(xiàn)的是key–>value這樣的一個(gè)表,map的存儲(chǔ)一般有哈希表和樹形存儲(chǔ)兩類,樹形存儲(chǔ)就可以采用上面這棵樹,樹的中間節(jié)點(diǎn)并沒有什么含義,葉子節(jié)點(diǎn)的值表示value,從根節(jié)點(diǎn)到葉子節(jié)點(diǎn)上分支的值就是key,這樣比較適合存儲(chǔ)那些key由多個(gè)不等長字符組成的場合,比如key如果是字符串,那么把二叉樹的分支擴(kuò)展很多,成為多叉樹,每個(gè)分支就是a,b,c,d這種字符,這棵樹也就是Trie樹,是一種很好使的數(shù)據(jù)結(jié)構(gòu)。利用樹的遍歷算法,就實(shí)現(xiàn)了一個(gè)有序Map。

好了,我們理解了Huffman編碼的思想,我們來看看distance的實(shí)際情況。ZIP中滑動(dòng)窗口大小固定為32KB,也就是說,distance的值范圍是1-32768。那么,通過上面的方式,統(tǒng)計(jì)頻率后,就得到32768個(gè)碼字,按照上面這種方式可以構(gòu)建出來。于是我們會(huì)遇到一個(gè)最大的問題,那就是這棵樹太大了,怎么記錄呢?

好了,個(gè)人認(rèn)為到了ZIP的核心了,那就是碼樹應(yīng)該怎么縮小,以及碼樹怎么記錄的問題。

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

分析上面的例子,看看這個(gè)碼表:

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

我們之前提過,0和1就是二進(jìn)制的一個(gè)標(biāo)志,互換一下其實(shí)根本不影響編碼長度,所以,下面的碼表其實(shí)是一樣的:

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

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

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

。。。。。

這些都可以,而且編碼長度完全一樣,只是碼字不同而已。

對比一下第一個(gè)和第二個(gè)例子,對應(yīng)的碼樹是這個(gè)樣子:

也就是說,我們把碼樹的任意節(jié)點(diǎn)的左右分支旋轉(zhuǎn)(0、1互換),也可以稱為樹的左右互換,其實(shí)不影響編碼長度,也就是說,這些碼表其實(shí)都是一樣好的,使用哪個(gè)都可以。

這個(gè)規(guī)律暗示了什么信息呢?暗示了碼表可以怎么記錄呢?Phil Katz當(dāng)年在牢里也深深地思考了這一問題。

為了體會(huì)Phil Katz當(dāng)時(shí)的心情,我們有必要盯著這兩棵樹思考幾分鐘:怎么把一顆樹用最少的比特記錄下來?

Phil Katz當(dāng)時(shí)思考的邏輯我猜是這樣的,既然這些樹的壓縮程度都一樣,那干脆使用最特殊的那棵樹,反正壓縮程度都一樣,只要ZIP規(guī)定了這棵樹的特殊性,那么我記錄的信息就可以最少,這種特殊化的思想在后面還會(huì)看到。不同的樹當(dāng)然有不同的特點(diǎn),比如數(shù)據(jù)結(jié)構(gòu)里面常見的平衡樹也是一類特殊的樹,他選的樹就是左邊那棵,這棵樹有一個(gè)特點(diǎn),越靠左邊越淺,越往右邊越深,是這些樹中最不平衡的樹。ZIP里的壓縮算法稱為Deflate算法,這棵樹也稱為Deflate樹,對應(yīng)的解壓縮算法稱為Inflate,Deflate的大致意思是把輪胎放氣了,意為壓縮;Inflate是給輪胎打氣的意思,意為解壓。那么,Deflate樹的特殊性又帶來什么了?

揭曉答案吧,Phil Katz認(rèn)為換來換去只有碼字長度不變,如果規(guī)定了一類特殊的樹,那么就只需要記錄碼字長度即可。比如,一個(gè)有效的碼表是0–>3;10–>4;110–>5;111–>6。但只需要記錄這個(gè)對應(yīng)關(guān)系即可:

3  4  5  6

1  2  3  3

也就是說,把1、2、3、3記錄下來,解壓一邊照著左邊那棵樹的形狀構(gòu)造一顆樹,然后只需要1、2、3、3這個(gè)信息自然就知道是0、10、110、111。這就是Phil Katz想出來的ZIP最核心的一點(diǎn):這棵碼樹用碼字長度序列記錄下來即可。

當(dāng)然,只把1、2、3、3這個(gè)序列記錄下來還不行,比如不知道111對應(yīng)5還是對應(yīng)6?

所以,構(gòu)造出樹來只是知道了有哪些碼字了,但是這些碼字到底對應(yīng)哪些整數(shù)還是不知道。

Phil Katz于是又出現(xiàn)了一個(gè)想法:記錄1、2、3、3還是記錄1、3、2、3,或者3、3、2、1,其實(shí)都能構(gòu)造出這棵樹來,那么,為什么不按照一個(gè)特殊的順序記錄呢?這個(gè)順序就是整數(shù)的大小順序,比如上面的3、4、5、6是整數(shù)大小順序排列的,那么,記錄的順序就是1、2、3、3。而不是2、3、3、1。

好了,根據(jù)1、2、3、3這個(gè)信息構(gòu)造出了碼字,這些碼字對應(yīng)的整數(shù)一個(gè)比一個(gè)大,假如我們知道編碼前的整數(shù)就是3、4、5、6這四個(gè)數(shù),那就能對應(yīng)起來了,不過到底是哪四個(gè)還是不知道???這個(gè)整數(shù)可以表示距離啊,距離不知道怎么去解碼LZ?

Phil Katz又想了,既然distance的范圍是1-32768,那么就按照這個(gè)順序記錄。上面的例子1和2沒有,那就記錄長度0。所以記錄下來的碼字長度序列為:

0、0、1、2、3、3、0、0、0、0、0、。。。。。。。。。。。。

這樣就知道構(gòu)造出來的碼字對應(yīng)哪個(gè)整數(shù)了吧,但因?yàn)閐istance可能的值很多(32768個(gè)),但實(shí)際出現(xiàn)的往往不多,中間會(huì)出現(xiàn)很多0(也就是根本就沒出現(xiàn)這個(gè)距離),不過這個(gè)問題倒是可以對連續(xù)的0做個(gè)特殊標(biāo)記,這樣是不是就行了呢?還有什么問題?

我們還是要站在時(shí)代的高度來看待這個(gè)問題。我們明白,每個(gè)distance肯定對應(yīng)唯一一個(gè)碼字,使用Huffman編碼可以得到所有碼字,但是因?yàn)閐istance可能非常多,雖然一般不會(huì)有32768這么多,但對一個(gè)大些的文件進(jìn)行LZ編碼,distance上千還是很正常的,所以這棵樹很大,計(jì)算量、消耗的內(nèi)存都容易超越了那個(gè)時(shí)代的硬件條件,那么怎么辦呢?這里再次體現(xiàn)了Phil Katz對Huffman編碼掌握的深度,他把distance劃分成多個(gè)區(qū)間,每個(gè)區(qū)間當(dāng)做一個(gè)整數(shù)來看,這個(gè)整數(shù)稱為Distance Code。當(dāng)一個(gè)distance落到某個(gè)區(qū)間,則相當(dāng)于是出現(xiàn)了那個(gè)Code,多個(gè)distance對應(yīng)于一個(gè)Distance Code,Distance雖然很多,但Distance Code可以劃分得很少,只要我們對Code進(jìn)行Huffman編碼,得到Code的編碼后,Distance Code再根據(jù)一定規(guī)則擴(kuò)展出來。那么,劃分多少個(gè)區(qū)間?怎么劃分區(qū)間呢?我們分析過,越小的距離,出現(xiàn)的越多;越大的距離,出現(xiàn)的越少,所以這種區(qū)間劃分不是等間隔的,而是越來越稀疏的,類似于下面的劃分:

1、2、3、4這四個(gè)特殊distance不劃分,或者說1個(gè)Distance就是1個(gè)區(qū)間;5,6作為一個(gè)區(qū)間;7、8作為一個(gè)區(qū)間等等,基本上,區(qū)間的大小都是1、2、4、8、16、32這么遞增的,越往后,涵蓋的距離越多。為什么這么分呢?首先自然是距離越小出現(xiàn)頻率越高,所以距離值小的時(shí)候,劃分密一些,這樣相當(dāng)于一個(gè)放大鏡,可以對小的距離進(jìn)行更精細(xì)地編碼,使得其編碼長度與其出現(xiàn)次數(shù)盡量匹配;對于距離較大那些,因?yàn)槌霈F(xiàn)頻率低,所以可以適當(dāng)放寬一些。另一個(gè)原因是,只要知道這個(gè)區(qū)間Code的碼字,那么對于這個(gè)區(qū)間里面的所有distance,后面追加相應(yīng)的多個(gè)比特即可,比如,17-24這個(gè)區(qū)間的Huffman碼字是110,因?yàn)?7-24這個(gè)區(qū)間有8個(gè)整數(shù),于是按照下面的規(guī)則即可獲得其distance對應(yīng)的碼字:

17–>110 000

18–>110 001

19–>110 010

20–>110 011

21–>110 100

22–>110 101

23–>110 110

24–>110 111

這樣計(jì)算復(fù)雜度和內(nèi)存消耗是不是很小了,因?yàn)樾枰M(jìn)行Huffman編碼的整數(shù)一下字變少了,這棵樹不會(huì)多大,計(jì)算起來時(shí)間和空間復(fù)雜度降低,擴(kuò)展起來也比較簡單。當(dāng)然,從理論上來說,這樣的編碼方式實(shí)際上將編碼過程分為了兩級,并不是理論上最優(yōu)的,把所有distance當(dāng)作一個(gè)大空間去編碼才可能得到最優(yōu)結(jié)果,不過還是那句話,工程實(shí)現(xiàn)的限制,在壓縮軟件實(shí)現(xiàn)上,我們不能用壓縮率作為衡量一個(gè)算法優(yōu)劣的唯一指標(biāo),其實(shí)耗費(fèi)的時(shí)間和空間同樣是指標(biāo),所以需要看綜合指標(biāo)。很多其他軟件也一樣,擴(kuò)展性、時(shí)間空間復(fù)雜度、穩(wěn)定性、移植性、維護(hù)的方便性等等是工程上很重要的東西。我沒有看過RAR是如何壓縮的,有可能是在類似的地方進(jìn)行了改進(jìn),如果如此,那也是站在巨人的肩膀上,而且硬件條件不同,進(jìn)行一些改進(jìn)也并不奇怪。

具體來說,Phil Katz把distance劃分為30個(gè)區(qū)間,如下圖:

這個(gè)圖是我從David Salomon的《Data Compression The Complete Reference》這本書(第四版)中拷貝出來的,下面的有些圖也是,如果需要對數(shù)據(jù)壓縮進(jìn)行全面的了解,這本書幾乎是最全的了,強(qiáng)烈推薦。

當(dāng)然,你要問為什么是30個(gè)區(qū)間,我也沒分析過,也許是復(fù)雜度和壓縮率經(jīng)過試驗(yàn)之后的一種折中吧。

其中,左邊的Code表示區(qū)間的編號,是0-29,共30個(gè)區(qū)間,這只是個(gè)編號,沒有特別的含義,但Huffman就是對0-29這30個(gè)Code進(jìn)行編碼的,得到區(qū)間的碼字;

bits表示distance的碼字需要在Code的碼字基礎(chǔ)上擴(kuò)展幾位,比如0就表示不擴(kuò)展,最大的13表示要擴(kuò)展13位,因此,最大的區(qū)間包含的distance數(shù)量為8192個(gè)。

Distance一列則表示這個(gè)區(qū)間涵蓋的distance范圍。

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

6、ZIP中l(wèi)iteral和length的壓縮方式

說完了distance,LZ編碼結(jié)果還有兩類:literal和length。這兩類也利用了類似于distance的方式進(jìn)行壓縮。

前面分析過,literal表示未匹配的字符,我們前面之所以拿漢字來舉例,完全是為了便于理解,ZIP之所以是通用壓縮,它實(shí)際上是針對字節(jié)作為基本字符來編碼的,所以一個(gè)literal至多有256種可能。

length表示重復(fù)字符串長度,length=1當(dāng)然不會(huì)出現(xiàn),因?yàn)橐粋€(gè)字符不值得用distance+length去記錄,重復(fù)字符串當(dāng)然越長越好,Phil Katz(下面還是簡稱PK了,拷貝太麻煩)認(rèn)為,length=2也不值得用這種方式記錄,還是太短了,所以PK把length最小值認(rèn)為是3,必須3個(gè)以上字符的字符串出現(xiàn)重復(fù)才用distance+length記錄。那么,最大的length是多少呢?理論上當(dāng)然可以很長很長,比如一個(gè)文件就是連續(xù)的0,這個(gè)重復(fù)字符串長度其實(shí)接近于這個(gè)文件的實(shí)際長度。但是PK把length的范圍做了限制,限定length的個(gè)數(shù)跟literal一樣,也只有256個(gè),因?yàn)镻K認(rèn)為,一個(gè)重復(fù)字符串達(dá)到了256個(gè)已經(jīng)很長了,概率非常小;另外,其實(shí)哪怕超過了256,我還是認(rèn)為是一段256再加上另外一段,增加一個(gè)distance+length就行了嘛,并不影響結(jié)果。而且這樣做,我想同樣也考慮了硬件條件吧。

初看有點(diǎn)奇怪的在于,將literal和length二者合二為一,什么意思呢?就是對這兩種整數(shù)(literal本質(zhì)上是一個(gè)字節(jié))共用一個(gè)Huffman碼表,一會(huì)兒解釋為什么。PK對Huffman的理解我覺得達(dá)到了爐火純青的地步,前面已經(jīng)看到,后面還會(huì)看到。他認(rèn)為Huffman編碼的輸入反正說白了就是一個(gè)集合的元素就行,無論這個(gè)元素是啥,所以多個(gè)集合看做一個(gè)集合當(dāng)作Huffman編碼的輸入沒啥問題。literal用整數(shù)0-255表示,256是一個(gè)結(jié)束標(biāo)志,解碼以后結(jié)果是256表示解碼結(jié)束;從257開始表示length,所以257這個(gè)數(shù)表示length=3,258這個(gè)數(shù)表示length=4等等,但PK也不是一直這么一一對應(yīng),和distance一樣,也是把length(總共256個(gè)值)劃分為29個(gè)區(qū)間,其結(jié)果如下圖:

其中的含義和distance類似,不再贅述,所以literal/length這個(gè)Huffman編碼的輸入元素一共285個(gè),其中256表示解碼結(jié)束標(biāo)志。為什么要把二者合二為一呢?因?yàn)楫?dāng)解碼器接收到一個(gè)比特流的時(shí)候,首先可以按照literal/length這個(gè)碼表來解碼,如果解出來是0-255,就表示未匹配字符,如果是256,那自然就結(jié)束,如果是257-285之間,則表示length,把后面擴(kuò)展比特加上形成length后,后面的比特流肯定就表示distance,因此,實(shí)際上通過一個(gè)Huffman碼表,對各類情況進(jìn)行了統(tǒng)一,而不是通過加一個(gè)什么標(biāo)志來區(qū)分到底是literal還是重復(fù)字符串。

好了,理解了上面的過程,就理解了ZIP壓縮的第二步,第一步是LZ編碼,第二步是對LZ編碼后結(jié)果(literal、distance、length)進(jìn)行的再編碼,因?yàn)閘iteral/length是一個(gè)碼表,我稱其為Huffman碼表1,distance那個(gè)碼表稱為Huffman碼表2。前面我們已經(jīng)分析了,Huffman碼樹用一個(gè)碼字長度序列表示,稱為CL(Code Length),記錄兩個(gè)碼表的碼字長度序列分別記為CL1、CL2。碼樹記錄下來,對literal/length的編碼比特流稱為LIT比特流;對distance的編碼比特流稱為DIST比特流。

按照上面的方法,LZ的編碼結(jié)果就變成四塊:CL1、CL2、LIT比特流、DIST比特流。CL1、CL2是碼字長度的序列,這個(gè)序列說白了就是一堆正整數(shù),因此,PK繼續(xù)深挖,認(rèn)為這個(gè)序列還應(yīng)該繼續(xù)壓縮,也就是說,對碼表進(jìn)行壓縮。

7、ZIP中對CL進(jìn)行再次壓縮的方法

這里仍然沿用Huffman的想法,因?yàn)镃L也是一堆整數(shù),那么當(dāng)然可以再次應(yīng)用Huffman編碼。不過在這之前,PK對CL序列進(jìn)行了一點(diǎn)處理。這個(gè)處理也是很精巧的。

CL序列表示一系列整數(shù)對應(yīng)的碼字長度,對于literal/length來說,總共有0-285這么多符號,所以這個(gè)序列長度為286,每個(gè)符號都有一個(gè)碼字長度,當(dāng)然,這里面可能會(huì)出現(xiàn)大段連續(xù)的0,因?yàn)槟承┳址蜷L度不存在,尤其是對英文文本編碼的時(shí)候,非ASCII字符就根本不會(huì)出現(xiàn),length較大的值出現(xiàn)概率也很小,所以出現(xiàn)大段的0是很正常的;對于distance也類似,也可能出現(xiàn)大段的0。PK于是先進(jìn)行了一下游程編碼。在說什么是游程編碼之前,我們談?wù)凱K對CL序列的認(rèn)識。

literal/length的編碼符號總共286個(gè)(回憶:256個(gè)Literal+1個(gè)結(jié)束標(biāo)志+29個(gè)length區(qū)間),distance的編碼符號總共30個(gè)(回憶:30個(gè)區(qū)間),所以這顆碼樹不會(huì)特別深,Huffman編碼后的碼字長度不會(huì)特別長,PK認(rèn)為最長不會(huì)超過15,也就是樹的深度不會(huì)超過15,這個(gè)是否是理論證明我還沒有分析,有興趣的同學(xué)可以分析一下。因此,CL1和CL2這兩個(gè)序列的任意整數(shù)值的范圍是0-15。0表示某個(gè)整數(shù)沒有出現(xiàn)(比如literal=0×12, length Code=8, distance Code=15等等)。

什么叫游程呢?就是一段完全相同的數(shù)的序列。什么叫游程編碼呢?說起來原理更簡單,就是對一段連續(xù)相同的數(shù),記錄這個(gè)數(shù)一次,緊接著記錄出現(xiàn)了多少個(gè)即可。David的書中舉了這個(gè)例子,比如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
那么,游程編碼的結(jié)果為:

4, 16, 01(二進(jìn)制), 3, 3, 3, 6, 16, 11(二進(jìn)制), 16, 00(二進(jìn)制), 17,011(二進(jìn)制), 2, 16, 00(二進(jìn)制)
這是什么意思呢?因?yàn)镃L的范圍是0-15,PK認(rèn)為重復(fù)出現(xiàn)2次太短就不用游程編碼了,所以游程長度從3開始。用16這個(gè)特殊的數(shù)表示重復(fù)出現(xiàn)3、4、5、6個(gè)這樣一個(gè)游程,分別后面跟著00、01、10、11表示(實(shí)際存儲(chǔ)的時(shí)候需要低比特優(yōu)先存儲(chǔ),需要把比特倒序來存,博文的一些例子有時(shí)候會(huì)忽略這點(diǎn),實(shí)際寫程序的時(shí)候一定要注意,否則會(huì)得到錯(cuò)誤結(jié)果)。于是4,4,4,4,4,這段游程記錄為4,16,01,也就是說,4這個(gè)數(shù),后面還會(huì)連續(xù)出現(xiàn)了4次。6,16,11,16,00表示6后面還連續(xù)跟著6個(gè)6,再跟著3個(gè)6;因?yàn)檫B續(xù)的0出現(xiàn)的可能很多,所以用17、18這兩個(gè)特殊的數(shù)專門表示0游程,17后面跟著3個(gè)比特分別記錄長度為3-10(總共8種可能)的游程;18后面跟著7個(gè)比特表示11-138(總共128種可能)的游程。17,011(二進(jìn)制)表示連續(xù)出現(xiàn)6個(gè)0;18,0111110(二進(jìn)制)表示連續(xù)出現(xiàn)62個(gè)0??傊涀。?-15是CL可能出現(xiàn)的值,16表示除了0以外的其它游程;17、18表示0游程。因?yàn)槎M(jìn)制實(shí)際上也是個(gè)整數(shù),所以上面的序列用整數(shù)表示為:

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

我們又看到了一串整數(shù),這串整數(shù)的值的范圍是0-18。這個(gè)序列稱為SQ(Sequence的意思)。因?yàn)橛袃蓚€(gè)CL1、CL2,所以對應(yīng)的有兩個(gè)SQ1、SQ2。

針對SQ1、SQ2,PK用了第三個(gè)Huffman碼表來對這兩個(gè)序列進(jìn)行編碼。通過統(tǒng)計(jì)各個(gè)整數(shù)(0-18范圍內(nèi))的出現(xiàn)次數(shù),按照相同的思路,對SQ1和SQ2進(jìn)行了Huffman編碼,得到的碼流記為SQ1 bits和SQ2 bits。同時(shí),這里又需要記錄第三個(gè)碼表,稱為Huffman碼表3。同理,這個(gè)碼表也用相同的方法記錄,也等效為一個(gè)碼長序列,稱為CCL,因?yàn)橹炼嘤?-18個(gè),PK認(rèn)為樹的深度至多為7,于是CCL的范圍是0-7。

當(dāng)?shù)玫搅薈CL序列后,PK決定不再折騰,對這個(gè)序列用普通的3比特定長編碼記錄下來即可,即000代表0,111代表7。但實(shí)際上還有一點(diǎn)小折騰,就是最后這個(gè)序列如果全部記錄,那就需要19*3=57個(gè)比特,PK認(rèn)為CL序列里面CL范圍為0-15,特殊的幾個(gè)值是16、17、18,如果把CCL序列位置置換一下,把16、17、18這些放前面,那么這個(gè)CCL序列就很可能最后面跟著一串0(因?yàn)镃L=14,15這些很可能沒有),所以最后還引入了一個(gè)置換,其示意圖如下,分別表示置換前的CCL序列和置換后的CCL??梢钥闯?,16、17、18對應(yīng)的CCL被放到了前面,這樣如果尾部出現(xiàn)了一些0,就只需要記錄CCL長度即可,后面的0不記錄??梢岳^續(xù)節(jié)省一些比特,不過這個(gè)例子尾部置換后只有1個(gè)0:

不過粗看起來,這個(gè)置換效果并不好,我一開始接觸這個(gè)置換的時(shí)候,我覺得應(yīng)該按照16、17、18、0、1、2、3、。。。這樣的順序來存儲(chǔ),如果按照我理解的,那么置換后的結(jié)果如下:

2、4、0、4、5、5、1、5、0、6、0、0、0、0、0、0、0、0、0

這樣后面的一大串0直接截?cái)?,比PK的方法更短。但PK卻按照上面的順序。我總是認(rèn)為,我覺得牛人可能出錯(cuò)了的時(shí)候,往往是我自己錯(cuò)了,所以我又仔細(xì)想了一下,上面的順序特點(diǎn)比較明顯,直觀上看,PK認(rèn)為CL為0和中間的值出現(xiàn)得比較多(放在了前面),但CL比較小的和比較大的出現(xiàn)得比較少(1、15、2、14這些放在了后面,你看,后面交叉著放),在文件比較小的時(shí)候,這種方法效果不算好,上面就是一個(gè)典型的例子,但文件比較大了以后,CL1、CL2碼樹比較大,碼字長度普遍比較長,大部分很可能接近于中間值,那么這個(gè)時(shí)候PK的方法可能就體現(xiàn)出優(yōu)勢了。不得不說,對一個(gè)算法或者數(shù)據(jù)結(jié)構(gòu)的優(yōu)化程度,簡直完全取決于程序員對那個(gè)東西細(xì)節(jié)的理解的深度。當(dāng)我仔細(xì)研究了ZIP壓縮算法的過程之后,我對PK這種深夜埋頭冥思苦想的大牛佩服得五體投地。

到此為止,ZIP壓縮算法的結(jié)果已經(jīng)完畢。這個(gè)算法命名為Deflate算法。總結(jié)一下其編碼流程為:

8、Deflate壓縮數(shù)據(jù)格式

ZIP的格式實(shí)際上就是Deflate壓縮碼流外面套了一層文件相關(guān)的信息,這里先介紹Deflate壓縮碼流格式。其格式為:

Header:3個(gè)比特,第一個(gè)比特如果是1,表示此部分為最后一個(gè)壓縮數(shù)據(jù)塊;否則表示這是.ZIP文件的某個(gè)中間壓縮數(shù)據(jù)塊,但后面還有其他數(shù)據(jù)塊。這是ZIP中使用分塊壓縮的標(biāo)志之一;第2、3比特表示3個(gè)選擇:壓縮數(shù)據(jù)中沒有使用Huffman、使用靜態(tài)Huffman、使用動(dòng)態(tài)Huffman,這是對LZ77編碼后的literal/length/distance進(jìn)行進(jìn)一步編碼的標(biāo)志。我們前面分析的都是動(dòng)態(tài)Huffman,其實(shí)Deflate也支持靜態(tài)Huffman編碼,靜態(tài)Huffman編碼原理更為簡單,無需記錄碼表(因?yàn)镻K自己定義了一個(gè)固定的碼表),但壓縮率不高,所以大多數(shù)情況下都是動(dòng)態(tài)Huffman。

HLIT:5比特,記錄literal/length碼樹中碼長序列(CL1)個(gè)數(shù)的一個(gè)變量。后面CL1個(gè)數(shù)等于HLIT+257(因?yàn)橹辽儆?-255總共256個(gè)literal,還有一個(gè)256表示解碼結(jié)束,但length的個(gè)數(shù)不定)。

HDIST:5比特,記錄distance碼樹中碼長序列(CL2)個(gè)數(shù)的一個(gè)變量。后面CL2個(gè)數(shù)等于HDIST+1。哪怕沒有1個(gè)重復(fù)字符串,distance都為0也是一個(gè)CL。

HCLEN:4比特,記錄Huffman碼表3中碼長序列(CCL)個(gè)數(shù)的一個(gè)變量。后面CCL個(gè)數(shù)等于HCLEN+4。PK認(rèn)為CCL個(gè)數(shù)不會(huì)低于4個(gè),即使對于整個(gè)文件只有1個(gè)字符的情況。

接下來是3比特編碼的CCL,一共HCLEN+4個(gè),用以構(gòu)造Huffman碼表3;

接下來是對CL1(碼長)序列經(jīng)過游程編碼(SQ1:縮短的整數(shù)序列)后,并對SQ1繼續(xù)用Huffman編碼后的比特流。包含HLIT+257個(gè)CL1,其解碼碼表為Huffman碼表3,用以構(gòu)造Huffman碼表1;

接下來是對CL2(碼長)序列經(jīng)過游程編碼(SQ2:縮短的整數(shù)序列)后,并對SQ2繼續(xù)用Huffman編碼后的比特流。包含HDIST+1個(gè)CL2,其解碼碼表為Huffman碼表3,用于構(gòu)造Huffman碼表2;

總之,上面的數(shù)據(jù)都是為了構(gòu)造LZ解碼需要的2個(gè)Huffman碼表。

接下來才是經(jīng)過Huffman編碼的壓縮數(shù)據(jù),解碼碼表為Huffman碼表1和碼表2。
最后是數(shù)據(jù)塊結(jié)束標(biāo)志,即literal/length這個(gè)碼表輸入符號位256的編碼比特。
對倒數(shù)第1、2內(nèi)容塊進(jìn)行解碼時(shí),首先利用Huffman碼表1進(jìn)行解碼,如果解碼所得整數(shù)位于0-255之間,表示literal未匹配字符,接下來仍然利用Huffman碼表1解碼;如果位于257-285之間,表示length匹配長度,之后需要利用Huffman碼表2進(jìn)行解碼得到distance偏移距離;如果等于256,表示數(shù)據(jù)塊解碼結(jié)束。

9、ZIP文件格式解析

上面各節(jié)對ZIP的原理進(jìn)行了分析,這一節(jié)我們來看一個(gè)實(shí)際的例子,為了更好地描述,我們盡量把這個(gè)例子舉得簡單一些。下面是我隨便從一本書拷貝出來的一段較短的待壓縮的英文文本數(shù)據(jù):

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

這段英文文本長度為80字節(jié)。經(jīng)過ZIP壓縮后,其內(nèi)容如下:

可以看到,第1、2字節(jié)就是PK。看著怎么比原文還長,這怎么叫壓縮?實(shí)際上,這里面大部分內(nèi)容是ZIP的文件標(biāo)記開銷,真正壓縮的內(nèi)容(也就是我們前面提到的Deflate數(shù)據(jù),劃線部分都是ZIP文件開銷)其實(shí)肯定要比原文短(否則ZIP不會(huì)啟用壓縮),我們這個(gè)例子是個(gè)短文本,但對于更長的文本而言,那ZIP文件總體長度肯定是要短于原始文本的。上面的這個(gè)ZIP文件,可以看到好幾個(gè)以PK開頭的區(qū)域,也就是不同顏色的劃線區(qū)域,這些其實(shí)都是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壓縮數(shù)據(jù),在一個(gè)ZIP文件里,至少有一段,至多那就不好說了,假如你要壓縮的文件一共有10個(gè),那這個(gè)地方至少會(huì)有10段,ZIP對每個(gè)文件進(jìn)行了獨(dú)立壓縮,RAR在此進(jìn)行了改進(jìn),將多個(gè)文件聯(lián)合起來進(jìn)行壓縮,提高了壓縮率。local file header的格式如下:

可見,起始的4個(gè)字節(jié)就是0×50(P)、0x4B(K)、0×03、0×04,因?yàn)槭堑妥止?jié)優(yōu)先,所以Signature=0x03044B50.接下來的內(nèi)容按照上面的格式解析,十分簡單,這個(gè)區(qū)域在上面ZIP數(shù)據(jù)的那個(gè)圖里面是紅色劃線區(qū)域,之后則是壓縮后的Deflate數(shù)據(jù)。在文件的尾部,還有ZIP尾部數(shù)據(jù),上面這個(gè)例子包含了central directory和end of central directory record,一般這兩部分也是必須的。central directory以0×50、0x4B、0×01、0×02開頭;end of central directory record以0×50、0x4B、0×05、0×06開頭,其含義比較簡單,分別對應(yīng)于上面ZIP數(shù)據(jù)那個(gè)圖的藍(lán)色和綠色部分,下面是兩者的格式:

end of central directory record格式:

這幾張圖是我從網(wǎng)上找的,寫得比較清晰。對于其中的含義,解釋起來也比較簡單,我分析的結(jié)果如下:注意ZIP采用的低字節(jié)優(yōu)先,在一個(gè)字節(jié)里面低位優(yōu)先,需要反過來看。

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字節(jié),整個(gè)文件長度為186字節(jié),因此Deflate壓縮數(shù)據(jù)長度為72字節(jié)(576比特)。盡管這里看起來只是從80字節(jié)壓縮到72字節(jié),那是因?yàn)檫@是一段短文本,重復(fù)字符串出現(xiàn)較少,但如果文本較長,那壓縮率就會(huì)增加,這里只是舉個(gè)例子。

下面對其中的關(guān)鍵部分,也就是Deflate壓縮數(shù)據(jù)進(jìn)行解析。

10、Deflate解碼過程實(shí)例分析

我們按照ZIP格式把Deflate壓縮數(shù)據(jù)(72字節(jié))提取出來,如下(每行8字節(jié)):

1010100001010011100010111011000000000001000001000011000010100010
1000101110101010011110110000000001100011101110000011100010100101
0101001111001100000010001101001010010010000101101010101100001101
1011110100011111100011101111111001110010011101110110011100010101
0010110100010100101100110001100100000100110111101101111000011101
0010001001100110111001000010011001101010101000110110000001110101
0100011010010011100010110111001000111101101001011100101010010111
0111000011111000011110000011010111001011011111111100100010001001
1010001100001110000010101010111101101010100101111101011111100000

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

Header:101, 第一個(gè)比特是1,表示此部分為最后一個(gè)壓縮數(shù)據(jù)塊;后面的兩個(gè)比特01表示采用動(dòng)態(tài)哈夫曼、靜態(tài)哈夫曼、或者沒有編碼的標(biāo)志,01表示采用動(dòng)態(tài)Huffman;在RFC1951里面是這么說明的:

00 – no compression

01 – compressed with fixed Huffman codes

10 – compressed with dynamic Huffman codes

11 – reserved (error)

注意,這里需要按照低比特在先的方式去看,否則會(huì)誤以為是靜態(tài)Huffman。

接下來:
HLIT:01000,記錄literal/length碼樹中碼長序列個(gè)數(shù)的一個(gè)變量,表示HLIT=2(低位在前),說明后面存在HLIT + 257=259個(gè)CL1,CL1即0-258被編碼后的長度,其中0-255表示Literal,256表示無效符號,257、258分別表示Length=3、4(length從3開始)。因此,這里實(shí)際上只出現(xiàn)了兩種重復(fù)字符串的長度,即3和4?;仡欉@個(gè)圖可以更清楚:

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

繼續(xù):

HCLEN:0111,記錄Huffman碼樹3中碼長序列個(gè)數(shù)的一個(gè)變量,表示HCLEN=14(1110二進(jìn)制),即說明緊接著跟著HCLEN+4=18個(gè)CCL,前面已經(jīng)分析過,CCL記錄了一個(gè)Huffman碼表,這個(gè)碼表可以用一個(gè)碼長序列表示,根據(jù)這個(gè)碼長序列可以得到碼表。于是接下來我們把后面的18*3=54個(gè)比特拷貝出來,上面的碼流目前解析為下面的結(jié)果:

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

標(biāo)準(zhǔn)的CCL長度為19(回憶一下:CCL范圍為0-18,按照整數(shù)大小排序記錄各自的碼字長度),因此最后一個(gè)補(bǔ)0。得到序列:

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

其長度分別為(低位在前):
0、5、3、3、0、0、0、2、0、2、0、3、0、5、0、5、0、5、0
前面已經(jīng)分析過,這個(gè)CCL序列實(shí)際上是經(jīng)過一次置換操作得到的,需要進(jìn)行相反的置換,置換后為:

3、5、5、5、3、2、2、0、0、0、0、0、0、0、0、0、0、5、3
這個(gè)就是對應(yīng)于0-18的碼字長度序列。
根據(jù)Deflate樹的構(gòu)造方式,得到下面的碼表(Huffman碼表3):

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

接下來就是CL1序列,按照前面的指示,一共有259個(gè),分別對應(yīng)于literal/length:0-258對應(yīng)的碼字長度序列,我們隊(duì)跟著CCL后面的比特按照上面獲得的碼表進(jìn)行逐步解碼,在解碼之前,實(shí)際上并不知道CL1的比特流長度有多少,需要根據(jù)259這個(gè)數(shù)字來判定,解完了259個(gè)整數(shù),表明解析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比特,記錄連續(xù)的11-138個(gè)0,此處一共0010101b=21,即記錄21+11=32個(gè)0)

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

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

01(6)110(18)0010100(7比特,記錄連續(xù)的11-138個(gè)0,此處為10100b=20,即記錄20+11=31個(gè)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比特,記錄連續(xù)的11-138個(gè)0,此處為1111011b=123,即記錄123+11=134個(gè)0)

統(tǒng)計(jì)一下,上面已經(jīng)解了32+11+18+31+134+30=256個(gè)數(shù)了,因?yàn)榭偣?59個(gè),還差三個(gè):

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個(gè),每行40個(gè)。根據(jù)這個(gè)序列,同樣按照Deflate樹構(gòu)造方法,得到literal/length碼表(Huffman碼表1)為:

000     –> (System.Char)(看前面的CL1序列,空格對應(yīng)的ASCII為0×20=32,碼字長度3,即上面序列中第一個(gè)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序列,對應(yīng)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序列,對應(yīng)256,碼字長度6)
111111  –>4(System.Int32)(看前面的CL1序列,對應(yīng)258,碼字長度6)

可以看出,碼表里存在兩個(gè)重復(fù)字符串長度3和4,當(dāng)解碼結(jié)果為-1(上面進(jìn)行了處理,即256),或者說遇到111110的時(shí)候,表示Deflate碼流結(jié)束。

按照同樣的道理,對CL2序列進(jìn)行解析,前面已經(jīng)知道HDIST=10,即有11個(gè)CL2整數(shù)序列:

11111(17)000(3比特,記錄連續(xù)的3-10個(gè)0,此處為0,即記錄3個(gè)0)

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

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

已經(jīng)結(jié)束,總共11個(gè)。

于是CL2序列為:

0 0 0 2 0 0 0 0 1 0 2

分別記錄的是distance碼為0-10的碼字長度,根據(jù)下面的對應(yīng)關(guān)系,需要進(jìn)行擴(kuò)展:

比如,第1個(gè)碼長2記錄的是Code=3的長度,即Distance=4對應(yīng)的碼字為:

10      –>4(System.Int32)

第1個(gè)碼長1記錄的是Code=8的長度(碼字為0,擴(kuò)展三位000-111),即Distance=17-24對應(yīng)的碼字為(注意,低比特優(yōu)先):

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)

注意,擴(kuò)展的時(shí)候還是低比特優(yōu)先。

最后1個(gè)碼長2記錄的是Code=10的長度(其實(shí)是碼字:11,擴(kuò)展四位0000-1111),即Distance=33-48對應(yīng)的碼字為:

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碼表1、Huffman碼表2已經(jīng)還原出來,接下來是對LZ壓縮所得到的literal、distance、length進(jìn)行解碼,目前剩余的比特流如下,先按照Huffman碼表1解碼,如果解碼結(jié)果是長度(>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,表示下一個(gè)需要用Huffman解碼)10(Distance=4,即重復(fù)字符串為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,接下來根據(jù)distance解碼)0110(Distance=20,即重復(fù)字符串為s o)

[ther ]:111111(長度指示=4,接下來根據(jù)distance解碼)111001(Distance=42,即重復(fù)字符串為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,結(jié)束標(biāo)志]111110(結(jié)束標(biāo)志)0000(字節(jié)補(bǔ)齊的0)

于是解壓縮結(jié)果為:

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

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

譯碼過程:
1、根據(jù)HCLEN得到截尾信息,并參照固定置換表,根據(jù)CCL比特流得到CCL整數(shù)序列;
2、根據(jù)CCL整數(shù)序列構(gòu)造出等價(jià)于CCL的二級Huffman碼表3;
3、根據(jù)二級Huffman碼表3對CL1、CL2比特流進(jìn)行解碼,得到SQ1整數(shù)序列,SQ2整數(shù)序列;
4、根據(jù)SQ1整數(shù)序列,SQ2整數(shù)序列,利用游程編碼規(guī)則得到等價(jià)的CL1整數(shù)序列、CL2整數(shù)序列;
5、根據(jù)CL1整數(shù)序列、CL2整數(shù)序列分別構(gòu)造兩個(gè)一級Huffman碼表:literal/length碼表、distance碼表;
6、根據(jù)兩個(gè)一級Huffman碼表對后面的LZ壓縮數(shù)據(jù)進(jìn)行解碼得到literal/length/distance流;
7、根據(jù)literal/length/distance流按照LZ規(guī)則進(jìn)行解碼。

Deflate碼流長度總共為72字節(jié)=576比特,其中:

3比特Header;

5比特HLIT;

5比特HDIST;

4比特HCLEN;

54比特CCL序列碼流;

133比特CL1序列碼流;

34比特CL2序列碼流;

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

11、ZIP的其它說明

上面各個(gè)環(huán)節(jié)已經(jīng)詳細(xì)分析了ZIP壓縮的過程以及解碼流程,通過對一個(gè)實(shí)例的解壓縮過程分析,可以徹底地掌握ZIP壓縮和解壓縮的原理和過程。還有一些情況需要說明:

(1)上面的算法復(fù)雜度主要在于壓縮一端,因?yàn)樾枰y(tǒng)計(jì)literal/length/distance,創(chuàng)建動(dòng)態(tài)Huffman碼表,相反解壓只需要還原碼表后,逐比特解析即可,這也是壓縮軟件的一個(gè)典型特點(diǎn),解壓速度遠(yuǎn)快于壓縮速度。

(2)上面我們分析了動(dòng)態(tài)Huffman,對于LZ壓縮后的literal/length/distance,也可以采用靜態(tài)Huffman編碼,這主要取決于ZIP在壓縮中看哪種方式更節(jié)省空間,靜態(tài)Huffman編碼不需要記錄碼表,因?yàn)檫@個(gè)碼表是固定的,在RFC1951里面也有說明。對于literal/length碼表來說,需要對0-285進(jìn)行編碼,其碼表為:

對于Distance來說,需要對Code=0-29的數(shù)進(jìn)行編碼,則直接采用5比特表示。Distance和動(dòng)態(tài)Huffman一樣,在此基礎(chǔ)上進(jìn)行擴(kuò)展。

(3)ZIP中使用的LZ77算法是一種改進(jìn)的LZ77。主要區(qū)別有兩點(diǎn):

1)標(biāo)準(zhǔn)LZ77在找到重復(fù)字符串時(shí)輸出三元組(length, distance, 下一個(gè)未匹配的字符)(有興趣可以關(guān)注LZ77那篇論文);Deflate在找到重復(fù)字符串時(shí)僅輸出雙元組(length, distance)。
2)標(biāo)準(zhǔn)LZ77使用”貪婪“的方式解析,尋找的都是最長匹配字符串。Deflate中不完全如此。David Salomon的書里給了一個(gè)例子:

對于上面這個(gè)例子,標(biāo)準(zhǔn)LZ77在滑動(dòng)窗口中查找最長匹配字符串,找到的是”the”與前面的there的前三個(gè)字符匹配,這種貪婪解析方式邏輯簡單,但編碼效率不一定最高。Deflate則不急于輸出,跳過t繼續(xù)往后查看,發(fā)現(xiàn)”th ne”這5個(gè)字符存在重復(fù)字符串,因此,Deflate算法會(huì)選擇將t作為未匹配字符輸出,而對后面的匹配字符串用(length, distance)編碼輸出。顯然,這樣就提高了壓縮效率,因?yàn)闃?biāo)準(zhǔn)的LZ77找到的重復(fù)字符串長度為3,而Deflate找到的是5。換句話說,Deflate算法并不是簡單的尋找最長匹配后輸出,而是會(huì)權(quán)衡幾種可行的編碼方式,用其中最高效的方式輸出。

12、總結(jié)

本篇博文對ZIP中使用的壓縮算法進(jìn)行了詳細(xì)分析,從一個(gè)簡單地例子出發(fā),一步步地分析了PK設(shè)計(jì)Deflate算法的思路。最后,通過一個(gè)實(shí)際例子,分析了其解壓縮流程。總的來看,ZIP的核心在于如何對LZ壓縮后的literal、length、distance進(jìn)行Huffman編碼,以及如何以最小空間記錄Huffman碼表。整個(gè)過程充滿了對數(shù)據(jù)結(jié)構(gòu)尤其是樹的深入優(yōu)化利用。按照上面的分析,如果要對ZIP進(jìn)行進(jìn)一步改進(jìn),可以考慮的地方也有不少,典型的有:

(1)擴(kuò)大LZ編碼的滑動(dòng)窗口的大??;

(2)將Huffman編碼改進(jìn)為算術(shù)編碼等壓縮率更高的方法,畢竟,Huffman的碼字長度必須為整數(shù),這就從理論上限制了它的壓縮率只能接近于理論極限,但難以達(dá)到。我記得在JPEG圖像編碼領(lǐng)域,以前的JPEG采用了DCT變換編碼+Huffman的方式,現(xiàn)在JPEG2000將其改為小波變換+算數(shù)編碼,所以數(shù)據(jù)壓縮也可以嘗試類似的思路;

(3)將多個(gè)文件進(jìn)行合并壓縮,ZIP中,不同的文件壓縮過程沒有關(guān)系,獨(dú)立進(jìn)行,如果將它們合并起來一起進(jìn)行壓縮,壓縮率可以得到進(jìn)一步提高。

描述分析有誤的地方,敬請指正。針對數(shù)據(jù)壓縮相關(guān)的話題,后續(xù)會(huì)對HBase列壓縮等等進(jìn)行分析,看看ZIP這種文件壓縮和HBase這種數(shù)據(jù)庫數(shù)據(jù)壓縮的區(qū)別和聯(lián)系。

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多