上一篇講了LZW編碼,本篇討論另外一種不一樣的編碼算法,算數編碼。和哈夫曼編碼同樣,算數編碼是熵編碼的一種,是基於數據中字符出現的機率,給不一樣字符以不一樣的編碼。本文也會對這兩種編碼方式的類似和不一樣點進行比較。算法
算數編碼的原理我我的感受其實並不太容易用三言兩語直觀地表達出來,其背後的數學思想則更是深入。固然在這裏我仍是儘量地將它表述,並着重結合例子來詳細講解它的原理。編程
簡單來講,算數編碼作了這樣一件事情:segmentfault
$$low = low + (high - low) * L \\\ high = low + (high - low) * H$$優化
乍一看這些數學和公式很難給人直觀理解,因此咱們仍是看例子。例若有一段很是簡單的原始數據:編碼
ARBER
統計它們出現的次數和機率:spa
Symbol | Times | P |
---|---|---|
A | 1 | 0.2 |
B | 1 | 0.2 |
E | 1 | 0.2 |
R | 2 | 0.4 |
將這幾個字符的區間在 [0,1) 上按照機率大小連續一字排開,咱們獲得一個劃分好的 [0,1)區間:debug
開始編碼,初始區間是 [0,1)。注意這裏又用了區間這個詞,不過這個區間不一樣於上面表明各個字符的機率區間 [0,1)。這裏咱們能夠稱之爲編碼區間,這個區間是會變化的,確切來講是不斷變小。咱們將編碼過程用下圖完整地表示出來:code
拆解開來一步一步看:blog
$$low = low + (high - low)* L=0\\\quad high = low + (high - low)* H=0.2$$圖片
$$low = low + (high - low)* L=0.12\\high = low + (high - low)* H=0.2$$
$$low = low + (high - low)* L=0.136\\\ \ high = low + (high - low)* H=0.152$$
上面的圖已經很是清楚地展示了算數編碼的思想,咱們能夠看到一個不斷變化的小數編碼區間。每次編碼一個字符,就在現有的編碼區間上,按照機率比例取出這個字符對應的子區間。例如一開始A落在0到0.2上,所以編碼區間縮小爲 [0,0.2),第二個字符是R,則在 [0,0.2)上按比例取出R對應的子區間 [0.12,0.2),以此類推。每次獲得的新的區間都能精確無誤地肯定當前字符,而且保留了以前全部字符的信息,由於新的編碼區間永遠是在以前的子區間。最後咱們會獲得一個長長的小數,這個小數即神奇地包含了全部的原始數據,不得不說這真是一種很是精彩的思想。
若是你理解了編碼的原理,則解碼的方法顯而易見,就是編碼過程的逆推。從編碼獲得的小數開始,不斷地尋找小數落在了哪一個機率區間,就能將原來的字符一個個地找出來。例如獲得的小數是0.14432,則第一個字符顯然是A,由於它落在了 [0,0.2)上,接下來再看0.14432落在了 [0,0.2)區間的哪個相對子區間,發現是 [0.6,1), 就能找到第二個字符是R,依此類推。在這裏就不贅述解碼的具體步驟了。
算數編碼的原理簡潔而又精緻,理解起來也不很困難,但具體的編程實現其實並非想象的那麼容易,主要是由於小數的問題。雖然咱們在講解原理時很是容易地不斷計算,但若是真的用編程實現,例如C++,而且不借助第三方數學庫,咱們不可能簡單地用一個double類型去表示和計算這個小數,由於數據和編碼能夠任意長,小數也會到達小數點後成千上萬位。
怎麼辦?其實也很容易,小數點是能夠挪動的。給定一個編碼區間,例如從上面例子裏最後的區間 [0.14432,0.1456)開始,假定還有新的數據進來要繼續編碼。現有區間小數點後的高位0.14實際上是肯定的,那麼實際上14已經能夠輸出了,小數點能夠向後移動兩位,區間變成 [0.432,0.56),在這個區間上繼續計算後面的子區間。這樣編碼區間永遠保持在一個有限的精度要求上。
上述是基於十進制的,實際數字是用二進制表示的,固然原理是同樣的,用十進制只是爲了表述方便。算數編碼/解碼的編程實現其實還有不少tricky的東西和corner case,我當時寫的時候debug了很久,所以我也建議讀者本身動手寫一遍,相信會有收穫。
這實際上是我想重點探討的一個部分。在這裏默認你已經懂哈夫曼編碼,由於這是一種最基本的壓縮編碼,算法課都會講。哈夫曼編碼和算數編碼都屬於熵編碼,仔細分析它們的原理,這兩種編碼是十分相似的,但也有微妙的不一樣之處,這也致使算數編碼的壓縮率一般比哈夫曼編碼略高,這些咱們都會加以探討。
不過咱們首先要了解什麼是熵編碼,熵是借用了物理上的一個概念,簡單來講表示的是物質的無序度,混亂度。信息學裏的熵表示數據的無序度,熵越高,則包含的信息越多。其實這個概念仍是很抽象,舉個最簡單的例子,假如一段文字全是字母A,則它的熵就是0,由於根本沒有任何變化。若是有一半A一半B,則它能夠包含的信息就多了,熵也就高。若是有90%的A和10%的B,則熵比剛纔的一半A一半B要低,由於大多數字母都是A。
熵編碼就是根據數據中不一樣字符出現的機率,用不一樣長度的編碼來表示不一樣字符。出現機率越高的字符,則用越短的編碼表示;出現機率地的字符,能夠用比較長的編碼表示。這種思想在哈夫曼編碼中其實已經很清晰地體現出來了。那麼給定一段數據,用二進制表示,最少須要多少bit才能編碼呢?或者說平均每一個字符須要幾個bit表示?其實這就是信息熵的概念,若是從數學上理論分析,香農天才地給出了以下公式:
$$ H(x) = -\sum_{i=1}^{n}p(x_{i})\log_{2}p(x_{i})$$
其中 p (xi) 表示每一個字符出現的機率。log對數計算的是每個字符須要多少bit表示,對它們進行機率加權求和,能夠理解爲是求數學指望值,最後的結果即表示最少平均每一個字符須要多少bit表示,即信息熵,它給出了編碼率的極限。
在這裏咱們不對信息熵和背後的理論作過多分析,只是爲了幫助理解算數編碼和哈夫曼編碼的本質思想。爲了比較這兩種編碼的異同點,咱們首先回顧哈夫曼編碼,例如給定一段數據,統計裏面字符的出現次數,生成哈夫曼樹,咱們能夠獲得字符編碼集:
Symbol | Times | Encoding |
---|---|---|
a | 3 | 00 |
b | 3 | 01 |
c | 2 | 10 |
d | 1 | 110 |
e | 2 | 111 |
仔細觀察編碼所表示的小數,從0.0到0.111,其實就是構成了算數編碼中的各個機率區間,而且機率越大,所用的bit數越少,區間則反而越大。若是用哈夫曼編碼一段數據abcde,則獲得:
00 01 10 110 111
若是點上小數點,把它也當作一個小數,其實和算數編碼的形式很相似,不斷地讀入字符,找到它應該落在當前區間的哪個子區間,整個編碼過程造成一個不斷收攏變小的區間。
由此咱們能夠看到這兩種編碼,或者說熵編碼的本質。機率越小的字符,用更多的bit去表示,這反映到機率區間上就是,機率小的字符所對應的區間也小,所以這個區間的上下邊際值的差值越小,爲了惟一肯定當前這個區間,則須要更多的數字去表示它。咱們仍以十進制來講明,例如大區間0.2到0.3,咱們須要0.2來肯定,一位足以表示;但若是是小的區間0.11112到0.11113,則須要0.11112才能肯定這個區間,編碼時就須要5位才能將這個字符肯定。其實編碼一個字符須要的bit數就等於 -log ( p ),這裏是十進制,因此log應以10爲底,在二進制下以2爲底,也就是香農公式裏的形式。
-------
哈夫曼編碼的不一樣之處就在於,它所劃分出來的子區間並非嚴格按照機率的大小等比例劃分的。例如上面的d和e,機率實際上是不一樣的,但卻獲得了相同的子區間大小0.125;再例如c,和d,e構成的子樹,c應該比d,e的區間之和要小,但實際上它們是同樣的都是0.25。咱們能夠將哈夫曼編碼和算術編碼在這個例子裏的機率區間作個對比:
這說明哈夫曼編碼能夠看做是對算數編碼的一種近似,它並非完美地呈現原始數據中字符的機率分佈。也正是由於這一點微小的誤差,使得哈夫曼編碼的壓縮率一般比算數編碼略低一些。或者說,算數編碼能更逼近香農給出的理論熵值。
爲了更好地理解這一點,咱們舉一個最簡單的例子,好比有一段數據,A出現的機率是0.8,B出現的機率是0.2,如今要編碼數據:
AAA...........AAABBB...BBB (800個A,200個B)
若是用哈夫曼編碼,顯然A會被編成0,B會被編成1,若是表示在機率區間上,則A是 [0, 0.5),B是 [0.5, 1)。爲了編碼800個A和200個B,哈夫曼會用到800個0,而後跟200個1:
0.000......000111...111 (800個0,200個1)
在編碼800個A的過程當中,若是咱們試圖去觀察編碼區間的變化,它是不斷地以0.5進行指數遞減,最後造成一個 [0, 0.5^800) 的編碼區間,而後開始B的編碼。
可是若是是算數編碼呢?由於A的機率是0.8,因此算數編碼會使用區間 [0, 0.8) 來編碼A,800個A則會造成一個區間 [0, 0.8^800),顯然這個區間比 [0, 0.5^800) 大得多,也就是說800個A,哈夫曼編碼用了整整800個0,而算數編碼只須要不到800個0,更少的bit數就能表示。
固然對B而言,哈夫曼編碼的區間大小是0.5,算數編碼是0.2,算數編碼會用到更多的bit數,但由於B的出現機率比A小得多,整體而言,算術編碼」犧牲「B而「照顧」A,最終平均須要的bit數就會比哈夫曼編碼少。而哈夫曼編碼,因爲其算法的特色,只能「不合理」地使用0.5和0.5的機率分佈。這樣的結果是,出現機率很高的A,和出現機率低的B使用了相同的編碼長度1。二者相比,顯然算術編碼能更好地實現熵編碼的思想。
---
從另一個角度來看,在哈夫曼編碼下,整個bit流能夠清晰地分割出原始字符串:
而在算數編碼下,每個字符並非嚴格地對應整數個bit的,有些字符與字符之間的邊界多是模糊的,或者說是重疊的,因此它的壓縮率會略高:
固然這樣的解釋並不徹底嚴格,若是必定要究其緣由,那必須從數學上進行證實,算數編碼的區間分割是更接近於信息熵的結果的,這就不在本文的討論範圍了。在這裏我只是試圖用更直觀地方式解釋算數編碼和哈夫曼編碼之間微妙的區別,以及它們同屬於熵編碼的本質性原理。
算數編碼的講解就到這裏。說實話我很是喜歡這種編碼以及它所蘊含的思想,那種觸及了數學本質的美感。若是說哈夫曼編碼只是直觀地基於機率,優化了字符編碼長度實現壓縮,那麼算術編碼是真正地從信息熵的本質,展示了信息到底是以怎樣的形式進行無損壓縮,以及它的極限是什麼。在討論算術編碼時,老是要說起哈夫曼編碼,並與之進行比較,咱們必須認識到它們之間的關係,才能對熵編碼有一個完整的理解。