幾種壓縮演算法原理介紹
1 RLE
RLE 又叫 Run Length Encoding ,是一個針對無失真壓縮的非常簡單的演算法。它用重複位元組和重複的次數來簡單描述來代替重複的位元組。儘管簡單並且對於通常的壓縮非常低效,但它有的時候卻非常有用(例如, JPEG 就使用它)。
1.1 原理
圖 2.1 顯示了一個如何使用 RLE 演算法來對一個數據流編碼的例子,其中出現六次的符號‘ 93 ’已經用 3 個位元組來代替:一個標記位元組(‘ 0 ’在本例中)重複的次數(‘ 6 ’)和符號本身(‘ 93 ’)。
RLE 解碼器遇到符號‘ 0 ’ 的時候,它表明後面的兩個位元組決定了需要輸出哪個符號以及輸出多少次。
1.2 實現
RLE 可以使用很多不同的方法。基本壓縮庫中詳細實現的方式是非常有效的一個。一個特殊的標記位元組用來指示重複節的開始,而不是對於重複非重複節都 coding run 。
因此非重複節可以有任意長度而不被控制位元組打斷,除非指定的標記位元組出現在非重複節(頂多以兩個位元組來編碼)的稀有情況下。為了最優化效率,標記位元組應該是輸入流中最少出現的符號(或許就不存在)。
重複 runs 能夠在 32768 位元組的時候運轉。少於 129 位元組的要求 3 個位元組編碼(標記 + 次數 + 符號),而大雨 128 位元組要求四個位元組(標記 + 次數的高 4 位 |0x80+ 次數的低 4 位)。這是通常所有采用的壓縮的做法,並且也是相比較三個位元組固定編碼(允許使用 3
在這種模式下,最壞的壓縮結果是:
輸出大小 =257/256* 輸入大小 +1
2 哈夫曼
哈夫曼編碼是無失真壓縮當中最好的方法。它使用預先二進位制描述來替換每個符號,長度由特殊符號出現的頻率決定。常見的符號需要很少的位來表示,而不常見的符號需要很多為來表示。
哈夫曼演算法在改變任何符號二進位制編碼引起少量密集表現方面是最佳的。然而,它並不處理符號的順序和重複或序號的序列。
2.1 原理
我不打算探究哈夫曼編碼的所有實際的細節,但基本的原理是為每個符號找到新的二進位制表示,從而通常符號使用很少的位,不常見的符號使用較多的位。
簡短的說,這個問題的解決方案是為了查詢每個符號的通用程度,我們建立一個未壓縮資料的柱狀圖;通過遞迴拆分這個柱狀圖為兩部分來建立一個二叉樹,每個遞迴的一半應該和另一半具有同樣的權(權是 ∑ N K =1 符號數 k , N 是分之中符號的數量,符號數 k 是符號 k出現的次數 )
這棵樹有兩個目的:
1. 編碼器使用這棵樹來找到每個符號最優的表示方法
2. 解碼器使用這棵樹唯一的標識在壓縮流中每個編碼的開始和結束,其通過在讀壓縮資料位的時候自頂向底的遍歷樹,選擇基於資料流中的每個獨立位的分支,一旦一個到達葉子節點,解碼器知道一個完整的編碼已經讀出來了。
我們來看一個例子會讓我們更清楚。圖 2.2 顯示了一個 10 個位元組的未壓縮的資料。
根據符號頻率,哈夫曼編碼器生成哈夫曼樹(圖 2.4 )和相應的編碼表示(圖 2.3 )。
你可以看到,常見的符號接近根,因此只要少數位來表示。於是最終的壓縮資料流如圖 2.5 所示。
壓縮後的資料流是 24 位(三個位元組),原來是 80 位( 10 個位元組)。當然,我應該儲存哈夫曼樹,這樣解碼器就能夠解碼出對應的壓縮流了,這就使得該例子中的真正資料流比輸入的流資料量大。這是相對較短的資料上的副作用。對於大資料量來說,上面的哈夫曼樹就不佔太多比例了。
解碼的時候,從上到下遍歷樹,為壓縮的流選擇從左 / 右分支,每次碰到一個葉子節點的時候,就可以將對應的位元組寫到解壓輸出流中,然後再從根開始遍歷。
2.2 實現
哈夫曼編碼器可以在基本壓縮庫中找到,其是非常直接的實現。
這個實現的基本缺陷是:
1. 慢位流實現
2. 相當慢的解碼(比編碼慢)
3. 最大的樹深度是 32 (編碼器在任何超過 32 位大小的時候退出)。如果我不是搞錯的話,這是不可能的,除非輸出的資料大於 2 32位元組。
另一方面,這個實現有幾個優點:
1. 哈夫曼樹以一個緊密的形式每個符號要求 12 位(對於 8 位的符號)的方式儲存,這意味著最大的頭為 384 。
2. 編碼相當容易理解
哈夫曼編碼在資料有噪音的情況(不是有規律的,例如 RLE )下非常好,這中情況下大多數基於字典方式的編碼器都有問題。
3 Rice
對於由大 word (例如: 16 或 32 位)組成的資料和教低的資料值, Rice 編碼能夠獲得較好的壓縮比。音訊和高動態變化的影象都是這種型別的資料,它們被某種預言預處理過(例如 delta 相鄰的取樣)。
儘管哈夫曼編碼處理這種資料是最優的,卻由於幾個原因而不適合處理這種資料(例如: 32 位大小要求 16GB 的柱狀圖緩衝區來進行哈夫曼樹編碼)。因此一個比較動態的方式更適合由大 word 組成的資料。
3.1 原理
Rice 編碼背後的基本思想是儘可能的用較少的位來儲存多個字(正像使用哈夫曼編碼一樣)。實際上,有人可能想到 Rice 是靜態的哈夫曼編碼(例如,編碼不是由實際資料內容的統計資訊決定,而是由小的值比高的值常見的假定決定)。
編碼非常簡單:將值 X 用 X 個‘ 1 ’位之後跟一個 0 位來表示。
3.2 實現
在基本壓縮庫針對 Rice 做了許多優化:
1. 每個字最沒有意義的位被儲存為 k 和最有意義的 N-k 位用 Rice 編碼。 K 作為先前流中少許取樣的位平均數。這是通常最好使用Rice 編碼的方法,隱藏噪音且對於動態變化的範圍並不導致非常長的 Rice 編碼。
2. 如果 rice 編碼比固定的開端長, T ,一個可選的編碼:輸出 T 個‘ 1 ’位,緊跟( log2(X-T) )個‘ 1 ’和一個‘ 0 ’位,接著是 X-T (最沒有意義的 (log2(X-T))-1 位)。這對於大值來說都是比較高效的程式碼並且阻止可笑的長 Rice 編碼(最壞的情況,對於一個 32 位 word 單個 Rice 編碼可能變成 2 32 位或 512MB )。
如果開端是 4 ,下面是結果編碼表:
X |
bin |
Rice |
Thresholded |
Rice |
0 |
00000 |
0 |
0 |
|
1 |
00001 |
10 |
10 |
|
2 |
00010 |
110 |
110 |
|
3 |
00011 |
1110 |
1110 |
|
4 |
00100 |
11110 |
11110 |
|
5 |
00101 |
111110 |
111110 |
|
6 |
00110 |
1111110 |
11111100 |
+1 |
7 |
00111 |
11111110 |
11111101 |
|
8 |
01000 |
111111110 |
1111111000 |
+1 |
9 |
01001 |
1111111110 |
1111111001 |
|
10 |
01010 |
11111111110 |
1111111010 |
-1 |
11 |
01011 |
111111111110 |
1111111011 |
-2 |
12 |
01100 |
1111111111110 |
111111110000 |
|
13 |
01101 |
11111111111110 |
111111110001 |
-1 |
14 |
01110 |
111111111111110 |
111111110010 |
-2 |
15 |
01111 |
1111111111111110 |
111111110011 |
-3 |
16 |
10000 |
11111111111111110 |
111111110100 |
-4 |
17 |
10001 |
111111111111111110 |
111111110101 |
-5 |
18 |
10010 |
1111111111111111110 |
111111110110 |
-6 |
19 |
10011 |
11111111111111111110 |
111111110111 |
-7 |
20 |
10100 |
111111111111111111110 |
11111111100000 |
-5 |
就像你看到的一樣,在這個實現中使用 threshold 方法僅僅兩個編碼導致一個最壞的情況;剩下的編碼產生比標準 Rice 編碼還要短的編碼。
3. 最壞的情況,輸出。
4 Lempel-Ziv (LZ77)
Lempel-Ziv 壓縮模式有許多不同的變數。基本壓縮庫有清晰的 LZ77 演算法的實現( Lempel-Ziv , 1977 ),執行的很好,原始碼也非常容易理解。
LZ 編碼器能用來通用目標的壓縮,特別對於文字執行的很好。它也在 RLE 和哈夫曼編碼器( RLE , LZ ,哈夫曼)中使用來大多數情況下獲得更多的壓縮。
4.1 原理
在 LZ 壓縮演算法的背後是使用 RLE 演算法用先前出現的相同位元組序列的引用來替代。
簡單的講, LZ 演算法被認為是字串匹配的演算法。例如:在一段文字中某字串經常出現,並且可以通過前面文字中出現的字串指標來表示。當然這個想法的前提是指標應該比字串本身要短。
例如,在上一段短語“字串”經常出現,可以將除第一個字串之外的所有用第一個字串引用來表示從而節省一些空間。
一個字串引用通過下面的方式來表示:
1. 唯一的標記
2. 偏移數量
3. 字串長度
由編碼的模式決定引用是一個固定的或變動的長度。後面的情況經常是首選,因為它允許編碼器用引用的大小來交換字串的大小(例如,如果字串相當長,增加引用的長度可能是值得的)。
4.2 實現
使用 LZ77 的一個問題是由於演算法需要字串匹配,對於每個輸入流的單個位元組,每個流中此位元組前面的哪個位元組都必須被作為字串的開始從而儘可能的進行字串匹配,這意味著演算法非常慢。
另一個問題是為了最優化壓縮而調整字串引用的表示形式並不容易。例如,必須決定是否所有的引用和非壓縮位元組應該在壓縮流中的位元組邊界發生。
基本壓縮庫使用一個清晰的實現來保證所有的符號和引用是位元組對齊的,因此犧牲了壓縮比率,並且字串匹配程式並不是最優化的(沒有快取、歷史緩衝區或提高速度的小技巧),這意味著程式非常慢。
另一方面,解壓縮程式非常簡單。
一個提高 LZ77 速度的試驗已經進行了,這個試驗中使用陣列索引來加速字串匹配的過程。然而,它還是比通常的壓縮程式慢。