眾所周知,通過網(wǎng)絡(luò)上傳或者下載數(shù)據(jù)的每一個字節(jié)都是要花流量的,即需要花錢的。盡管現(xiàn)存的壓縮算法已經(jīng)有幾十上百種,但其中最流行的壓縮算法可能還是 zip。gzip 壓縮算法雖然和 zip 有著相似的名字,但卻是另一種不同的算法。gzip 算法應(yīng)用也相當廣泛,它被 HTTP 三種標準壓縮規(guī)范之一(譯者注:屬于端到端壓縮技術(shù),參見 HTTP 協(xié)議中的數(shù)據(jù)壓縮 )所采用。雖然各種壓縮算法適用于不同場景,但是它們的底層都是基于 DEFLATE 。 DEFLATE是同時使用了 LZ77 算法與 哈夫曼編碼 (Huffman Coding)的一種無損數(shù)據(jù)壓縮算法。
LZ77
DEFLATE基于 LZ77 算法——這是一種用于文本壓縮的無損壓縮技術(shù)。
壓縮
LZ77算法通過使用編碼器或者解碼器中已經(jīng)出現(xiàn)過的相應(yīng)匹配數(shù)據(jù)信息替換當前數(shù)據(jù)從而實現(xiàn)壓縮功能。
此算法并非同時在整個文本中查找重復(fù)的字母,一般會先設(shè)定一個固定大小的搜索緩沖區(qū),例如 20(在真實場景中,這個緩沖區(qū)的大小一般是幾十 kB )。接著在逐一對文本中字母進行編碼時,首先會判斷當前字母是否有出現(xiàn)在前面緩沖區(qū)的 20 個字母中。如果能找到匹配的字母,就記錄下當前字母與找到的字母的偏移量 d ,這樣就完成了一個字母編碼的第一階段。接來下,用當前在編碼字母鄰近的下一個字母與緩沖區(qū)中匹配上字母鄰近的下一字母進行匹配,如果匹配上就繼續(xù)進行下一個字母的匹配,如此循環(huán)往復(fù)直到緩沖區(qū) 20 個字母匹配完或者鄰近的字母未匹配上,就結(jié)束匹配過程。結(jié)束上述過程后,將當前位置匹配上的連續(xù)字母替換成與緩沖區(qū)字母的偏移量以及這段連續(xù)字母的個數(shù) l 。這樣,字母編碼的第二階段就完成了。
讓我們用這個例子來看看它是如何工作的:
ERTOORTORR
首先能想到的最簡單做法就是直接替換第二次出現(xiàn)的 O 為指向第一次出現(xiàn)的 O 的一個標記,或者替換第二次出現(xiàn)的 RTO 為指向第一次出現(xiàn)的 RTO 。
下面更具體地描述一下這個過程,假定我們設(shè)置的緩沖區(qū)大小是 4,把這 4 個長度的緩沖區(qū)看成是一個滑動窗口沿著正文文本向右滑動:
1) [....E]RTOORTORR
2) [...ER]TOORTORR
3) [..ERT]OORTORR
4) [.ERTO]ORTORR
5) [ERTOO]RTORR -> ERTO(1, 1)RTOORR
6) E[RTOOR]TORR -> ERTO(1, 1)(4, 3)RR
7) ERTO[ORTOR]R -> ERTO(1, 1)(4, 3)(3, 1)R
8) ERTOO[RTORR] -> ERTO(1, 1)(4, 3)(3, 1)(1, 1)
滑動窗口隨著隨著編碼的迭代一步步向右移動,前面 4 步中滑動窗口內(nèi)的字母都沒有發(fā)現(xiàn)重復(fù)。到了第 5 步,滑動窗口內(nèi)字母 O 已經(jīng)出現(xiàn)重復(fù)了,然后查看字母 O 右側(cè)的 R 發(fā)現(xiàn)在滑動窗口中匹配字母 O 右側(cè)相鄰的字母并非 R ,便不再繼續(xù)向右進行匹配,將第 2 個 O 替換成 (1, 1) (表示:滑動窗口中匹配的字母離當前字母偏移距離為 1,匹配上的連續(xù)字母長度為 1)。在第 6 步中,滑動窗口中字母 R 與其左邊第 4 個字母匹配上了,繼續(xù)檢查下一個字母 T 的匹配情況,然后發(fā)現(xiàn)滑動窗口中 RT 也匹配上了。然后繼續(xù)下一個字母 O ,在滑動窗口中匹配 RTO 也匹配上了,并且到此為止,因為下一個字母匹配上了。滑動窗口中匹配上的字母與當前字母的偏移距離為 4,同時有連續(xù) 3 個字母匹配上了,所以這里將匹配上的 3 個字母替換成 (4, 3) 。接著在第 7 步中,字母 R 與偏移距離 3 出的字母匹配上,但是接下來的 RR 并未匹配上,在第 8 步中發(fā)現(xiàn)最近的匹配上的 R 的偏移距離為 1。最終整段文本經(jīng)過編碼的結(jié)果如下:
ERTO(1, 1)(4, 3)(3, 1)(1, 1)
解壓
壓縮過的文本其實是由一系列的這種 (d, 1) 標記對和字母組成,標記對無法直接找到相匹配的字母。在解壓過程中,字母保持不變,這種標記對轉(zhuǎn)換為其指向位置的字母。下面看一個解壓的例子:
abc(3, 2)(1, 1)
字母 abc 保持不變,標記對 (3, 2) 表示從當前位置向左移動 3 個單位,然后取出 2 個字母,因此其轉(zhuǎn)換為 ab 。現(xiàn)在原始文本變成了這樣 abcab(1, 1) ,最后的一個標記對表示從當前位置向左移動 1 個單位,然后取出 1 個字母,因此轉(zhuǎn)換為 b 。最終解壓完成的文本為 abcabb 。
哈夫曼編碼
在用 LZ77 消除了文本中重復(fù)的字母后,再使用 哈夫曼編碼 進行第二次壓縮。這種方法用較短的編碼代替較常用的字母,用較長的編碼代替較少用的字母,從而減少了文本的總長度。
讓我們用一個簡單的示例文本來看看它是如何工作的。
壓縮
EFTUPOEERRREOOPRRUTUTTEEE
這個例子中,我們希望能無損地壓縮這段文本。通常一個字母占用 8 字節(jié),所以這段文本總長度有 200 字節(jié)。在這段本文中,我們發(fā)現(xiàn)其中字母 F 只出現(xiàn)了 1 次,而字母 E 出現(xiàn)了 7 次。哈夫曼編碼正是利用了這一特性,通過減少出現(xiàn)頻率高的字母本身的字節(jié)長度,來減少整個文本所占的總長度。
要采用哈夫曼編碼壓縮文章,首先需要統(tǒng)計各個文本中各個字母的出現(xiàn)頻率,上述例子中的字母頻率如下:
頻率:
E: 7, R: 5, T: 4, U: 3, O: 3, P: 2, F: 1
我們需要使用文本中的字母作為葉子節(jié)點來構(gòu)建一顆二叉樹,通過這顆二叉樹來編碼文本中的每一個字母。從出現(xiàn)頻率最小的字母: P 和 F 開始,讓其作為底層的葉子節(jié)點,將其頻率相加的值作為父節(jié)點,這樣便得到了如下的二叉樹:
(3)
/
P(2) F(1)
重復(fù)上面的步驟,依次使用頻率最小的字母: U 和 O 以及 R 和 T ,最后剩下頻率最高的字母 E 先單獨放著。
(6) (9) E(7)
/ /
U(3) O(3) R(5) T(4)
接下來使用上面得到的 4 個二叉樹作為子節(jié)點來創(chuàng)建一顆更大的二叉樹,將上面的二叉樹的根節(jié)點的頻率值遞增排序,優(yōu)先使用根節(jié)點頻率值小的二叉樹作為新的二叉樹子節(jié)點。這里使用 U 和 O 、 R 和 T 這兩組二叉樹組成了如下的一顆二叉樹:
(9)
/
/
(6) (3)
/ /
U O P F
這時候還有 3 顆二叉樹,根節(jié)點分別為:9、9、7(第一個 9 是上一步創(chuàng)建的二叉樹),同樣的,將根節(jié)點頻率值最小的兩個作為子節(jié)點創(chuàng)建新的二叉樹如下:
(16)
/
(9) E
/
/
(6) (3)
/ /
U O P F
復(fù)制代碼
現(xiàn)在剩下一顆將根節(jié)點值為 16 的大二叉樹和根節(jié)點值為 9 葉子節(jié)點為 R 、 T 的二叉樹,將其作為子節(jié)點創(chuàng)建一顆新的二叉樹如下:
(25)
/
/
(16) (9)
/ /
(9) E R T
/
/
(6) (3)
/ /
U O P F
現(xiàn)在我們要做的就是根據(jù)這棵二叉樹來對文本進行編碼。依次從跟節(jié)點訪問各個字母,遇到左分支當成 0,遇到右分支當成 1,按照字母沿著二叉樹訪問路徑的順序所將這些 0、1 連接起來。比如,從根節(jié)點到字母 E 先后需要經(jīng)過 1 次左分支和 1 次右分支,所以字母 E 的編碼為 10 。字母 U 需要經(jīng)過 4 次左分支,其編碼為 1111 ; F 需要經(jīng)過 2 次左分支和 2 次右分支,其編碼為 1100 。可以發(fā)現(xiàn),在這里例子中出現(xiàn)頻率非常高的字母 E 編碼后位數(shù)比出現(xiàn)頻率較少的字母 F 編碼后位數(shù)要少。經(jīng)過這樣的編碼處理,最終壓縮過的文本如下:
10110000111111011110101001010110111011101101010111110011110000101010
這段壓縮后的文本長度只有 68 位,遠比原始的 200 位長度小。
解壓
假如收到這樣一段壓縮過的文本,我們希望能夠解壓它讓其變得可以理解。我們都知道一段未壓縮過的文本中的一個字符占用 8 位,上面說過經(jīng)過哈夫曼編碼壓縮后一個字符的位數(shù)并不是固定 8 位的,所以并不清楚一段數(shù)據(jù)(比如: 011 )是表示 1 個字符、2 個字符或者 3 個字符,因此這段壓縮過的文本將如何解壓呢?
這一步不存在任何奇跡,要準確解壓還需要上面編碼中構(gòu)建的二叉樹。得到這個用于編碼的二叉樹有兩種方案,第一種是其和壓縮后的文本放一起作為原始文本的壓縮結(jié)果,這可能會導(dǎo)致壓縮后的文本比原始文本還要大;第二種方案是使用預(yù)先定義好的二叉樹。我們知道各個字母在英語中的使用頻率,完全可以根據(jù)這個頻率來構(gòu)建上述的二叉樹。使用這種預(yù)先定義的公共字母頻率二叉樹壓縮部分文本的結(jié)果可能比根據(jù)文本內(nèi)容字母頻率二叉樹壓縮的效果差一些,但是這樣不再需要將字母頻率二叉樹保存到壓縮后的文件中。總而言之,這兩種方案各有優(yōu)缺點。
雖然本文沒有深入的分析各種壓縮算法原理的細節(jié)和對應(yīng)的實現(xiàn),但是經(jīng)過上述講解你應(yīng)該已經(jīng)對文本如何被壓縮成 zip 和 gzip 等格式有了大概的認識。希望本文能滿足你對壓縮算法神秘面紗的好奇心:)