簡介
無論現在計算機和網路的速度有多快,使用者始終要求更快速的體驗。為了降低傳輸資料的容量,我們通常會對資料進行壓縮。這就是電腦科學領域一直是研究和發展的焦點的原因。
資料壓縮演算法有很多,有些是無損的,有些是有損的,但是它們的主要目標都是降低儲存空間和傳輸量。對於兩個遠距離節點之間的資料傳輸,這些壓縮演算法非常有用。也許最直觀的例子就是web伺服器和瀏覽器之間的資料傳輸。
在過去的幾年裡做了很多關於檔案壓縮的研究,這些研究基於客戶端實現的。這樣的檔案有javascript、css、html和影象。實際上,伺服器和客戶端都具備一些資料壓縮技術,例如GZIP的使用極大地降低了資料傳輸量。此外,還有很多的工具和技巧能夠降低資料大小。
事實上,當檔案在客戶的虛擬機器上執行時,程式設計師不必理會檔案的具體格式如何。如此一來空格、水平製表符和換行符對於檔案上下文的理解沒有任何意義。這就是YUI Compressor、Google Closure Compiler等壓縮工具移除那些符號的原因。當然,為了提高壓縮率檔案還能被進一步壓縮。本篇文章暫不討論這一點,但這表明了資料壓縮演算法的重要性。
如果我們使用一些資料壓縮工具,效果會更好。不幸的是,事實並非如此,壓縮率通常取決於資料本身。很明顯,資料壓縮演算法的選擇主要取決於資料,我們必須首先對資料進行研究。
這裡我將討論“遊程編碼”,它是一種十分簡單的無損資料壓縮演算法,在某些情況下非常有用。
概述
該演算法的實現是用當前資料元素以及該元素連續出現的次數來取代字串中連續出現的資料部分。具體實現我們通過一個字串例項來說明。
1 |
aaaaaaaaaabbbaxxxxyyyzyx |
字串長度為24,我們可以看到字串中有很多的重複部分。使用遊程演算法,我們用較短的字串後加一個計數值來替換遊程物件。
1 |
a10b3a1x4y3z1y1x1 |
此時字串長度為17,大約是初始字串長度的70%。很明顯,這並不是壓縮給定字串的最佳方式。例如當字元僅出現一次時,我們並不需要其後新增“1”。在某些情況下,這種方式會增加初始字串的長度,而這違反了我們的初衷。這樣我們得到的字串如下。
1 |
a10b3ax4y3zyx |
此時字串長度為13,是初始長度的54%!上面例子的一個變種是不對字元保持計數,而是對位置進行計數。這樣原始字串可以被壓縮成下面這樣。
1 |
a0b10a13x14y18z21y22x23 |
使用這兩種方式中的哪一個取決於我們的目標。第二種情況下,我們能夠實現二分查詢的優化。
顯然,這個演算法不僅適用於字串。對陣列也能取得很好的結果。一個典型的例子是伺服器和客戶機之間字元物件(JSON)的傳輸。特別是如果有大量重複資料序列的存在,我們能獲取很好的壓縮結果。
實現
下面的實現是假設我們要使用PHP編寫程式對字串進行壓縮。但是這個演算法本質上並沒有限制我們只能壓縮字串。正如我前面所說,只要略微修改,我們就能將其用於其他資料結構。理解遊程演算法適用於大量重複元素序列非常重要,不管是字元元素還是陣列元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
$message = 'aaaaaaaaaabbbaxxxxyyyzyx'; function run_length_encode($msg) { $i = $j = 0; $prev = ''; $output = ''; while ($msg[$i]) { if ($msg[$i] != $prev) { if ($i) $output .= $j; $output .= $msg[$i]; $prev = $msg[$i]; $j = 0; } $j++; $i++; } $output .= $j; return $output; } // a10b3a1x4y3z1y1x1 echo run_length_encode($message); |
略微優化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$message = 'aaaaaaaaaabbbaxxxxyyyzyx'; function run_length_encode($msg) { $i = $j = 0; $prev = ''; $output = ''; while ($msg[$i]) { if ($msg[$i] != $prev) { if ($i && $j > 1) $output .= $j; $output .= $msg[$i]; $prev = $msg[$i]; $j = 0; } $j++; $i++; } if ($j > 1) $output .= $j; return $output; } // a10b3ax4y3zyx echo run_length_encode($message); |
最後一個小變化——現在我們儲存字元位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$message = 'aaaaaaaaaabbbaxxxxyyyzyx'; function run_length_encode($msg) { $i = 0; $prev = ''; $output = ''; while ($msg[$i]) { if ($msg[$i] != $prev) { $output .= $msg[$i] . $i; $prev = $msg[$i]; } $i++; } return $output; } // a0b10a13x14y18z21y22x23 echo run_length_encode($message); |
複雜性和資料壓縮
我們習慣使用時間複雜度來衡量時間,通常希望能找到最快的實現方式,比如查詢演算法。在這裡快速壓縮資料並不特別重要,重要的是儘可能的無失真壓縮,使得輸出儘可能的小。遊程編碼的優點在於該演算法容易實現。
應用程式
在很多情況下,我們可以使用遊程編碼。它常用於影象壓縮,特別是用於黑白圖片處理時是效果非常好。這裡,我將介紹上面提及的另一種應用情況。假設我們要使用JSON將大量陣列資料傳給我們的Ajax程式。假設這些傳輸資料是一些年份,例如電影首映的年份。一年內有很多電影首映,雖然資料已被排序,但實際上我們沒有得到任何好處。更要命的是有大量的資料序列。這裡我們可以使用遊程編碼。
1 2 3 4 5 6 7 8 9 10 11 |
$data = array( 0 => 1991, 1 => 1991, ... 2223 => 1991, 2224 => 1992, ... 19298 => 1995, 19299 => 1996, ... ); |
正如你看到的,傳輸整個陣列將會是一個噩夢,特別是如果網路的速度很慢。最好對資料進行壓縮(例如使用PHP的json_encode)。
1 2 |
// {"0":1991,"1":1991, ..., "2223":1991,"2224":1992, ..., "19298":1995,"19299":1996, ...} echo json_encode($data); |
執行遊程編碼之後,我們得到結果像以下陣列一樣(注意這些只是樣本資料,最佳儲存資料格式取決於你)。
1 2 3 4 5 6 |
$data = array( 0 => array(1991, 2224), 1 => array(1992, 3948), 2 => array(1995, 2398), 3 => array(1996, 3489), ); |
JSON輸出
1 2 |
// [[1991,2224],[1992,3948],[1995,2398],[1996,3489]] echo json_encode($data); |
注意如果是已排序資料,我們能夠獲得更好的壓縮結果!!!這種方式能夠用於影象,圖形或者地圖座標的壓縮。
這是資料壓縮在日常工作中有用的唯一例子。儘管伺服器和客戶機之間的通訊可以優化和壓縮,我們能夠改善它。換句話說我們不能夠保證對方是否支援壓縮。
那麼,相應的客戶端必須對資料進行解壓,這個過程很緩慢。在第一種情況下,我們只有時間去傳輸,如下面的流程圖所示。
未壓縮資料傳輸時間!
第二種情況,我們應該累加壓縮,傳輸和解壓的時間。
壓縮資料傳輸時間!
所有這些都很重要,但總的來說資料壓縮在我們日常生活中的多數情況下都很方便。