算法系列之八:RLE行程长度压缩算法

 RLE(Run Length Encoding)行程长度压缩算法(也称游程长度压缩算法),是最早出现、也是最简单的无损数据压缩算法。RLE算法的基本思路是把数据按照线性序列分成两种情况:一种是连续的重复数据块,另一种是连续的不重复数据块。对于第一种情况,对连续的重复数据块进行压缩,压缩方法就是用一个表示块数的属性加上一个数据块代表原来连续的若干块数据。对于第二种情况,RLE算法有两种处理方法,一种处理方法是用和第一种情况一样的方法处理连续的不重复数据块,仅仅是表示块数的属性总是1;另一种处理方法是不对数据进行任何处理,直接将原始数据作为压缩后的数据。

        为了更直观的说明RLE算法,下面就用示例数据就对RLE算法进行演示。首先是第一种情况,原始数据有5个连续相同的数据块组成:

 

[block] [block] [block] [block] [block]

 

则压缩后的数据就是:

 

[5] [block]

 

接着是第二种情况,原始数据由连续的不重复数据块组成:

 

[block1] [block2] [block3] [block4] [block5]

 

按照第一种处理方法,最后的压缩数据就如以下情形:

 

[1][block1] [1][block2] [1][block3] [1][block4] [1][block5]

 

如果按照第二种处理方法,最后的数据和原始数据一样:

 

[block1] [block2] [block3] [block4] [block5]

 

数据块block的长度可以是任意长度,数据块长度越长则连续重复的概率就越低,压缩的优势就体现不出来,因此,大多数RLE算法的实现都使用一个字节作为数据块长度。

        接下来本文就介绍几种RLE算法的实现,首先是最简单的一种算法实现,这种算法实现对连续的不重复字节采用和重复字节一样的处理方法,就是在每个字节前增加一个值是1的连续块数属性(一个字节)。采用这种处理方法的首先好处是压缩和解压缩算法简单,可以用相同的模式处理两种情况的压缩数据,缺点就是当原始数据重复率比较低时,压缩后的数据长度会超过原始数据的长度,起不到压缩的作用,最糟糕的情况就是所有数据块没有连续重复的情况,压缩后的数据反而膨胀一倍。压缩的过程是这样的,线性扫描原始数据,如果某一个字节后面有重复的字节,则增加重复计数,然后继续向后扫描,直到找到一个不重复的字节,然后将块数和这个字节的数据依次写入压缩数据,然后从新的开始字节继续扫描直到原书数据结束。算法中需要注意的一点就是块数属性是用一个字节存储的,因此最大值就是255,当连续的相同数据超过255个字节时,就从第255个字节处断开,将第256个字节以及256字节后面的数据当成新的数据处理。这种RLE压缩算法的C语言实现如下:

 6 int Rle_Encode_N(unsigned char *inbuf, int inSize, unsigned char *outbuf, int onuBufSize)

 7 {

 8     unsigned char *src = inbuf;

 9     int i;

10     int encSize = 0;

11 

12     while(src < (inbuf + inSize))

13     {

14         if((encSize + 2) > onuBufSize) /*输出缓冲区空间不够了*/

15         {

16             return 1;

17         }

18         unsigned char value = *src++;

19         i = 1;

20         while((*src == value) && (i < 255))

21         {

22             src++;

23             i++;

24         }

25         outbuf[encSize++] = i;

26         outbuf[encSize++] = value;

27     }

28 

29     return encSize;

30 }

 

        对于字符串“AAABBBBBCD”,用这种RLE算法Rle_Encode_N()函数压缩后的数据就是:0x03,0x41,0x05,0x42,0x01,0x43,0x01,0x44共8个字节,比原始长度10个字节少了2个字节,实现了数据长度的压缩。

        解压缩的过程也很简单,就是定为到第一个块数属性字节位置,根据块数属性的值n,连续向解压缩缓冲区还原n个原始数据,原始数据就是块数属性后面一个字节的数据,然后偏移到下一个块数属性字节位置继续上述处理,直到压缩数据结束。解压缩算法的C语言实现如下:

32 int Rle_Decode_N(unsigned char *inbuf, int inSize, unsigned char *outbuf, int onuBufSize)

