【信息论与编码课设】LZ-77编码实现文本文件的压缩与解压缩(一)
摘要
LZ-77算法采用字典的方式进行压缩与解压缩,是一个简单但十分高效的数据压缩算法。其方式就是把数据中一些可以组织成短语(最长字符)的字符加入字典,然后再有相同字符出现采用标记来代替字典中的短语,如此通过标记代替多数重复出现的方式以进行压缩,其解压类似于压缩的逆向过程,通过解码标记和保持滑动窗口中的符号来更新解压数据。
LZ-77的主要算法逻辑就是,先通过前向缓冲区预读数据,然后再向滑动窗口移入(滑动窗口有一定的长度),不断的寻找能与字典中短语匹配的最长短语,然后通过标记符标记。
本文基于LZ-77算法,对70695字的汤姆索亚历险记英文小说进行压缩与解压缩,并对过程进行计时,由结果可知,平均每个离散消息提供的自信息量是3.619738bit/sign,压缩共需要71秒,文本大小由337Kb压缩至74kb,对压缩后的文件进行解压缩,共需要38秒,文本大小由74kb解压缩至304MB,压缩效率效率77.8%,解压缩效率是91.01%
关键词: LZ-77算法;短语字典;向前缓冲区; 滑动窗口
1、算法介绍
LZ77是一种基于字典的算法,它将长字符串(也称为短语)编码成短小的标记,用小标记代替字典中的短语,从而达到压缩的目的。也就是说,它通过用小的标记来代替数据中多次重复出现的长串方法来压缩数据。其处理的符号不一定是文本字符,可以是任意大小的符号。
1.1LZ-77编码实现过程
LZ77使用一个前向缓冲区和一个滑动窗口,先将一部分数据载入前向缓冲区。为了便于理解前向缓冲区如何存储短语并形成字典,LZ77算法的主要思想就是在前向缓冲区中不断寻找能够与字典中短语匹配的最长短语。前向缓冲区和滑动窗口之间的匹配有两种情况:要么找到一个匹配短语,或找不到匹配的短语。当找到最长的匹配时,将其编码成短语标记。
1.2压缩和解压缩数据
当压缩数据的时候,前向缓冲区与移动窗口之间在做短语匹配的是后会存在2种情况,找不到匹配时:将未匹配的符号编码成符号标记(多数都是字符本身),找到匹配时:将其最长的匹配编码成短语标记。
短语标记包含三部分信息:(滑动窗口中的偏移量(从匹配开始的地方计算)、匹配中的符号个数、匹配结束后的前向缓冲区中的第一个符号)。
一旦把n个符号编码并生成响应的标记,就将这n个符号从滑动窗口的一端移出,并用前向缓冲区中同样数量的符号来代替它们,如此,滑动窗口中始终有最新的短语。
短语标记包含三个部分:1、滑动窗口中的偏移量(从头部到匹配开始的前一个字符);2、匹配中的符号个数;3、匹配结束后,前向缓冲区中的第一个符号。
当没有找到匹配时,将未匹配的符号编码成符号标记。这个符号标记仅仅包含符号本身,没有压缩过程。事实上,我们将看到符号标记实际上比符号多一位,所以会出现轻微的扩展。
三元标志码:(滑动窗口中的偏移量,匹配串长度,匹配后向前缓冲首字符)
1.2.2解压
解压类似于压缩的逆向过程,通过解码标记和保持滑动窗口中的符号来更新解压数据。
从文件开始到文件结束,每次先读一位标志位,通过这个标志位来判断下面是一个(之间的距离,匹配长度) 对,还是一个没有改动的字节。如果是一个(之间的距离,匹配长度)对,就读出固定位数的(之间的距离,匹配长度)对,然后根据对中的信息,将匹配串输出到当前位置。如果是一个没有改动的字节,就读出一个字节,然后输出这个字节。
2、实践题目
本文利用LZ-77编码,对70695字的汤姆索亚历险记英文小说进行编码和解码,并对编码和解码的过程进行计时。
3、程序代码
3.1计算信源熵
代码如下(示例):
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdlib.h>
#define N 1000 //出错地方:字符总数不对,求字符串长度有问题。似乎跟N有关。
int main(void) //没有输出字符个数。
{
char s[N],M[N];
int i = 0,j=0,n=0,L=0;
int len, num[27] = {0};
double result=0,p[27]= {0};
FILE *f;
char temp[N];
/*以下是打开一个指定文件的过程*/
if(!(f = fopen("c:larger.txt","rb")))
{
printf("文件打开失败!\n");
return 0;
}
while(!feof(f)) //feof输入输出函数,检查文件是否结束,如结束,则返回非零值,否则返回0 .函数原型为:int feof(FILE *fp)
{
len = fread(temp,1,486,f); //fread返回读取的字符个数 temp为内存区域首地址 1为每次读入字节数 486读入次数 f指针
}
fclose(f); //关闭文件
temp[len] = '\0'; //方便统计字符总数
memcpy(s, temp, sizeof(temp)); //从temp中拷贝sizeof个字节到目标s中
/*统计26个字母及空格出现次数*/
for(i=0; i<strlen(s); i++)
{
if(s[i]==' ')
num[26]++;
else if(s[i]>='a'&&s[i]<='z')
num[s[i]-97]++;
else if(s[i]>='A'&&s[i]<='Z')
num[s[i]-65]++;
}
printf("文档中各个字母出现的次数:\n");
for(j=0; j<26; j++)
{
M[j]=num[j];
printf("%3c:%d\t",j+65,M[j]);
L++;
if(L==3)
{
printf("\n");
L=0;
}
}
printf("空格:%d\t",num[26]);
/*统计26个字母或者空格出现概率*/
printf("\n文档中各个字母出现的概率:\n");
for(i=0; i<26; i++)
{
p[i]=(double)num[i]/strlen(s);
printf("%3c:%f\t",i+65,p[i]);
n++;
if(n==3)
{
printf("\n");
n=0;
}
}
p[26]=(double)num[26]/strlen(s);
printf("空格:%f\t",p[26]);
printf("\n");
/*计算信源熵*/
for(i=0; i<27; i++)
{
if (p[i]!=0)
result=result+p[i]*log(p[i])*1.433;
}
result=-result;
printf("信源熵为:%f",result);
printf(" \n文章总字符长度:%d",strlen(temp));
printf("\n");
return 0;
}
3.2文本压缩
代码如下(示例):
#include<iostream>
#include<sstream>
#include<fstream>
#include<string>
#include<vector>
#include<Windows.h>
#include<ctime>
#include<cstdio>
using namespace std;
int const MAX_SIZE = 80;//文件名长度
int const MAX=99999999;//数组长度
int n1 = 0;
vector<char> v,v1;
ifstream infile;//用于打开文件
string file_name;//文件名
string line;//读取文件时保存当前行
string str;
int num;
void read()
{
char c;
cout << "请输入需要解码的文件:" << endl;
cin >> file_name;//获取输入的文件名
infile.open(file_name.c_str(), ios::in);//根据文件名打开文件,打开方式是读取
if (!infile)//如果没有找到这个文件
{
cerr << "打开文件" << file_name << "失败" << endl;
}
getline(infile, str);
v.push_back(str[2]);
}
void decode()
{
int head = -9; int tail = 0;
while (getline(infile, str))
{
int n = (int)str[0] - 48;
int m = (int)str[1] - 48;
if (n == 0 && m == 0)
{
v.push_back(str[2]);
head++;
tail++;
}
else
{
if (head < 0)
{
string str_empty = "";
for (int i = 0; i <= tail; i++)
{
str_empty = str_empty + v[i];
}
str_empty = str_empty.substr(n + head, m);
for (int j = 0; j < str_empty.length(); j++)
{
v.push_back(str_empty[j]);
}
v.push_back(str[2]);
head = head + m + 1;
tail = tail + m + 1;
}
else
{
string str_empty = "";
for (int i = head; i <= tail; i++)
{
str_empty = str_empty + v[i];
}
str_empty = str_empty.substr(n, m);
for (int j = 0; j < str_empty.length(); j++)
{
v.push_back(str_empty[j]);
}
v.push_back(str[2]);
head = head + m + 1;
tail = tail + m + 1;
}
}
}
//将完成编码的内容输出到文件中
char arr[MAX_SIZE];
string str;
cout << "请输入完成解码的文件名:" << endl;
cin >> arr;
//cin.getline(arr,MAX_SIZE,'#');//cin.getline(字符指针(char*),字符个数N(int),结束符(char));
ofstream outfile(arr, ios_base::out);
if (!outfile)
{
cout << "error!" << endl;
}
for (int i = 0; i < v.size(); i++)
{
outfile << v[i];//解码得到的内容连续输出
}
outfile.close();
}
//计算解码时间
double Time()
{
clock_t time_start, time_finish, time_need;
time_start = clock();
read();
decode();
time_finish = clock();
time_need = (double)(time_finish - time_start) / CLOCKS_PER_SEC;
return time_need;
}
int main()
{
cout << "解码所需要的时间(单位:秒):" << Time() << "秒" << endl;
return 0;
}
3.3文本解压缩
#include<iostream>
#include<sstream>
#include<fstream>
#include<string>
#include<vector>
#include<Windows.h>
#include<ctime>
#include<cstdio>
using namespace std;
int const MAX_SIZE = 80;//文件名长度
vector<char> v;
//将LZ77编码后的格式设置为二进制形式:三元符号组(off, len, c)
struct Ternary_symbol_group
{
int off;
int len;
char c;
};
vector<Ternary_symbol_group> v_encode_bin;
int size_of_window = 10;//窗口的大小为 10 个字符
ifstream infile;//用于打开文件
string file_name;//文件名
string line;//读取文件时保存当前行
//读取文件内容,存入容器
void read()
{
cout << "请输入需要编码的文件:" << endl;
cin >> file_name;//获取输入的文件名
infile.open(file_name.c_str(), ios::in);//根据文件名打开文件,打开方式是读取
if (!infile)//如果没有找到这个文件
{
cerr << "打开文件" << file_name << "失败" << endl;
}
char c;
//存入容器
while (infile >> c)
{
v.push_back(c);
}
infile.close();//关闭文件
}
//编码函数
void encode()
{
int head = -10; int tail = -1;
while (tail + 1 < v.size())
{
if (tail == -1)
{
Ternary_symbol_group t;
t.len = 0;
t.off = 0;
t.c = v[0];
v_encode_bin.push_back(t);
head++; tail++;
}
else
{
if (head < 0)
{
int size = tail + 1; int flags = 0;
string s = ""; string s1 = "";
for (int i = 0; i <= tail; i++)
{
s = s + v[i];
}
for (int j = 1; j <= size; j++)
{
s1 = s1 + v[tail + j];
}
for (int k = size; k >= 1; k--)
{
int flag = 0;
string s2 = s1.substr(0, k);
int k1 = 0; int k2 = k;
for (; k2 + k1 <= size; )
{
if (s.substr(k1, k2) == s2)
{
Ternary_symbol_group p;
p.c = v[tail + k2 + 1]; p.len = k2; p.off = k1 - head;
v_encode_bin.push_back(p);
tail = tail + k2 + 1; head = head + k2 + 1;
flag = 1; flags = 1;
break;
}
k1++;
}
if (flag == 1)
{
break;
}
}
if (flags == 0)
{
Ternary_symbol_group f;
f.c = v[tail + 1]; f.len = 0; f.off = 0;
v_encode_bin.push_back(f);
head++; tail++;
}
}
else
{
int size;
if (v.size() - 1 - tail >= 10)
{
size = 10;
}
else
{
size = v.size() - 1 - tail;
}
int flags = 0;
string s = ""; string s1 = "";
for (int i = head; i <= tail; i++)
{
s = s + v[i];
}
for (int j = 1; j <= size; j++)
{
s1 = s1 + v[tail + j];
}
for (int k = size; k >= 1; k--)
{
int flag = 0;
string s2 = s1.substr(0, k);
int k1 = 0; int k2 = k;
for (; k1 + k2 <= 10; )
{
if (s.substr(k1, k2) == s2)
{
Ternary_symbol_group p;
p.c = v[tail + k2 + 1]; p.len = k2; p.off = k1;
v_encode_bin.push_back(p);
tail = tail + k2 + 1; head = head + k2 + 1;
flag = 1; flags = 1;
break;
}
k1++;
}
if (flag == 1)
{
break;
}
}
if (flags == 0)
{
Ternary_symbol_group f;
f.c = v[tail + 1]; f.len = 0; f.off = 0;
v_encode_bin.push_back(f);
head++; tail++;
}
}
}
}
//将完成编码的内容输出到文件中
char arr[MAX_SIZE];
string str;
cout << "请输入完成编码的文件名:" << endl;
cin>>arr;
//cin.getline(arr,MAX_SIZE,'#');//cin.getline(字符指针(char*),字符个数N(int),结束符(char));
ofstream outfile(arr, ios_base::out);
if (!outfile)
{
cout << "error!" << endl;
}
for (int i = 0; i < v_encode_bin.size(); i++)
{
outfile << v_encode_bin[i].off << v_encode_bin[i].len << v_encode_bin[i].c;//编码得到的内容连续输出
}
outfile.close();
char arr2[MAX_SIZE];
string str2;
cout << "请输入完成编码的文件名(逐行存储):" << endl;
cin >> arr2;
ofstream outfile2(arr2, ios_base::out);
if (!outfile2)
{
cout << "error!" << endl;
}
for (int i = 0; i < v_encode_bin.size(); i++)
{
outfile2 << v_encode_bin[i].off << v_encode_bin[i].len << v_encode_bin[i].c<< endl;//逐行输出
}
outfile2.close();
}
//计算编码时间
double Time()
{
clock_t time_start, time_finish, time_need;
time_start = clock();
read();
encode();
time_finish = clock();
time_need = (double)(time_finish - time_start) / CLOCKS_PER_SEC;
return time_need;
}
int main()
{
cout << "编码所需要的时间(单位:秒):" << Time() << "秒" << endl;
return 0;
}