RLE压缩算法C#详细教程
一、前言
什么是RLE算法
RLE(Run LengthEncoding)行程长度压缩算法是一个简单高效的无损数据压缩算法,其基本思路是把数据看成一个线性序列,而这些数据序列组织方式分成两种情况:一种是连续的重复数据块,另一种是连续的不重复数据块。
RLE算法的原理为,以一个表示块数长度的属性字节加上一个数据块,来代表原来连续的若干块数据,从而达到节省存储空间的目的。
本篇目的
- 尽可能描述RLE算法的原理
- 给出RLE算法的C#实现
开发环境
- 操作系统: Windows 10 X64
- SDK:NET Framework 4.7.2
- IDE:Visual Studio 2019
二、RLE算法原始方法及其变体解释
1.原始RLE算法
为了更直观地说明RLE算法,接下来使用示例数据进行演示
例:原始数据为:
A A A A A B B B B C C D E F
压缩后数据为:
5 A 4 B 2 C 1 D 1 E 1 F
原始算法进行压缩时,将逐个读取待压缩数据,若某个字节后有重复的字节,则重复计数加1,直到找到一个不重复字节时,将当前计数器中的值存入数据重复次数字节,对应的该字节的数据存放在其后的数据字节中,然后将计数器重置,继续使用相同方式读取剩下的未压缩数据。
由于数据重复次数的单位是一个字节,故最大值为255,因此若是数据序列的某一个数据重复次数大于255,当计数器值到达255时就需要将计数器值和当前数据值分别写入数据重复次数字节和数据字节中,然后计数器重置继续读取数据,直到到达文件末尾。
原始算法压缩完毕后的格式十分简单,即为:[重复次数] [数据字节] [重复次数] [数据字节]。因此解压缩时只需要读入重复次数n,再将其后的字节重复输出n份,再读入重复次数m,将其后的字节重复输出m份…以此类推循环往复,直到到达文件末尾,解压缩完毕。
2.RLE算法变体一
原始算法原理简单操作方便,但存在一个巨大的问题:如果需压缩的原始数据重复率很低,出现了连续的不重复数据,进行压缩反而会因为加入了太多的数据重复次数字节而导致数据膨胀,甚至变为原本长度的两倍,与压缩的目的背道而驰。因此针对这种情况,人们对算法进行了改进,即为,对连续出现的不重复数据不再简单插入块数属性,而是将其直接输出作为压缩后的数据。
但直接输出仍存在一个问题,由于解压缩时需要对压缩数据进行判断,判断其究竟是数据重复次数字节还是原始数据字节,因此在开头定义一个特殊字符作为数据重复字节的标志,每当需要数据重复次数字节时便在重复次数字节前插入特殊字符
同样以例子进行说明:
原始数据为:
A A A A A B B B B C C D E F
压缩后数据为:
T T 5 A T 4 B T 2 C D E F
此处特殊字符为T
这样在出现大量连续不重复数据时,就不会因为插入数据重复字节导致数据膨胀
这种变体的解压方法也很简单,压缩完毕后逐个读取字节,当读取到特殊字节时,其后一位字节作为数据重复次数n,再将重复字节后的数据字节输出n份,若没有读取到特殊字节,就作为不重复数据直接输出并继续读取直到文件末尾。
3.RLE算法变体二
上述压缩算法及变体1都存在一个问题,即解压缩时需要对每一个字节进行判断,区分它是重复次数字节还是数据字节,又或是特殊字符,在处理不重复字节时都是单个单个处理而非整体处理。因此变体2的改进即为,无论是重复或是不重复字节都各自作为一个整体来处理,在数据块前添加数据重复次数字节,通过重复字节的最高位来判断后跟的数据重复或是非重复。
例:
原始数据为:
A A A A A B B B B C C D E F
压缩后数据为:
85 A 84 B 5 C C D E F
此处85 84 5均为十六进制数,二位十六进制数表示一字节
(85H=10000101B,84H=10000100B,05H=00000101B)
此时压缩后数据格式均为:[重复次数] [数据块] [重复次数] [数据块]。在解压缩时,读入重复次数字节后进行二进制首位数的判断,若首位已置位为1,则其后的数据块即为被重复的数据,重复次数n储存在次数字节的后7位,解压时将该数据输出n次。反之,若首位为0,则代表其后数据块为不重复数据,且数据块长度m储存在后7位,解压时直接输出后面的m个字符。以此类推,直至读取到文件末尾。
由于数据重复次数的单位是一个字节,最高位用于进行数据类型的判断,故最大值为后7位可存储的数值,即127。若是数据块的长度大于127,当计数器值到达127时就需要将计数器值和当前数据值分别写入数据重复次数字节和数据块字节中,然后计数器重置继续读取数据。
同时,由于重复次数单位为1字节,设定仅当数据中出现连续3个字符重复,压缩时才将类型转换为重复数据,否则将其作为连续不重复数据处理。这是因为在出现2个字符重复即转换的情况下,由于插入了2个重复次数字节,压缩比反而会降低。
三、算法实现
本篇仅展示RLE算法变体二的C#代码实现
代码为直接输出压缩/解压后的数据,没有进行数据的存储
1.压缩函数代码
static void Compress(string str)//进行压缩的函数
{
int i = 0;
byte Repeat_num = 0;//重复计数
byte No_Repeat_num = 0;//不重复计数
char[] last = new char[2];//存储前两位数据的字符数组
last[0] = last[1] = '\0';//对字符数组初始化
string No_Repeat_str = null;//存储不重复数据的字符串
char Repeat_char = '\0';//存储重复数据的字符
//————————————————————————————————————————————————
//以上为函数需要的计数器及临时存储位
while (i < str.Length)
{
//——————————————————————————————————————————————————————
//由于重复次数字符只能储存数据0-127,在判断字符是否重复前先进行长度判断
//——————————————————————————————————————————————————————
if (Repeat_num == 127)
{
Console.Write("{0:x2}{1}", Repeat_num + 128, Repeat_char);
Repeat_num = 0;
if (i + 2 < str.Length)//为避免越界进行判断
{
last[0] = str[i];
last[1] = str[i+1];
i+=2;//将已满127的字符串与之后的字符串分隔开
}
else if (i + 1 < str.Length)//如果会出现越界但相等后面有两个字符
{
Console.WriteLine("{0:x2}{1}{2}", 2, str[i], str[i + 1]);//将最后两个字符输出
break;//结束循环
}
else
{
Console.WriteLine("{0:x2}{1}", 1, str[i]);//将最后一个字符输出
break;//结束循环
}
}
if (No_Repeat_num == 127)
{
Console.Write("{0:x2}{1}", No_Repeat_num, No_Repeat_str);
No_Repeat_num = 0;
No_Repeat_str = null;
}
//——————————————————————————————————————————————————————
//进行字符相等的判断
//——————————————————————————————————————————————————————
if (last[0] == last[1] && last[1] == str[i])//如果三个字符相等
{
if (Repeat_num == 0) { Repeat_num = 2; }
Repeat_char = last[0];
Repeat_num++;
if (No_Repeat_num != 0)
{ Console.Write("{0:x2}{1}", No_Repeat_num, No_Repeat_str); No_Repeat_num = 0; No_Repeat_str = null; }
}
//——————————————————————————————————————————————————————
//进行字符不等的判断
//——————————————————————————————————————————————————————
else if (i > 1)//如果三个字符不相等
{
if (Repeat_num != 0)//如果之前是相等状态
{
Console.Write("{0:x2}{1}", Repeat_num + 128, Repeat_char);
if (i + 2 < str.Length)//为避免越界进行判断
{
last[0] = last[1];
last[1] = str[i];
i++;//接下来三个字符不需要判断,必不相同,向前跨进一个字符
}
else if (i + 1 < str.Length)//如果会出现越界但相等后面有两个字符
{
Console.WriteLine("{0:x2}{1}{2}", 2, str[i], str[i + 1]);//将最后两个字符输出
Repeat_num = 0;
break;//结束循环
}
else
{
Console.WriteLine("{0:x2}{1}", 1, str[i]);//将最后一个字符输出
Repeat_num = 0;
break;//结束循环
}
}
else if (i + 1 < str.Length)//如果之前不是重复字符,且不位于字符串尾
{
No_Repeat_num++;
No_Repeat_str += last[0];
}
else//之前不是重复字符且位于字符串尾
{
No_Repeat_num += 3;
No_Repeat_str = No_Repeat_str + last[0] + last[1] + str[i];
}
Repeat_num = 0;
}
last[0] = last[1];
last[1] = str[i];
i++;
}
//文件遍历完毕,对是否存在最后一个字符串未输出进行判断
if (Repeat_num != 0)
{
Console.WriteLine("{0:x2}{1}", Repeat_num + 128, Repeat_char);
}
if (No_Repeat_num != 0)
{
Console.WriteLine("{0:x2}{1}", No_Repeat_num, No_Repeat_str);
}
}
2.解压缩函数代码
static void Decompression(string str)//进行解压的函数
{
string HEX;
int i = 0;
while (i < str.Length)
{
HEX = null;
HEX = HEX + str[i] + str[i + 1];//提取字符串中十六进制数位置的两个字符
if(((str[i+1] >= 48 && str[i+1] <= 57) || (str[i+1]>=65&&str[i+1]<=70)||(str[i+1]>=97&&str[i+1]<=102))&& ((str[i] >= 48 && str[i] <= 57) || (str[i] >= 65 && str[i] <= 70) || (str[i] >= 97 && str[i] <= 102)))
//判断提取的两个字符是否是十六进制数
{
byte a = byte.Parse(HEX, System.Globalization.NumberStyles.HexNumber);//将十六进制转换为十进制数
//进行次数字节首位的判断
if (a > 128)
{
i += 2;
for (int j = 0; j < a - 128; j++)
{
if (i == str.Length)//判断是否重复次数字节后存在数据字节
{
Console.Write("\n\n");
Console.WriteLine("出现错误,请检查是否输入正确的压缩编码\n");
break;
}
Console.Write("{0}", str[i]);//输出j个数据字节
}
i++;
}
if (a < 128)
{
i += 2;
for (int j = 0; j < a; j++)
{
if (i==str.Length)//判断是否不重复次数字节后存在数据字节
{
Console.Write("\n\n");
Console.WriteLine("出现错误,请检查是否输入正确的压缩编码\n");
break;
}
Console.Write("{0}", str[i]);//输出次数字节后的数据块
i++;
}
}
}
else//若提取的不是十六进制数
{
Console.Write("\n\n");
Console.WriteLine("出现错误,请检查是否输入正确的压缩编码\n");
break;
}
}
}
3.主体代码
using System;
namespace RLE压缩代码
{
class Program
{
static void Compress(string str)//进行压缩的函数
static void Decompression(string str)//进行解压的函数
static void Main(string[] args)//主函数
{
while (true)
{
Console.WriteLine("请输入数字选择需要RLE压缩或是解压缩。1:压缩 2:解压缩");
string select = Console.ReadLine();//通过判断读入的字符进行选择
if (select[0] == '1')
{
Console.WriteLine("请输入需要压缩的段落");
string str1 = Console.ReadLine();//读入需压缩字符串
Console.WriteLine("\n压缩完毕");
Console.WriteLine("RLE压缩后的编码为");
Console.WriteLine("─────────────────────────────────────");
Compress(str1);//调用RLE压缩函数
Console.WriteLine("─────────────────────────────────────");
Console.Write("\n");
}
else if (select[0] == '2')
{
Console.WriteLine("请输入需要解压缩的编码");
string str2 = Console.ReadLine();//读入需解压缩字符串
Console.WriteLine("\n解压完毕");
Console.WriteLine("RLE解压缩后的段落为");
Console.WriteLine("─────────────────────────────────────");
Decompression(str2);//调用RLE解压缩函数
Console.Write("\n");
Console.WriteLine("─────────────────────────────────────");
Console.Write("\n");
}
}
}
}
四、参考文献等
###本篇参考了以下文章
RLE压缩算法详解 http://data.biancheng.net/view/152.html
RLE行程长度编码压缩算法 https://blog.csdn.net/anzelin_ruc/article/details/9180525
算法系列之八:RLE行程长度压缩算法 https://blog.csdn.net/orbit/article/details/7062218
感谢阅读!