33 {

34     unsigned char *src = inbuf;

35     int i;

36     int decSize = 0;

37 

38     while(src < (inbuf + inSize))

39     {

40         int count = *src++;

41         if((decSize + count) > onuBufSize) /*输出缓冲区空间不够了*/

42         {

43             return 1;

44         }

45         unsigned char value = *src++;

46         for(i = 0; i < count; i++)

47         {

48             outbuf[decSize++] = value;

49         }

50     }

51 

52     return decSize;

53 }

        这个最简单的的RLE算法存在着一个致命问题,就是对连续出现的不重复数据,会因为插入太多块数属性字节而膨胀,如果一段数据连续出现重复数据的情况很少,大多数是连续不重复的数据,则使用上面的算法会导致数据没有被压缩,反而增大了,在极端的情况下,会因为插入的块数属性字节而导致数据膨胀一倍。针对这种情况,人们对算法进行了改进,改进的关键点就是对连续出现的不重复数据,不再简单插入块数属性,而是将其直接作为压缩后的数据处理。这样会遇到一个问题,就是解码的过程中,如何判断一个数据是块数属性数据还是正常的数据?方法就是对块数属性字节设置标志位,将块数属性字节的高两位作为标志位,当这两个标志位是连续的两个1时,则判断此字节数据是块数属性字节,它的剩下的6个位就是重复块数。如果这两个标志位不是连续的1,则认为这个字节是正常数据。现在的问题是,如果用户数据中出现了与标志位冲突的数据怎么办?其实没有太好的方法,解决这个问题的方案就是插入值是1的块数属性字节。往好的方面想,这个改进对于数据不超过192(0xC2)的原始数据是非常有效的,著名的图像文件格式PCX格式,就是使用了这种改进的压缩算法,效果还是不错的。下面就来看看一个改进的压缩算法实现:

55 int Rle_Encode_P(unsigned char *inbuf, int inSize, unsigned char *outbuf, int onuBufSize)

56 {

57     unsigned char *src = inbuf;

58     int i;

59     int encSize = 0;

60 

61     while(src < (inbuf + inSize))

62     {

63         unsigned char value = *src++;

64         i = 1;

65         while((*src == value) && (i < 63))

66         {

67             src++;

68             i++;

69         }

70 

71         if((encSize + i + 1) > onuBufSize) /*输出缓冲区空间不够了*/

72         {

73             return 1;

74         }

75         if(i > 1)

76         {

77             outbuf[encSize++] = i | 0xC0;

78             outbuf[encSize++] = value;

79         }

80         else

81         {

82             if((value & 0xC0) == 0xC0)

83             {

84                 outbuf[encSize++] = 0xC1;

85             }

86             outbuf[encSize++] = value;

87         }

88     }

89 

90     return encSize;

91 }

        对于字符串“AAABBBBBCD”,用这种RLE算法的Rle_Encode_P()函数压缩后的数据就是:0xC3,0x41,0xC5,0x42,0x43,0x44共6个字节,比原始长度10个字节少了4个字节,对于原始数据都小于192的数据能够有效地抑制因插入块数属性过多导致的数据膨胀。

        使用这种算法解压缩,需要判断当前数据是否有块属性标志,如果有则从低6位bit中去到重复数据的块数n,然后将下一个字节的数据重复复制n次。如果当前数据没有块属性标志,则直接使用当前数据,具体实现的C代码见Rle_Decode_P()函数:

 93 int Rle_Decode_P(unsigned char *inbuf, int inSize, unsigned char *outbuf, int onuBufSize)

 94 {

 95     unsigned char *src = inbuf;

 96     int i;

 97     int decSize = 0;

 98     int count = 0;

 99 

100     while(src < (inbuf + inSize))

101     {

102         unsigned char value = *src++;

103         int count = 1;

104         if((value & 0xC0) == 0xC0) /*是否有块属性标志*/

105         {

106             count = value & 0x3F; /*低位是count*/

107             value = *src++;

108         }

109         else

110         {

111             count = 1;

112         }

113         if((decSize + count) > onuBufSize) /*输出缓冲区空间不够了*/

114         {

115             return 1;

116         }

117         for(i = 0; i < count; i++)

118         {

119             outbuf[decSize++] = value;

120         }

121     }

122 

123     return decSize;

124 }

        上述优化后的RLE算法,在原始数据普遍大于192(0xC0)的情况下,其优化效果相对于优化前的算法没有明显改善。原因在于,原始的RLE算法和改进后的RLE算法对于连续出现的不重复数据的处理方式都是一个一个处理的,没有把不重复数据作为一个整体进行处理。现在考虑再对原始的RLE算法进行优化,主要优化思想就是对连续的不重复数据进行整理处理,用一个和处理连续重复数据一样的标志,标识后面的数据是长度为n的连续不重复数据。这样的标志字节就相当于是数据块的块头部分,描述后面跟的数据类型以及数据长度。由于标志是始终存在于数据块的前面,因此就不需要区分标志字节和原始数据,也就是说,第一个改进算法中用“高两位是连续的1”的方式区分标志字节和数据都是没有必要的,唯一需要区分的是后面跟的数据类型。区分的方法就是对标志字节的8个bit进行分工,用高位一个bit表示后面跟的数据类型,如果这个bit是1则表示后面跟的是连续重复的数据,如果这个bit是0则表示后面跟的是连续不重复的数据。标志字节的低7位bit存储一个数字(最大值是127),对于连续重复数据,这个数字表示需要重复的次数,对于连续不重复数据,这个数字表示连续不重复数据块的长度。需要注意的是,只有重复次数超过2的数据才被认为是连续重复数据,因为如果数据的重复次数是2,压缩后加上标志字节后总的长度没有变化,因此没有必要处理。

        下面根据上述优化思想进行算法设计,首先是压缩算法。在这种情况下,压缩算法就比前两种RLE压缩算法复杂一些,就是要能识别连续的重复数据和连续的不重复数据。首先设置搜索起始位置,算法开始时这个搜索起始位置就是原始数据的第一个字节。每次搜索就是从起始位置开始向后搜索比较数据,根据搜索比较结果,一种情况就是后面数据重复且数据长度超过2,则设置连续重复数据的标志,然后继续向后查找,直到找到第一个与之不相同的数据为止,将这个位置记为下次搜索的起始位置,根据位置差计算重复次数,连重复标志和重复次数以及原始数据一起写入压缩数据;另一种情况是后面的数据都没有连续重复的,则继续向后查找,直到找到连续重复的数据,然后设置不重复数据标志,将新位置记为下次搜索的起始位置,最后将标志字节写入压缩数据并将原始数据复制到压缩数据。从新的搜索起始位置重复上面的过程,直到原始数据结束。函数Rle_Encode_O()就是上述算法的C语言实现(只有算法的主体部分):

