zip 的压缩原理与实现
生活随笔
收集整理的這篇文章主要介紹了
zip 的压缩原理与实现
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
http://www.blueidea.com/bbs/newsdetail.asp?id=1819267&page=2&posts=&Daysprune=5&lp=1
無損數據壓縮是一件奇妙的事情,想一想,一串任意的數據能夠根據一定的規則轉換成只有原來 1/2 - 1/5 長度的數據,并且能夠按照相應的規則還原到原來的樣子,聽起來真是很酷。
半年前,苦熬過初學 vc 時那段艱難的學習曲線的我,對 MFC、SDK 開始失望和不滿,這些雖然不算易學,但和 DHTML 沒有實質上的區別,都是調用微軟提供的各種各樣的函數,不需要你自己去創建一個窗口,多線程編程時,也不需要你自己去分配 CPU 時間。我也做過驅動,同樣,有DDK(微軟驅動開發包),當然,也有 DDK 的“參考手冊”,連一個最簡單的數據結構都不需要你自己做,一切都是函數、函數……
微軟的高級程序員編寫了函數讓我們這些搞應用的去調用,我不想在這里貶低搞應用的人,正是這些應用工程師連接起了科學和社會之間的橋梁,將來可以做銷售,做管理,用自己逐漸積累起來的智慧和經驗在社會上打拼。
但是,在技術上來說,誠實地說,這并不高深,不是嗎?第一流的公司如微軟、Sybase、Oracle 等總是面向社會大眾的,這樣才能有巨大的市場。但是他們往往也是站在社會的最頂層的:操作系統、編譯器、數據庫都值得一代代的專家去不斷研究。這些帝國般的企業之所以偉大,恐怕不是“有經驗”、“能吃苦”這些中國特色的概念所能涵蓋的,艱深的技術體系、現代的管理哲學、強大的市場能力都是缺一不可的吧。我們既然有志于技術,并且正在起步階段,何必急不可耐地要轉去做“管理”,做“青年才俊”,那些所謂的“成功人士”的根底能有幾何,這樣子浮躁,胸中的規模和格局能有多大?
在我發現vc只是一個用途廣泛的編程工具,并不能代表“知識”、“技術”的時候,我有些失落,無所不能的不是我,而是 MFC、SDK、DDK,是微軟的工程師,他們做的,正是我想做的,或者說,我也想成為那種層次的人,現在我知道了,他們是專家,但這不會是一個夢,有一天我會做到的,為什么不能說出我的想法呢。
那時公司做的系統里有一個壓縮模塊,領導找了一個 zlib 庫,不讓我自己做壓縮算法,站在公司的立場上,我很理解,真的很理解,自己做算法要多久啊。但那時自己心中隱藏的一份倔強驅使我去尋找壓縮原理的資料,我完全沒有意識到,我即將打開一扇大門,進入一個神奇的“數據結構”的世界。“計算機藝術”的第一線陽光,居然也照到了我這樣一個平凡的人的身上。
上面說到“計算機藝術”,或者進一步細化說“計算機編程藝術”,聽起來很深奧,很高雅,但是在將要進入專業的壓縮算法的研究時,我要請大家做的第一件事情是:忘掉自己的年齡、學歷,忘掉自己的社會身份,忘掉編程語言,忘掉“面向對象”、“三層架構”等一切術語。把自己當作一個小孩,有一雙求知的眼睛,對世界充滿不倦的、單純的好奇,唯一的前提是一個正常的具有人類理性思維能力的大腦。
下面就讓我們開始一段神奇的壓縮算法之旅吧:
1. 原理部分:
有兩種形式的重復存在于計算機數據中,zip 就是對這兩種重復進行了壓縮。
一種是短語形式的重復,即三個字節以上的重復,對于這種重復,zip用兩個數字:1.重復位置距當前壓縮位置的距離;2.重復的長度,來表示這個重復,假設這兩個數字各占一個字節,于是數據便得到了壓縮,這很容易理解。
一個字節有 0 - 255 共 256 種可能的取值,三個字節有 256 * 256 * 256 共一千六百多萬種可能的情況,更長的短語取值的可能情況以指數方式增長,出現重復的概率似乎極低,實則不然,各種類型的數據都有出現重復的傾向,一篇論文中,為數不多的術語傾向于重復出現;一篇小說,人名和地名會重復出現;一張上下漸變的背景圖片,水平方向上的像素會重復出現;程序的源文件中,語法關鍵字會重復出現(我們寫程序時,多少次前后copy、paste?),以幾十 K 為單位的非壓縮格式的數據中,傾向于大量出現短語式的重復。經過上面提到的方式進行壓縮后,短語式重復的傾向被完全破壞,所以在壓縮的結果上進行第二次短語式壓縮一般是沒有效果的。
第二種重復為單字節的重復,一個字節只有256種可能的取值,所以這種重復是必然的。其中,某些字節出現次數可能較多,另一些則較少,在統計上有分布不均勻的傾向,這是容易理解的,比如一個 ASCII 文本文件中,某些符號可能很少用到,而字母和數字則使用較多,各字母的使用頻率也是不一樣的,據說字母 e 的使用概率最高;許多圖片呈現深色調或淺色調,深色(或淺色)的像素使用較多(這里順便提一下:png 圖片格式是一種無損壓縮,其核心算法就是 zip 算法,它和 zip 格式的文件的主要區別在于:作為一種圖片格式,它在文件頭處存放了圖片的大小、使用的顏色數等信息);上面提到的短語式壓縮的結果也有這種傾向:重復傾向于出現在離當前壓縮位置較近的地方,重復長度傾向于比較短(20字節以內)。這樣,就有了壓縮的可能:給 256 種字節取值重新編碼,使出現較多的字節使用較短的編碼,出現較少的字節使用較長的編碼,這樣一來,變短的字節相對于變長的字節更多,文件的總長度就會減少,并且,字節使用比例越不均勻,壓縮比例就越大。
在進一步討論編碼的要求以及辦法前,先提一下:編碼式壓縮必須在短語式壓縮之后進行,因為編碼式壓縮后,原先八位二進制值的字節就被破壞了,這樣文件中短語式重復的傾向也會被破壞(除非先進行解碼)。另外,短語式壓縮后的結果:那些剩下的未被匹配的單、雙字節和得到匹配的距離、長度值仍然具有取值分布不均勻性,因此,兩種壓縮方式的順序不能變。
在編碼式壓縮后,以連續的八位作為一個字節,原先未壓縮文件中所具有的字節取值不均勻的傾向被徹底破壞,成為隨機性取值,根據統計學知識,隨機性取值具有均勻性的傾向(比如拋硬幣試驗,拋一千次,正反面朝上的次數都接近于 500 次)。因此,編碼式壓縮后的結果無法再進行編碼式壓縮。
短語式壓縮和編碼式壓縮是目前計算機科學界研究出的僅有的兩種無損壓縮方法,它們都無法重復進行,所以,壓縮文件無法再次壓縮(實際上,能反復進行的壓縮算法是不可想象的,因為最終會壓縮到 0 字節)。
短語式重復的傾向和字節取值分布不均勻的傾向是可以壓縮的基礎,兩種壓縮的順序不能互換的原因也說了,下面我們來看編碼式壓縮的要求及方法:
壓縮文件無法再次壓縮是因為:
1. 短語式壓縮去掉了三個字節以上的重復,壓縮后的結果中包含的是未匹配的單雙字節,和匹配距離、長度的組合。這個結果當然仍然可能包含三個字節以上的重復,但是概率極低。因為三個字節有 256 * 256 * 256 共一千六百多萬種可能的情況。
所以只要把原始文件中“自然存在”的短語式重復傾向壓掉就可以了,一千六百萬分之一的概率再去壓縮沒有必要。
2.編碼式壓縮利用各個單字節使用頻率不一樣的傾向,使定長編碼變為不定長編碼,給使用頻率高的字節更短的編碼,使用頻率低的字節更長的編碼,起到壓縮的效果。如果把編碼式壓縮的“結果”按照8位作為1字節,重新統計各字節的使用頻率,應該是大致相等的。因為新的字節使用頻率是隨機的。相等的頻率再去變換字節長短是沒有意義的,因為變短的字節沒有比變長的字節更多。
所以壓掉了原始文件中“自然存在”的單字節使用頻率不均勻的傾向后,隨機的使用頻率再去壓縮也失去了意義。
首先,為了使用不定長的編碼表示單個字符,編碼必須符合“前綴編碼”的要求,即較短的編碼決不能是較長編碼的前綴,反過來說就是,任何一個字符的編碼,都不是由另一個字符的編碼加上若干位 0 或 1 組成,否則解壓縮程序將無法解碼。
看一下前綴編碼的一個最簡單的例子:
符號 編碼
A 0
B 10
C 110
D 1110
E 11110
有了上面的碼表,你一定可以輕松地從下面這串二進制流中分辨出真正的信息內容了:
1110010101110110111100010 - DABBDCEAAB
要構造符合這一要求的二進制編碼體系,二叉樹是最理想的選擇。考察下面這棵二叉樹:
根(root)
0 | 1
+-------+--------+
0 | 1 0 | 1
+-----+------+ +----+----+
| | | |
a | d e
0 | 1
+-----+-----+
| |
b c
要編碼的字符總是出現在樹葉上,假定從根向樹葉行走的過程中,左轉為0,右轉為1,則一個字符的編碼就是從根走到該字符所在樹葉的路徑。正因為字符只能出現在樹葉上,任何一個字符的路徑都不會是另一字符路徑的前綴路徑,符合要求的前綴編碼也就構造成功了:
a - 00 b - 010 c - 011 d - 10 e - 11
接下來來看編碼式壓縮的過程:
為了簡化問題,假定一個文件中只出現了 a,b,c,d ,e四種字符,它們的出現次數分別是
a : 6次
b : 15次
c : 2次
d : 9次
e : 1次
如果用定長的編碼方式為這四種字符編碼: a : 000 b : 001 c : 010 d : 011 e : 100
那么整個文件的長度是 3*6 + 3*15 + 3*2 + 3*9 + 3*1 = 99
用二叉樹表示這四種編碼(其中葉子節點上的數字是其使用次數,非葉子節點上的數字是其左右孩子使用次數之和):
根
|
+---------33---------+
| |
+----32---+ +----1---+
| | | |
+-21-+ +-11-+ +--1--+
| | | | | |
6 15 2 9 1
(如果某個節點只有一個子節點,可以去掉這個子節點。)
根
|
+------33------+
| |
+-----32----+ 1
| |
+--21--+ +--11--+
| | | |
6 15 2 9
現在的編碼是: a : 000 b : 001 c : 010 d : 011 e : 1 仍然符合“前綴編碼”的要求。
第一步:如果發現下層節點的數字大于上層節點的數字,就交換它們的位置,并重新計算非葉子節點的值。
先交換11和1,由于11個字節縮短了一位,1個字節增長了一位,總文件縮短了10位。
根
|
+----------33---------+
| |
+-----22----+ +----11----+
| | | |
+--21--+ 1 2 9
| |
6 15
再交換15和1、6和2,最終得到這樣的樹:
根
|
+----------33---------+
| |
+-----18----+ +----15----+
| | | |
+--3--+ 15 6 9
| |
2 1
這時所有上層節點的數值都大于下層節點的數值,似乎無法再進一步壓縮了。但是我們把每一層的最小的兩個節點結合起來,常會發現仍有壓縮余地。
第二步:把每一層的最小的兩個節點結合起來,重新計算相關節點的值。
在上面的樹中,第一、二、四三層都只有一或二個節點,無法重新組合,但第三層上有四個節點,我們把最小的3和6結合起來,并重新計算相關節點的值,成為下面這棵樹。
根
|
+----------33---------+
| |
+------9-----+ +----24----+
| | | |
+--3--+ 6 15 9
| |
2 1
然后,再重復做第一步。
這時第二層的9小于第三層的15,于是可以互換,有9個字節增長了一位,15個字節縮短了一位,文件總長度又縮短了6位。然后重新計算相關節點的值。
根
|
+----------33---------+
| |
15 +----18----+
| |
+------9-----+ 9
| |
+--3--+ 6
| |
2 1
這時發現所有的上層節點都大于下層節點,每一層上最小的兩個節點被并在了一起,也不可能再產生比同層其他節點更小的父節點了。
這時整個文件的長度是 3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63
這時可以看出編碼式壓縮的一個基本前提:各節點之間的值要相差比較懸殊,以使某兩個節點的和小于同層或下層的另一個節點,這樣,交換節點才有利益。
所以歸根結底,原始文件中的字節使用頻率必須相差較大,否則將沒有兩個節點的頻率之和小于同層或下層其他節點的頻率,也就無法壓縮。反之,相差得越懸殊,兩個節點的頻率之和比同層或下層節點的頻率小得越多,交換節點之后的利益也越大。
在這個例子中,經過上面兩步不斷重復,得到了最優的二叉樹,但不能保證在所有情況下,都能通過這兩步的重復得到最優二叉樹,下面來看另一個例子:
根
|
+---------19--------+
| |
+------12------+ 7
| |
+---5---+ +---7---+
| | | |
+-2-+ +-3-+ +-3-+ +-4-+
| | | | | | | |
1 1 1 2 1 2 2 2
這個例子中,所有上層節點都大于等于下層節點,每一層最小的兩個節點結合在了一起,但仍然可以進一步優化:
根
|
+---------19--------+
| |
+------12------+ 7
| |
+---4---+ +---8---+
| | | |
+-2-+ +-2-+ +-4-+ +-4-+
| | | | | | | |
1 1 1 1 2 2 2 2
通過最低一層的第4第5個節點對換,第3層的8大于第2層的7。
到這里,我們得出這樣一個結論:一棵最優二叉編碼樹(所有上層節點都無法和下層節點交換),必須符合這樣兩個條件:
1.所有上層節點都大于等于下層節點。
2.某節點,設其較大的子節點為m,較小的子節點為n,m下的任一層的所有節點都應大于等于n下的該層的所有節點。
當符合這兩個條件時,任一層都無法產生更小的節點去和下層節點交換,也無法產生更大的節點去和上層節點交換。
上面的兩個例子是比較簡單的,實際的文件中,一個字節有256種可能的取值,所以二叉樹的葉子節點多達256個,需要不斷的調整樹形,最終的樹形可能非常復雜,有一種非常精巧的算法可以快速地建起一棵最優二叉樹,這種算法由D.Huffman(戴·霍夫曼)提出,下面我們先來介紹霍夫曼算法的步驟,然后再來證明通過這么簡單的步驟得出的樹形確實是一棵最優二叉樹。
霍夫曼算法的步驟是這樣的:
·從各個節點中找出最小的兩個節點,給它們建一個父節點,值為這兩個節點之和。
·然后從節點序列中去除這兩個節點,加入它們的父節點到序列中。
重復上面兩個步驟,直到節點序列中只剩下唯一一個節點。這時一棵最優二叉樹就已經建成了,它的根就是剩下的這個節點。
仍以上面的例子來看霍夫曼樹的建立過程。
最初的節點序列是這樣的:
a(6) b(15) c(2) d(9) e(1)
把最小的c和e結合起來
| (3)
a(6) b(15) d(9) +------+------+
| |
c e
不斷重復,最終得到的樹是這樣的:
根
|
+-----33-----+
| |
15 +----18----+
| |
9 +------9-----+
| |
6 +--3--+
| |
2 1
這時各個字符的編碼長度和前面我們說過的方法得到的編碼長度是相同的,因而文件的總長度也是相同的: 3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63
考察霍夫曼樹的建立過程中的每一步的節點序列的變化:
6 15 2 9 1
6 15 9 3
15 9 9
15 18
33
下面我們用逆推法來證明對于各種不同的節點序列,用霍夫曼算法建立起來的樹總是一棵最優二叉樹:
對霍夫曼樹的建立過程運用逆推法:
當這個過程中的節點序列只有兩個節點時(比如前例中的15和18),肯定是一棵最優二叉樹,一個編碼為0,另一個編碼為1,無法再進一步優化。
然后往前步進,節點序列中不斷地減少一個節點,增加兩個節點,在步進過程中將始終保持是一棵最優二叉樹,這是因為:
1.按照霍夫曼樹的建立過程,新增的兩個節點是當前節點序列中最小的兩個,其他的任何兩個節點的父節點都大于(或等于)這兩個節點的父節點,只要前一步是最優二叉樹,其他的任何兩個節點的父節點就一定都處在它們的父節點的上層或同層,所以這兩個節點一定處在當前二叉樹的最低一層。
2.這兩個新增的節點是最小的,所以無法和其他上層節點對換。符合我們前面說的最優二叉樹的第一個條件。
3.只要前一步是最優二叉樹,由于這兩個新增的節點是最小的,即使同層有其他節點,也無法和同層其他節點重新結合,產生比它們的父節點更小的上層節點來和同層的其他節點對換。它們的父節點小于其他節點的父節點,它們又小于其他所有節點,只要前一步符合最優二叉樹的第二個條件,到這一步仍將符合。
這樣一步步逆推下去,在這個過程中霍夫曼樹每一步都始終保持著是一棵最優二叉樹。
由于每一步都從節點序列中刪除兩個節點,新增一個節點,霍夫曼樹的建立過程共需 (原始節點數 - 1) 步,所以霍夫曼算法不失為一種精巧的編碼式壓縮算法。
附:對于 huffman 樹,《計算機程序設計藝術》中有完全不同的證明,大意是這樣的:
1.二叉編碼樹的內部節點(非葉子節點)數等于外部節點(葉子節點)數減1。
2.二叉編碼樹的外部節點的加權路徑長度(值乘以路徑長度)之和,等于所有內部節點值之和。(這兩條都可以通過對節點數運用數學歸納法來證明,留給大家做練習。)
3.對 huffman 樹的建立過程運用逆推,當只有一個內部節點時,肯定是一棵最優二叉樹。
4.往前步進,新增兩個最小的外部節點,它們結合在一起產生一個新的內部節點,當且僅當原先的內部節點集合是極小化的,加入這個新的內部節點后仍是極小化的。(因為最小的兩個節點結合在一起,并處于最低層,相對于它們分別和其他同層或上層節點結合在一起,至少不會增加加權路徑長度。)
5.隨著內部節點數逐個增加,內部節點集合總維持極小化。
2.實現部分
如果世界上從沒有一個壓縮程序,我們看了前面的壓縮原理,將有信心一定能作出一個可以壓縮大多數格式、內容的數據的程序,當我們著手要做這樣一個程序的時候,會發現有很多的難題需要我們去一個個解決,下面將逐個描述這些難題,并詳細分析 zip 算法是如何解決這些難題的,其中很多問題帶有普遍意義,比如查找匹配,比如數組排序等等,這些都是說不盡的話題,讓我們深入其中,做一番思考。
待續。。。。
無損數據壓縮是一件奇妙的事情,想一想,一串任意的數據能夠根據一定的規則轉換成只有原來 1/2 - 1/5 長度的數據,并且能夠按照相應的規則還原到原來的樣子,聽起來真是很酷。
半年前,苦熬過初學 vc 時那段艱難的學習曲線的我,對 MFC、SDK 開始失望和不滿,這些雖然不算易學,但和 DHTML 沒有實質上的區別,都是調用微軟提供的各種各樣的函數,不需要你自己去創建一個窗口,多線程編程時,也不需要你自己去分配 CPU 時間。我也做過驅動,同樣,有DDK(微軟驅動開發包),當然,也有 DDK 的“參考手冊”,連一個最簡單的數據結構都不需要你自己做,一切都是函數、函數……
微軟的高級程序員編寫了函數讓我們這些搞應用的去調用,我不想在這里貶低搞應用的人,正是這些應用工程師連接起了科學和社會之間的橋梁,將來可以做銷售,做管理,用自己逐漸積累起來的智慧和經驗在社會上打拼。
但是,在技術上來說,誠實地說,這并不高深,不是嗎?第一流的公司如微軟、Sybase、Oracle 等總是面向社會大眾的,這樣才能有巨大的市場。但是他們往往也是站在社會的最頂層的:操作系統、編譯器、數據庫都值得一代代的專家去不斷研究。這些帝國般的企業之所以偉大,恐怕不是“有經驗”、“能吃苦”這些中國特色的概念所能涵蓋的,艱深的技術體系、現代的管理哲學、強大的市場能力都是缺一不可的吧。我們既然有志于技術,并且正在起步階段,何必急不可耐地要轉去做“管理”,做“青年才俊”,那些所謂的“成功人士”的根底能有幾何,這樣子浮躁,胸中的規模和格局能有多大?
在我發現vc只是一個用途廣泛的編程工具,并不能代表“知識”、“技術”的時候,我有些失落,無所不能的不是我,而是 MFC、SDK、DDK,是微軟的工程師,他們做的,正是我想做的,或者說,我也想成為那種層次的人,現在我知道了,他們是專家,但這不會是一個夢,有一天我會做到的,為什么不能說出我的想法呢。
那時公司做的系統里有一個壓縮模塊,領導找了一個 zlib 庫,不讓我自己做壓縮算法,站在公司的立場上,我很理解,真的很理解,自己做算法要多久啊。但那時自己心中隱藏的一份倔強驅使我去尋找壓縮原理的資料,我完全沒有意識到,我即將打開一扇大門,進入一個神奇的“數據結構”的世界。“計算機藝術”的第一線陽光,居然也照到了我這樣一個平凡的人的身上。
上面說到“計算機藝術”,或者進一步細化說“計算機編程藝術”,聽起來很深奧,很高雅,但是在將要進入專業的壓縮算法的研究時,我要請大家做的第一件事情是:忘掉自己的年齡、學歷,忘掉自己的社會身份,忘掉編程語言,忘掉“面向對象”、“三層架構”等一切術語。把自己當作一個小孩,有一雙求知的眼睛,對世界充滿不倦的、單純的好奇,唯一的前提是一個正常的具有人類理性思維能力的大腦。
下面就讓我們開始一段神奇的壓縮算法之旅吧:
1. 原理部分:
有兩種形式的重復存在于計算機數據中,zip 就是對這兩種重復進行了壓縮。
一種是短語形式的重復,即三個字節以上的重復,對于這種重復,zip用兩個數字:1.重復位置距當前壓縮位置的距離;2.重復的長度,來表示這個重復,假設這兩個數字各占一個字節,于是數據便得到了壓縮,這很容易理解。
一個字節有 0 - 255 共 256 種可能的取值,三個字節有 256 * 256 * 256 共一千六百多萬種可能的情況,更長的短語取值的可能情況以指數方式增長,出現重復的概率似乎極低,實則不然,各種類型的數據都有出現重復的傾向,一篇論文中,為數不多的術語傾向于重復出現;一篇小說,人名和地名會重復出現;一張上下漸變的背景圖片,水平方向上的像素會重復出現;程序的源文件中,語法關鍵字會重復出現(我們寫程序時,多少次前后copy、paste?),以幾十 K 為單位的非壓縮格式的數據中,傾向于大量出現短語式的重復。經過上面提到的方式進行壓縮后,短語式重復的傾向被完全破壞,所以在壓縮的結果上進行第二次短語式壓縮一般是沒有效果的。
第二種重復為單字節的重復,一個字節只有256種可能的取值,所以這種重復是必然的。其中,某些字節出現次數可能較多,另一些則較少,在統計上有分布不均勻的傾向,這是容易理解的,比如一個 ASCII 文本文件中,某些符號可能很少用到,而字母和數字則使用較多,各字母的使用頻率也是不一樣的,據說字母 e 的使用概率最高;許多圖片呈現深色調或淺色調,深色(或淺色)的像素使用較多(這里順便提一下:png 圖片格式是一種無損壓縮,其核心算法就是 zip 算法,它和 zip 格式的文件的主要區別在于:作為一種圖片格式,它在文件頭處存放了圖片的大小、使用的顏色數等信息);上面提到的短語式壓縮的結果也有這種傾向:重復傾向于出現在離當前壓縮位置較近的地方,重復長度傾向于比較短(20字節以內)。這樣,就有了壓縮的可能:給 256 種字節取值重新編碼,使出現較多的字節使用較短的編碼,出現較少的字節使用較長的編碼,這樣一來,變短的字節相對于變長的字節更多,文件的總長度就會減少,并且,字節使用比例越不均勻,壓縮比例就越大。
在進一步討論編碼的要求以及辦法前,先提一下:編碼式壓縮必須在短語式壓縮之后進行,因為編碼式壓縮后,原先八位二進制值的字節就被破壞了,這樣文件中短語式重復的傾向也會被破壞(除非先進行解碼)。另外,短語式壓縮后的結果:那些剩下的未被匹配的單、雙字節和得到匹配的距離、長度值仍然具有取值分布不均勻性,因此,兩種壓縮方式的順序不能變。
在編碼式壓縮后,以連續的八位作為一個字節,原先未壓縮文件中所具有的字節取值不均勻的傾向被徹底破壞,成為隨機性取值,根據統計學知識,隨機性取值具有均勻性的傾向(比如拋硬幣試驗,拋一千次,正反面朝上的次數都接近于 500 次)。因此,編碼式壓縮后的結果無法再進行編碼式壓縮。
短語式壓縮和編碼式壓縮是目前計算機科學界研究出的僅有的兩種無損壓縮方法,它們都無法重復進行,所以,壓縮文件無法再次壓縮(實際上,能反復進行的壓縮算法是不可想象的,因為最終會壓縮到 0 字節)。
短語式重復的傾向和字節取值分布不均勻的傾向是可以壓縮的基礎,兩種壓縮的順序不能互換的原因也說了,下面我們來看編碼式壓縮的要求及方法:
壓縮文件無法再次壓縮是因為:
1. 短語式壓縮去掉了三個字節以上的重復,壓縮后的結果中包含的是未匹配的單雙字節,和匹配距離、長度的組合。這個結果當然仍然可能包含三個字節以上的重復,但是概率極低。因為三個字節有 256 * 256 * 256 共一千六百多萬種可能的情況。
所以只要把原始文件中“自然存在”的短語式重復傾向壓掉就可以了,一千六百萬分之一的概率再去壓縮沒有必要。
2.編碼式壓縮利用各個單字節使用頻率不一樣的傾向,使定長編碼變為不定長編碼,給使用頻率高的字節更短的編碼,使用頻率低的字節更長的編碼,起到壓縮的效果。如果把編碼式壓縮的“結果”按照8位作為1字節,重新統計各字節的使用頻率,應該是大致相等的。因為新的字節使用頻率是隨機的。相等的頻率再去變換字節長短是沒有意義的,因為變短的字節沒有比變長的字節更多。
所以壓掉了原始文件中“自然存在”的單字節使用頻率不均勻的傾向后,隨機的使用頻率再去壓縮也失去了意義。
首先,為了使用不定長的編碼表示單個字符,編碼必須符合“前綴編碼”的要求,即較短的編碼決不能是較長編碼的前綴,反過來說就是,任何一個字符的編碼,都不是由另一個字符的編碼加上若干位 0 或 1 組成,否則解壓縮程序將無法解碼。
看一下前綴編碼的一個最簡單的例子:
符號 編碼
A 0
B 10
C 110
D 1110
E 11110
有了上面的碼表,你一定可以輕松地從下面這串二進制流中分辨出真正的信息內容了:
1110010101110110111100010 - DABBDCEAAB
要構造符合這一要求的二進制編碼體系,二叉樹是最理想的選擇。考察下面這棵二叉樹:
根(root)
0 | 1
+-------+--------+
0 | 1 0 | 1
+-----+------+ +----+----+
| | | |
a | d e
0 | 1
+-----+-----+
| |
b c
要編碼的字符總是出現在樹葉上,假定從根向樹葉行走的過程中,左轉為0,右轉為1,則一個字符的編碼就是從根走到該字符所在樹葉的路徑。正因為字符只能出現在樹葉上,任何一個字符的路徑都不會是另一字符路徑的前綴路徑,符合要求的前綴編碼也就構造成功了:
a - 00 b - 010 c - 011 d - 10 e - 11
接下來來看編碼式壓縮的過程:
為了簡化問題,假定一個文件中只出現了 a,b,c,d ,e四種字符,它們的出現次數分別是
a : 6次
b : 15次
c : 2次
d : 9次
e : 1次
如果用定長的編碼方式為這四種字符編碼: a : 000 b : 001 c : 010 d : 011 e : 100
那么整個文件的長度是 3*6 + 3*15 + 3*2 + 3*9 + 3*1 = 99
用二叉樹表示這四種編碼(其中葉子節點上的數字是其使用次數,非葉子節點上的數字是其左右孩子使用次數之和):
根
|
+---------33---------+
| |
+----32---+ +----1---+
| | | |
+-21-+ +-11-+ +--1--+
| | | | | |
6 15 2 9 1
(如果某個節點只有一個子節點,可以去掉這個子節點。)
根
|
+------33------+
| |
+-----32----+ 1
| |
+--21--+ +--11--+
| | | |
6 15 2 9
現在的編碼是: a : 000 b : 001 c : 010 d : 011 e : 1 仍然符合“前綴編碼”的要求。
第一步:如果發現下層節點的數字大于上層節點的數字,就交換它們的位置,并重新計算非葉子節點的值。
先交換11和1,由于11個字節縮短了一位,1個字節增長了一位,總文件縮短了10位。
根
|
+----------33---------+
| |
+-----22----+ +----11----+
| | | |
+--21--+ 1 2 9
| |
6 15
再交換15和1、6和2,最終得到這樣的樹:
根
|
+----------33---------+
| |
+-----18----+ +----15----+
| | | |
+--3--+ 15 6 9
| |
2 1
這時所有上層節點的數值都大于下層節點的數值,似乎無法再進一步壓縮了。但是我們把每一層的最小的兩個節點結合起來,常會發現仍有壓縮余地。
第二步:把每一層的最小的兩個節點結合起來,重新計算相關節點的值。
在上面的樹中,第一、二、四三層都只有一或二個節點,無法重新組合,但第三層上有四個節點,我們把最小的3和6結合起來,并重新計算相關節點的值,成為下面這棵樹。
根
|
+----------33---------+
| |
+------9-----+ +----24----+
| | | |
+--3--+ 6 15 9
| |
2 1
然后,再重復做第一步。
這時第二層的9小于第三層的15,于是可以互換,有9個字節增長了一位,15個字節縮短了一位,文件總長度又縮短了6位。然后重新計算相關節點的值。
根
|
+----------33---------+
| |
15 +----18----+
| |
+------9-----+ 9
| |
+--3--+ 6
| |
2 1
這時發現所有的上層節點都大于下層節點,每一層上最小的兩個節點被并在了一起,也不可能再產生比同層其他節點更小的父節點了。
這時整個文件的長度是 3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63
這時可以看出編碼式壓縮的一個基本前提:各節點之間的值要相差比較懸殊,以使某兩個節點的和小于同層或下層的另一個節點,這樣,交換節點才有利益。
所以歸根結底,原始文件中的字節使用頻率必須相差較大,否則將沒有兩個節點的頻率之和小于同層或下層其他節點的頻率,也就無法壓縮。反之,相差得越懸殊,兩個節點的頻率之和比同層或下層節點的頻率小得越多,交換節點之后的利益也越大。
在這個例子中,經過上面兩步不斷重復,得到了最優的二叉樹,但不能保證在所有情況下,都能通過這兩步的重復得到最優二叉樹,下面來看另一個例子:
根
|
+---------19--------+
| |
+------12------+ 7
| |
+---5---+ +---7---+
| | | |
+-2-+ +-3-+ +-3-+ +-4-+
| | | | | | | |
1 1 1 2 1 2 2 2
這個例子中,所有上層節點都大于等于下層節點,每一層最小的兩個節點結合在了一起,但仍然可以進一步優化:
根
|
+---------19--------+
| |
+------12------+ 7
| |
+---4---+ +---8---+
| | | |
+-2-+ +-2-+ +-4-+ +-4-+
| | | | | | | |
1 1 1 1 2 2 2 2
通過最低一層的第4第5個節點對換,第3層的8大于第2層的7。
到這里,我們得出這樣一個結論:一棵最優二叉編碼樹(所有上層節點都無法和下層節點交換),必須符合這樣兩個條件:
1.所有上層節點都大于等于下層節點。
2.某節點,設其較大的子節點為m,較小的子節點為n,m下的任一層的所有節點都應大于等于n下的該層的所有節點。
當符合這兩個條件時,任一層都無法產生更小的節點去和下層節點交換,也無法產生更大的節點去和上層節點交換。
上面的兩個例子是比較簡單的,實際的文件中,一個字節有256種可能的取值,所以二叉樹的葉子節點多達256個,需要不斷的調整樹形,最終的樹形可能非常復雜,有一種非常精巧的算法可以快速地建起一棵最優二叉樹,這種算法由D.Huffman(戴·霍夫曼)提出,下面我們先來介紹霍夫曼算法的步驟,然后再來證明通過這么簡單的步驟得出的樹形確實是一棵最優二叉樹。
霍夫曼算法的步驟是這樣的:
·從各個節點中找出最小的兩個節點,給它們建一個父節點,值為這兩個節點之和。
·然后從節點序列中去除這兩個節點,加入它們的父節點到序列中。
重復上面兩個步驟,直到節點序列中只剩下唯一一個節點。這時一棵最優二叉樹就已經建成了,它的根就是剩下的這個節點。
仍以上面的例子來看霍夫曼樹的建立過程。
最初的節點序列是這樣的:
a(6) b(15) c(2) d(9) e(1)
把最小的c和e結合起來
| (3)
a(6) b(15) d(9) +------+------+
| |
c e
不斷重復,最終得到的樹是這樣的:
根
|
+-----33-----+
| |
15 +----18----+
| |
9 +------9-----+
| |
6 +--3--+
| |
2 1
這時各個字符的編碼長度和前面我們說過的方法得到的編碼長度是相同的,因而文件的總長度也是相同的: 3*6 + 1*15 + 4*2 + 2*9 + 4*1 = 63
考察霍夫曼樹的建立過程中的每一步的節點序列的變化:
6 15 2 9 1
6 15 9 3
15 9 9
15 18
33
下面我們用逆推法來證明對于各種不同的節點序列,用霍夫曼算法建立起來的樹總是一棵最優二叉樹:
對霍夫曼樹的建立過程運用逆推法:
當這個過程中的節點序列只有兩個節點時(比如前例中的15和18),肯定是一棵最優二叉樹,一個編碼為0,另一個編碼為1,無法再進一步優化。
然后往前步進,節點序列中不斷地減少一個節點,增加兩個節點,在步進過程中將始終保持是一棵最優二叉樹,這是因為:
1.按照霍夫曼樹的建立過程,新增的兩個節點是當前節點序列中最小的兩個,其他的任何兩個節點的父節點都大于(或等于)這兩個節點的父節點,只要前一步是最優二叉樹,其他的任何兩個節點的父節點就一定都處在它們的父節點的上層或同層,所以這兩個節點一定處在當前二叉樹的最低一層。
2.這兩個新增的節點是最小的,所以無法和其他上層節點對換。符合我們前面說的最優二叉樹的第一個條件。
3.只要前一步是最優二叉樹,由于這兩個新增的節點是最小的,即使同層有其他節點,也無法和同層其他節點重新結合,產生比它們的父節點更小的上層節點來和同層的其他節點對換。它們的父節點小于其他節點的父節點,它們又小于其他所有節點,只要前一步符合最優二叉樹的第二個條件,到這一步仍將符合。
這樣一步步逆推下去,在這個過程中霍夫曼樹每一步都始終保持著是一棵最優二叉樹。
由于每一步都從節點序列中刪除兩個節點,新增一個節點,霍夫曼樹的建立過程共需 (原始節點數 - 1) 步,所以霍夫曼算法不失為一種精巧的編碼式壓縮算法。
附:對于 huffman 樹,《計算機程序設計藝術》中有完全不同的證明,大意是這樣的:
1.二叉編碼樹的內部節點(非葉子節點)數等于外部節點(葉子節點)數減1。
2.二叉編碼樹的外部節點的加權路徑長度(值乘以路徑長度)之和,等于所有內部節點值之和。(這兩條都可以通過對節點數運用數學歸納法來證明,留給大家做練習。)
3.對 huffman 樹的建立過程運用逆推,當只有一個內部節點時,肯定是一棵最優二叉樹。
4.往前步進,新增兩個最小的外部節點,它們結合在一起產生一個新的內部節點,當且僅當原先的內部節點集合是極小化的,加入這個新的內部節點后仍是極小化的。(因為最小的兩個節點結合在一起,并處于最低層,相對于它們分別和其他同層或上層節點結合在一起,至少不會增加加權路徑長度。)
5.隨著內部節點數逐個增加,內部節點集合總維持極小化。
2.實現部分
如果世界上從沒有一個壓縮程序,我們看了前面的壓縮原理,將有信心一定能作出一個可以壓縮大多數格式、內容的數據的程序,當我們著手要做這樣一個程序的時候,會發現有很多的難題需要我們去一個個解決,下面將逐個描述這些難題,并詳細分析 zip 算法是如何解決這些難題的,其中很多問題帶有普遍意義,比如查找匹配,比如數組排序等等,這些都是說不盡的話題,讓我們深入其中,做一番思考。
待續。。。。
總結
以上是生活随笔為你收集整理的zip 的压缩原理与实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows自动启动程序的十大藏身之所
- 下一篇: PHP feof() 函数读文件的使用