简介
无论现在计算机和网络的速度有多快,用户始终要求更快速的体验。为了降低传输数据的容量,我们通常会对数据进行压缩。这就是计算机科学领域一直是研究和发展的焦点的原因。
数据压缩算法有很多,有些是无损的,有些是有损的,但是它们的主要目标都是降低存储空间和传输量。对于两个远距离节点之间的数据传输,这些压缩算法非常有用。也许最直观的例子就是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 |