185 int Rle_Encode_O(unsigned char *inbuf, int inSize, unsigned char *outbuf, int onuBufSize)

186 {

187     unsigned char *src = inbuf;

188     int i;

189     int encSize = 0;

190     int srcLeft = inSize;

191 

192     while(srcLeft > 0)

193     {

194         int count = 0;

195         if(IsRepetitionStart(src, srcLeft)) /*是否连续三个字节数据相同?*/

196         {

197             if((encSize + 2) > onuBufSize) /*输出缓冲区空间不够了*/

198             {

199                 return 1;

200             }

201             count = GetRepetitionCount(src, srcLeft);

202             outbuf[encSize++] = count | 0x80;

203             outbuf[encSize++] = *src;

204             src += count;

205             srcLeft -= count;

206         }

207         else

208         {

209             count = GetNonRepetitionCount(src, srcLeft);

210             if((encSize + count + 1) > onuBufSize) /*输出缓冲区空间不够了*/

211             {

212                 return 1;

213             }

214             outbuf[encSize++] = count;

215             for(i = 0; i < count; i++) /*逐个复制这些数据*/

216             {

217                 outbuf[encSize++] = *src++;;

218             }

219             srcLeft -= count;

220         }

221     }

222     return encSize;

223 }

        现在用数据“AAABBBBBCABCDDD”检验上述算法,得到压缩后的数据:0x83,0x41,0x85,0x42,0x04,0x43,0x41,0x42,0x43,0x83,0x44,原始数据长度是15字节,压缩后是11字节,这种改进后的算法,原始数据越长,压缩的效果就越明显。

        这种改进方法的解压缩算法就比较简单了,因为两种情况下的数据的首部都有标志,只要根据标志判断如何处理就可以了。首先从压缩数据中取出一个字节的标志字节,然后判断是连续重复数据的标志还是连续不重复数据的标志,如果是连续重复数据,则将标志字节后面的数据重复复制n份,;如果是连续不重复数据,则将连续复制标志字节后面的n个数据。n的值是标志字节与0x3F做与操作后得到,因为标志字节的低7位bit就是数据块数属性。

        改进的解压缩算法如下:

225 int Rle_Decode_O(unsigned char *inbuf, int inSize, unsigned char *outbuf, int onuBufSize)

226 {

227     unsigned char *src = inbuf;

228     int i;

229     int decSize = 0;

230     int count = 0;

231 

232     while(src < (inbuf + inSize))

233     {

234         unsigned char sign = *src++;

235         int count = sign & 0x3F;

236         if((decSize + count) > onuBufSize) /*输出缓冲区空间不够了*/

237         {

238             return 1;

239         }

240         if((sign & 0x80) == 0x80) /*连续重复数据标志*/

241         {

242             for(i = 0; i < count; i++)

243             {

244                 outbuf[decSize++] = *src;

245             }

246             src++;

247         }

248         else

249         {

250             for(i = 0; i < count; i++)

251             {

252                 outbuf[decSize++] = *src++;

253             }

254         }

255     }

256 

257     return decSize;

258 }

用前面Rle_Encode_O()函数得到的压缩数据进行验证,结果正确。

        当今常用的压缩软件普遍使用从LZ77压缩算法改进的LZSS压缩算法。LZSS压缩算法是一种基于字典模型的压缩算法,算法依据是在文本流中词汇或短语很可能会重复出现,同样图像流中图像模式很也可能会重复出现。因此,在处理的过程中,构造一个编码表,用较短的编码代替重复序列,就可以有效地减少数据大小。与LZSS算法相比,RLE的压缩率显然没有LZSS的高,但是RLE算法具有速度快的优势,在LZSS算法出现之前,还是得到了很广泛的应用,比如著名的图像文件格式PCX就是使用了本文提到的第二种RLE算法。

点赞