题目要求
编写一个程序,可以在命令行输入参数,完成指定文件的压缩解压,命令行参数如下:rle file1 –c(-d) file2
第一个参数为可执行程序名称,第二个参数为原始文件名,第三个参数为压缩或解压缩选项,第四个参数为新文件名。
涉及知识点
文件读写、位操作、内存管理、结构体定义、RLW 算法、命令行参数
(一)原题解析
问题分析
题目已经提示了要求用 RLE 算法结合位运算来实现解压缩,我们知道所有文件的本质都是二进制文件,这道题需要通过位操作和 RLE 算法直接压缩二进制代码。现在的问题有:
- 什么是 RLE 算法?如何用 C 语言实现 RLE 算法?
- 如何按照二进制的方式分块读入文件并在文件压缩和解压缩后输出?
解决方案(思路)
RLE 算法的本质在于区分连续字节块和非连续字节块,用单独的字节来存储连续字节块和非连续字节块的长度。对于其原理和代码实现,我们一起来看一下。
对于单个文件,传入文件后通过文件指针每次读入一定数量的字节数据,这些字节数据传入函数进行压缩编码或者解压解码后再把新数据写入指定的新文件。
算法分析
根据输入的命令行参数进行对应操作。
若为压缩操作则将指定要压缩的文件名和要输出的文件名传入 Compression 函数,分别创建两个文件指针指向输入和输出文件,两个申请的内存空间中 inbuf 用于存储待压缩的数据块,outbuf 用于存储待输出的数据块。然后用 fread 函数每次读入定长的字节数,并用 length 记录成功读入的字节数。把读取的字节数 length、指向两块申请的内存空间 inbuf 和 outbuf、存储待输出数据块的数组 outbuf 的大小传入 RLe_Encode 函数进行压缩编码。在 RLe_Encode 函数内建立输入指针 src指向 inbuf。循环遍历 inbuf 数组直到 inbuf 剩余字节为 0。循环时用 IsrepetitionStart函数判断,若连续的三个字节数据相同时则利用 GetRepetitionCount 函数得到重复的个数并将连续字节块和记录连续字节块的字节写入输出数组 outbuf 同时移动数组指针。否则利用 GetNonRepetitionCount 函数得到不重复的个数并逐个写入 outbuf 数组。
若为解压缩操作则将指定要解压的文件名和要输出的文件名传入 Decompression函数,分别创建两个文件指针指向输入和输出文件。两个申请的内存空间中 inbuf用于存储待解压缩的字节块,outbuf 用于存储待输出的字节块。然后用 fread 函数每次读入定长的字节数,并用 length 记录成功读入的字节数。把读取的字节数length、指向两块申请的内存空间 inbuf 和 outbuf、存储待输出数据块的数组 outbuf的大小传入 RLe_Decode 函数进行解压缩解码。在 RLe_Decode 函数内建立输入指针 src 指向 inbuf,循环遍历 inbuf 数组,若发现连续重复标记则则将标识字节后面的数据重复复制 n 份写入 outbuf。否则说明是非连续数据,将标识字节后面的 n 个数据复制到 outbuf。n 值由存储长度信息的字节块的值确定。
(二)RLE 压缩算法原理与 C 语言实现
RLE 算法实现
RLE 压缩算法(简称 RLE 算法)的基本思路是把数据按照线性序列分成两种情况:一种是连续的重复数据块,另一种是连续的不重复数据块。
RLE 算法的原理就是用一个表示块数的属性加上一个数据块代表原来连续的若干块数据,从而达到节省存储空间的目的。一般 RLE 算法都选择数据块的长度为 1 字节,表示块数的属性也用 1 字节表示,对于颜色数小于 256 色的图像文件或文本文件,块长度选择 1 字节是比较合适的。
连续重复数据的处理
RLE 算法有很多优化和改进的变种算法,这些算法对连续重复数据的处理方式基本上都是一样的。对于连续重复出现的数据,RLE 算法一般用两字节表示原来连续的多字节重复数据。我们用一个例子更直观地说明 RLE 算法对这种情况的处理,假如原始数据有 5 字节的连续数据:
[data] [data] [data] [data] [data]
则压缩后的数据就包含块数和 [data] 两字节,其中 [data] 只存储了一次,节省了存储空间:
[5] [data]
需要注意的是,一般 RLE 算法都采用插入一个长度属性字节存储连续数据的重复次数,因此能够表达的极大值就是 255 字节,如果连续的相同数据超过 255 字节时,就从第 255 字节处断开,将第 256 字节以及 256 字节后面的数据当成新的数据处理。
随着 RLE 算法采用的优化方式不同,这个长度属性字节所表达的意义也不同,对于本节给出的这种优化算法,长度属性字节的最高位被用来做一个标志位,只有 7 位用来表示长度。
连续非重复数据的处理
对于连续的非重复数据,RLE 算法的处理方法一般是不对数据进行任何处理,直接将原始数据作为压缩后的数据存储。
假如有以下 5 字节的连续非重复数据:
[datal] [data2] [data3] [data4] [data5]
按照这种处理方法,最后的数据和原始数据一样:
[data1] [data2] [data3] [data4] [data5]
现在有一个问题。在 RLE 算法解码的时候,如何区分连续重复和非重复数据?解决方法是把连续非重复数据也当成一组数据整体考虑。首先给连续重复数据和连续非重复数据都附加一个表示长度的属性字节,并利用这个长度属性字节的最高位来区分两种情况。
长度属性字节的最高位如果是 1,则表示后面紧跟的是个重复数据,需要重复的次数由长度属性字节的低 7 位(最大值是 127)表示。长度属性字节的最高位如果是 0,则表示后面紧跟的是非重复数据,长度也由长度属性字节的低 7 位表示。
采用这种优化方式,压缩后的数据非常有规律,两种类型的数据都从长度属性字节开始,除了标志位的不同,后跟的数据也不同。第一种情况后跟一个字节的重复数据,第二种情况后跟的是若干个字节的连续非重复数据。
算法实现
数据压缩的编码过程实现
原理
釆用前面给出的优化方式,编码算法不仅要能够识别连续重复数据和连续非重复数据两种情况,还要能够统计出两种情况下数据块的长度。
编码算法从数据的起始位置开始向后搜索,如果发现后面是重复数据且重复次数超过 2,则设置连续重复数据的标志并继续向后查找,直到找到第一个与之不相同的数据为止,将这个位置记为下次搜索的起始位置,根据位置差计算重复次数,最后长度属性字节以及一个字节的原始重复数据一起写入压缩数据;如果后面数据不是连续重复数据,则继续向后搜索查找连续重复数据,直到发现连续重复的数据且重复次数大于 2 为止,然后设置不重复数据标志,将新位置记为下次搜索的起始位置,最后将长度属性字节写入压缩数据并将原始数据逐字节复制到压缩数据。然后从上一步标记的新的搜索起始位开始,一直重复上面的过程,直到原始数据结束。
代码及其分析
代码说明
Rle_Encode() 函数是 RLE 算法的实现。
IsRepetitionStart() 函数
IsRepetitionStart() 函数判断从 src 开始的数据是否是连续重复数据。根据算法要求,只有数据重复出现两次以上才算作连续重复数据,因此IsRepetitionStart() 函数检査连续的 3 字节是否是相同的数据,如果是则判定为出现连续重复数据。之所以要求至少要 3 字节的重复数据才判定为连续重复数据,是为了尽量优化对短重复数据间隔出现时的压缩效率。
举个例子,对于这样的数据“AABCCD”,如果不采用这个策略,最终的压缩数据应该是 : [0x82][A][0x01][B][0x82][C][0x01][D]
注:此时 A 重复次数为 2,保存长度数据的字节数据为 10000010,开头的 1 表示为重复数据块,转换为十六进制为[0x82]
压缩后数据长度是 8 字节。如果采用这个策略,则上述数据就被认定为连续非重复数据,会被压缩为: [0x06][A][A][B][C][C][D]
压缩后数据长度是 7 字节,这样的数据越长,效果越明显。
GetRepetitionCount() 函数
如果是连续重复数据,则调用 GetRepetitionCount() 函数计算出连续重复数据的长度,将长度属性字节的最高位置 1 并向输出缓冲区写入一个字节的重复数据,具体做法是将 GetRepetitionCount() 函数的返回值 count|0x80 得到最高位置 1 的长度属性字节。
GetNonRepetitionCount() 函数
如果不是连续重复数据,则调用 GetNonRepetitionCount() 函数计算连续非重复数据的长度,将长度属性字节的最高位置 0 并向输出缓冲区复制连续的多个非重复数据。
数据解压缩的编码过程实现
代码说明
Rle_Decode() 函数是解压缩算法的实现代码,每组数据的第一字节是长度标识字节,其最高位是标识位,低 7 位是数据长度属性,根据标识位分别进行处理即可。
因为两种情况下的压缩数据首部都是 1 字节的长度属性标识,只要根据这个标识判断如何处理就可以了。首先从压缩数据中取出 1 字节的长度属性标识,然后判断是连续重复数据的标识还是连续非重复数据的标识,具体做法是通过位操作,也就是标识字节&0x80 是否等于 0x80 来判断标识字节最高位是否是 1):
如果是连续重复数据,则将标识字节后面的数据重复复制 n 份写入输出缓冲区;如果是连续非重复数据,则将标识字节后面的 n 个数据复制到输出缓冲区。n 的值是标识字节与 0x7F 做与操作后得到,因为标识字节低 7 位就是数据长度属性。
具体代码
方法一
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
/*编码算法从数据的起始位置开始向后搜索,
如果发现后面是重复数据且重复次数超过 2,则设置连续重复数据的标志并继续向后查找,
直到找到第一个与之不相同的数据为止,将这个位置记为下次搜索的起始位置,
根据位置差计算重复次数,最后长度属性字节以及一个字节的原始重复数据一起写入压缩数据;
如果后面数据不是连续重复数据,则继续向后搜索查找连续重复数据,
直到发现连续重复的数据且重复次数大于 2 为止,然后设置不重复数据标志,将新位置记为下次搜索的起始位置,
最后将长度属性字节写入压缩数据并将原始数据逐字节复制到压缩数据。
然后从上一步标记的新的搜索起始位开始,一直重复上面的过程,直到原始数据结束。*/
int IsrepetitionStart(unsigned char *src,int srcLeft){ //判断是否为有重复数超过3的重复数据
if(srcLeft<3){ //剩余数据数不足3返回0
return 0;
}
if((src[0]==src[1])&&(src[1]==src[2])){
return 1;
}
return 0;
}
int GetRepetitionCount(unsigned char *src,int srcLeft){ //获得重复数据个数
int repeatedbuf=src[0]; //repeatedbuf表示重复的值
int length=1; // 长度
while(length<srcLeft&&length<0x7f&&src[length]==repeatedbuf){ //长度标识占一字节,高位表示 重复与否,因此length最大为127
length++;
}
return length; //返回的length<=127,important
}
int GetNonRepetitionCount(unsigned char *src,int srcLeft){ //获得不重复数据个数
if(srcLeft<3){
return srcLeft;
}
int length=2;
int a=src[0],b=src[1];
while(length<srcLeft&&length<0x7f&&((a!=b)||(b!=src[length]))){ //三个连续数不全相等
a=b;
b=src[length];
length++;
}
return length;
}
int Rle_Encode(unsigned char *inbuf,int inSize,unsigned char *outbuf,int onuBufSize) //压缩算法,返回压缩后数据大小
{ //传入:输入数据缓冲区首地址,输入数据大小, 输出缓冲区首地址, 输出数据大小
unsigned char *src=inbuf; //定义指针遍历输入数据
int i;
int encSize=0; //输出缓冲区大小
int srcLeft=inSize;
while(srcLeft>0){
int count=0;
if(IsrepetitionStart(src,srcLeft)){ //有重复
if((encSize+2)>onuBufSize){ //输出缓冲区空间不够了
return -1;
}
count=GetRepetitionCount(src,srcLeft);
outbuf[encSize++]=count|0x80; //按位或运算,保证高位为1,传入输出缓冲区,(即为长度标识)
outbuf[encSize++]=*src; // 即为数据标识
src+=count; //设置新的搜索位置
srcLeft-=count; //更新剩余数据数
}
else{ //无重复
count=GetNonRepetitionCount(src,srcLeft);
if((encSize+count+1)>onuBufSize){
return -1;
}
outbuf[encSize++]=count;
for(i=0;i<count;i++){ //逐个复制这些数据
outbuf[encSize++]=*src++;
}
srcLeft-=count;
}
}
return encSize;
}
int Rle_Decode(unsigned char *inbuf,int inSize,unsigned char *outbuf,int onuBufSize){ //解压算法
unsigned char *src=inbuf;
int i;
int decSize=0; //输出缓冲区大小
int count=0;
while(src<(inbuf+inSize)){
unsigned char sign=*src++; //定义指针遍历输入数据
int count=sign & 0x7F; //获取长度标识,按位与运算,保留常量(转换为二进制形式)的后7位数
if((decSize+count)>onuBufSize){ //输出缓冲区空间不够了
return -1;
}
if((sign&0x80)==0x80){ //连续重复数据标志
for(i=0;i<count;i++){
outbuf[decSize++]=*src;
}
src++;
}else{
for(i=0;i<count;i++){
outbuf[decSize++]=*src++;
}
}
}
return decSize;
}
int Compression(char*Inputfilename,char*Outputfilename){ //文件压缩
FILE *Input=fopen(Inputfilename, "rb"); //源文件
FILE *Output=fopen(Outputfilename, "wb"); //目标文件
if (Input==NULL||Output==NULL){
printf("We can't open the file successfully!");
}
unsigned char*inbuf; //输入缓存区
unsigned char*outbuf; //输出缓存区
inbuf =(unsigned char*)malloc((sizeof(unsigned char))*1024*1024*1024);
outbuf=(unsigned char*)malloc((sizeof(unsigned char))*1024*1024*1024);
int length;
while ((length=fread(inbuf, sizeof(unsigned char),1024,Input))!= 0){ //length表示读入的数据块数目 ,这块用while我不太明白
int tmp=Rle_Encode(inbuf,length,outbuf,1024*1024*1024);
if(tmp==-1){
return -2;
}
fwrite(outbuf, sizeof(unsigned char),tmp,Output); //输出缓冲区数据写入目标文件
}
fclose(Input);
fclose(Output);
}
int Decompression(char*Inputfilename,char*Outputfilename){ //文件解压
FILE *Input=fopen(Inputfilename, "rb");
FILE *Output=fopen(Outputfilename, "wb");
if (Input==NULL||Output==NULL){
printf("We can't open the file successfully!");
}
unsigned char*inbuf; //输入缓存区
unsigned char*outbuf; //输出缓存区
inbuf=(unsigned char*)malloc((sizeof(unsigned char))*1024*1024*1024);
outbuf=(unsigned char*)malloc((sizeof(unsigned char))*1024*1024*1024);
int length;
while((length=fread(inbuf, sizeof(unsigned char),1024*1024*1024,Input))!=0){
int tmp=Rle_Decode(inbuf,length,outbuf,1024*1024*1024);
if(tmp==-1){
return -2;
}
fwrite(outbuf, sizeof(unsigned char),tmp,Output);
}
fclose(Input);
fclose(Output);
}
int main(int argc,char**argv)
{
if(strcmp(argv[2],"-c")==0){
Compression(argv[1],argv[3]);
}else if(strcmp(argv[2],"-d")==0){
Decompression(argv[1],argv[3]);
}
return 0;
}
方法二
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int IsRepetitionStart(unsigned char *src,int srcLeft){
if(srcLeft<3){ //剩余数据数不足3返回0
return 0;
}
if((src[0]==src[1])&&(src[1]==src[2])){
return 1;
}
return 0;
}
int GetRepetitionCount(unsigned char *src,int srcLeft){//获得重复数据个数
int repeatedbuf=src[0]; //repeatedbuf表示重复的值
int length=1; // 长度
while(length<srcLeft&&length<0x7f&&src[length]==repeatedbuf){ //长度标识占一字节,高位表示 重复与否,因此length最大为127
length++;
}
return length; //返回的length<=127
}
int GetNonRepetitionCount(unsigned char *src,int srcLeft){//获得不重复数据个数
if(srcLeft<3){
return srcLeft;
}
int length=2;
int a=src[0],b=src[1];
while(length<srcLeft&&length<0x7f&&((a!=b)||(b!=src[length]))){ //三个连续数不全相等
a=b;
b=src[length];
length++;
}
return length;
}
int Rle_Encode(unsigned char *inbuf,int inSize,unsigned char *outbuf,int onuBufSize){
unsigned char *src=inbuf;//建立输入指针src指向inbuf
int i;
int encSize=0; //输出缓冲区大小
int srcLeft=inSize;
while(srcLeft>0){
int count=0;
// 使用IsrepetitionStart函数判断是否有重复
if(IsRepetitionStart(src,srcLeft)){ //有重复
if((encSize+2)>onuBufSize){ //输出缓冲区空间不够了
return -1;
}
count=GetRepetitionCount(src,srcLeft); //利用GetRepetitionCount函数得到重复的个数并将连续字节块和记录连续字节块的字节写入输出数组outbuf
outbuf[encSize++]=count|0x80; //按位或运算,保证高位为1,传入输出缓冲区,(即为长度标识)
outbuf[encSize++]=*src; // 即为数据标识
src+=count; //设置新的搜索位置
srcLeft-=count; //更新剩余数据数
}
else{ //无重复
count=GetNonRepetitionCount(src,srcLeft); //得到不重复的个数并逐个写入 outbuf 数组
if((encSize+count+1)>onuBufSize){
return -1;
}
outbuf[encSize++]=count;
for(i=0;i<count;i++){ //逐个复制这些数据
outbuf[encSize++]=*src++;
}
srcLeft-=count;
}
}//循环遍历inbuf数组直到inbuf剩余字节为0
return encSize;
}//压缩算法
int Compression(char *filename,char *outfile){
FILE *in,*out;//定义指向文件的指针
char now,temp;
int filelen=1;//重复出现字符次数
if(!(in=fopen(filename,"rb"))){//以二进制方式打开只读文件
printf("文件打开失败\n");//若原文件不存在则进行提示
}else{
out=fopen(outfile,"wb");//二进制方式打开只写文件
}
int length;//记录成功读入的字节数
unsigned char*inbuf; //存储待压缩的数据块
unsigned char*outbuf; //存储待输出的数据块
inbuf =(unsigned char*)malloc((sizeof(unsigned char))*1024*1024*1024);
outbuf=(unsigned char*)malloc((sizeof(unsigned char))*1024*1024*1024);//申请开辟内存存储空间
while((length=fread(inbuf,sizeof(unsigned char),1024,in))!= 0){//用fread函数每次读入定长的字节数
int tmp=Rle_Encode(inbuf,length,outbuf,1024*1024*1024);//将存储待输出数据块的数组outbuf的大小传入RLe_Encode函数进行压缩编码
if(tmp==-1){
return -2;
}
fwrite(outbuf, sizeof(unsigned char),tmp,out); //输出缓冲区数据写入目标文件
}
fclose(in);
fclose(out);//关闭文件
}//文件压缩
int Rle_Decode(unsigned char *inbuf,int inSize,unsigned char *outbuf,int onuBufSize){
unsigned char *src=inbuf;
int i;
int decSize=0; //输出缓冲区大小
int count=0;
while(src<(inbuf+inSize)){
unsigned char sign=*src++; //定义指针遍历输入数据
int count=sign & 0x7F; //获取长度标识,按位与运算,保留常量(转换为二进制形式)的后7位数
if((decSize+count)>onuBufSize){ //输出缓冲区空间不够了
return -1;
}
if((sign&0x80)==0x80){ //连续重复数据标志
for(i=0;i<count;i++){
outbuf[decSize++]=*src;
}
src++;
}else{
for(i=0;i<count;i++){
outbuf[decSize++]=*src++;
}
}
}
return decSize;
}//解压算法
int Decompression(char*filename,char*outfile){
FILE *in=fopen(filename, "rb");
FILE *out=fopen(outfile, "wb");
int length;
if(!(in=fopen(filename,"rb"))){
printf("文件打开失败\n");
}else{
out=fopen(outfile,"wb");
}
unsigned char*inbuf; //输入缓存区
unsigned char*outbuf; //输出缓存区
inbuf=(unsigned char*)malloc((sizeof(unsigned char))*1024*1024*1024);
outbuf=(unsigned char*)malloc((sizeof(unsigned char))*1024*1024*1024);
while((length=fread(inbuf, sizeof(unsigned char),1024*1024*1024,in))!=0){
int tmp=Rle_Decode(inbuf,length,outbuf,1024*1024*1024);
if(tmp==-1){
return -2;
}
fwrite(outbuf, sizeof(unsigned char),tmp,out);
}
fclose(in);
fclose(out);
}//文件解压
int main(int argc,char**argv)
{
if(strcmp(argv[2],"-c")==0){
Compression(argv[1],argv[3]);
}else if(strcmp(argv[2],"-d")==0){
Decompression(argv[1],argv[3]);
}
return 0;
}
后记
代码写过很久了,隐约记得哪个代码好像有一点问题,但是懒得检查了 ,大家可以看解题思路,个人觉得还是写的比较完整的。欢迎指正。