上一篇文章给出了Huffman编码和Shannon Fano编码的编码原理以及C++的程序,程序可以用来实现给任意类型的文件进行无损压缩,缺点是比较耗时,不能作为正常的通用压缩软件来使用,但是作为算法理解,算法思路是没有问题的,后续可能需要进行优化,下面的LZ编码和算数编码和Huffman、Fano编码是走的截然不同的道路,他们的思想差别很大,但却殊途同归,在算法理解上我借助了一些网友前辈的博客中的例子,文章最后我也会指出引用了那些文章,谢谢前辈们给出的经典的例子能让晚辈站在巨人的肩膀上,所以在此声明,希望我的引用不会侵犯到前辈们的权益
* 先上源代码*
信源编码源代码
算数编码不同于Huffman编码,它是非分组(非块)码。它从全序列出发,考虑符号之间的依赖关系来进行编码的。
算数编码主要的编码方法正是计算输入信源符号序列所对应的区间。术编码的基本原理是将编码的消息表示成实数0和1之间的一个间隔(Interval),消息越长,编码表示它的间隔就越小,表示这一间隔所需的二进制位就越多。
算术编码用到两个基本的参数:符号的概率和它的编码间隔。信源符号的概率决定压缩编码的效率,也决定编码过程中信源符号的间隔,而这些间隔包含在0到1之间。编码过程中的间隔决定了符号压缩后的输出。
给定事件序列的算术编码步骤如下:
(1)编码器在开始时将“当前间隔” [ L, H) 设置为[0,1)。
(2)对每一事件,编码器按步骤(a)和(b)进行处理
(a)编码器将“当前间隔”分为子间隔,每一个事件一个。
(b)一个子间隔的大小与下一个将出现的事件的概率成比例,编码器选择子间隔对应于下一个确切发生的事件相对应,并使它成为新的“当前间隔”。
(3)最后输出的“当前间隔”的下边界就是该给定事件序列的算术编码。
设Low和High分别表示“当前间隔”的下边界和上边界,CodeRange为编码间隔的长度,LowRange(symbol)和HighRange(symbol)分别代表为了事件symbol分配的初始间隔下边界和上边界。算术编码也是一种对错误很敏感的编码方法,如果有一位发生错误就会导致整个消息译错。
算术编码可以是静态的或者自适应的。在静态算术编码中,信源符号的概率是固定的。在自适应算术编码中,信源符号的概率根据编码时符号出现的频繁程度动态地 进行修改,在编码期间估算信源符号概率的过程叫做建模。需要开发动态算术编码的原因是因为事先知道精确的信源概率是很难的,而且是不切实际的。当压缩消息 时,我们不能期待一个算术编码器获得最大的效率,所能做的最有效的方法是在编码过程中估算概率。因此动态建模就成为确定编码器压缩效率的关键。
假设有信源符号{A, B. C, D}, 对应的概率和初始编码间隔如下表所示
符号 | A | B | C | D |
概率 | 01. | 0.4 | 0.2 | 0.3 |
初始编码间隔 | [0, 0.1) | [0.1, 0.5) | [0.5, 0.7) | [0.7, 1) |
如果二进制消息序列的输入为:CADACDB。编码时首先输入的符号是C, 找到他的编码范围是[0.5, 0.7)。由于消息由于消息中第二个符号A的编码范围是[0, 0.1),因此它的间隔就取[0.5, 0.7)的第一个十分之一作为新间隔[0.5, 0.52)。依此类推,编码第3个符号D时取新间隔为[0.514, 0.52),编码第4个符号A时,取新间隔为[0.514, 0.5146),…。消息的编码输出可以是最后一个间隔中的任意数
编码过程如下表所示
表3.2 算数编码的编码过程
步骤 | 输入符号 | 编码间隔 | 编码判决 |
---|---|---|---|
1 | C | [0.5, 0.7] | 符号的间隔范围[0.5, 0.7] |
2 | A | [0.5, 0.52] | [0.5, 0.7]间隔的第一个1/10 |
3 | D | [0.514, 0.52] | [0.5, 0.52]间隔的最后一个1/10 |
4 | A | [0.514, 0.5146] | [0.514, 0.52]间隔的第一个1/10 |
5 | C | [0.5143, 0.51442] | [0.514, 0.5146]间隔的第五个1/10开始,二个1/10 |
6 | D | [0.514384, 0.51442] | [0.5143, 0.51442]间隔的最后3个1/10 |
7 | B | [0.5143836,0.514402] | [0.514384,0.51442]间隔的4个1/10,从第1个1/10开始 |
8 | 从[0.5143876, 0.514402]中选择一个数作为输出:0.5143876 |
译码过程如下表
表3.3 算数编码的译码过程
步骤 | 间隔 | 译码符号 | 译码判决 |
---|---|---|---|
1 | [0.5, 0.7] | C | 0.51439在间隔 [0.5, 0.7) |
2 | [0.5, 0.52] | A | 0.51439在间隔 [0.5, 0.7)的第1个1/10 |
3 | [0.514, 0.52] | D | 0.51439在间隔[0.5, 0.52)的第7个1/10 |
4 | [0.514, 0.5146] | A | 0.51439在间隔[0.514, 0.52]的第1个1/10 |
5 | [0.5143, 0.51442] | C | 0.51439在间隔[0.514, 0.5146]的第5个1/10 |
6 | [0.514384, 0.51442] | D | 0.51439在间隔[0.5143, 0.51442]的第7个1/10 |
7 | [0.51439,0.5143948] | B | 0.51439在间隔[0.51439, 0.5143948]的第1个1/10 |
8 | 译码的消息:C A D A C D B |
这几种算法唯独算数编码我没有用C++实现,当时记得为了应付课堂作业,借用了网上一位博友的代码,大家如果想借鉴这个算法实现的代码,我这里可以给出我更改后的版本,但并不是我原创的,但是很抱歉具体是在哪里借鉴的我忘记了,当时比较草率,没有想太多,这里我还是把过程及测试结果给大家介绍清楚,代码的话如果能找到原主人最好不过了,这里我给出我改过的版本。
算数编码我只是使用字符串进行了测试,不能做到像Huffman编码一样对任何类型的文件都进行编码和译码。
主函数
int main()
{
string str; //输入要编码的String类型字符串
int number = 0, size = 0; //number--字符串中不重复的字符个数;size--字符串长度
char c[N]; //用于存储不重复的字符
long double p[N], output; //p[N]--不重复字符的概率,output--编码结果
disp();
cout << "输入要编码的字符串:";
getline(cin, str); //输入要编码的字符串
size = str.length(); //字符串长度
number = proba(str, c, p, size);//调用求概率函数,返回不重复字符的个数
cout.setf(ios::fixed); //“魔法配方”规定了小数部分的个数
cout.setf(ios::showpoint); //在此规定编码结果的小数部分有十个
cout.precision(10);//调用编码函数,返回编码结果
output = bma(c, p, str, number, size);//调用译码函数,输出要编码的字符串,
yma(str, c, p, number, size, output); //以验证编码是否正确
getchar();
return 0;
}
特殊结构和功能函数的定义
#define N 100 //输入的字符应该不超过50个
struct L //结构用于求各字符及其概率
{
char ch; //存储出现的字符(不重复)
int num; //存储字符出现的次数
double f;//存储字符的概率
};
//显示信息
void disp();
//求概率函数,输入:字符串;输出:字符数组、字符的概率数组;返回:数组长度; int proba(string str,char c[],long double p[],int count);
//求概率的辅助函数
int search(vector arch, char, int n);
long double bma(char c[], long double p[], string str, int number, int size);
int proba(string str, char c[], long double p[], int count);
//编码函数,输入:字符串,字符数组,概率数组,以及数组长度;输出:编码结果 long double bma(char c[],long double p[],string str,int number,int size);
//译码函数,输入:编码结果,字符串,字符数组,概率数组,以及它们的长度;输出:字符串
//该函数可以用于检测编码是否正确
void yma(string str, char c[], long double p[], int number, int size, long double input);
void disp()
{
cout << endl;
cout << "此程序只需要输入要编码的字符串,不需要输入字符概率\n";
cout << endl;
}
//求概率函数
int proba(string str, char c[], long double p[], int count)
{
cout.setf(ios::fixed); //“魔法配方”规定了小数部分位数为三位
cout.setf(ios::showpoint);
cout.precision(3);
vector pt; //定义了结构类型的向量,用于同时存储不重复的字符和其概率
L temp; //结构类型的变量
temp.ch = str[0]; //暂存字符串的第一个字符,它的个数暂设为1
temp.num = 1;
temp.f = 0.0;
pt.push_back(temp); //将该字符及其个数压入向量
for (int i = 1; i//对整个字符串进行扫描
{
temp.ch = str[i]; //暂存第二个字符
temp.num = 1;
temp.f = 0.0;
for (int j = 0; j//在结构向量中寻找是否有重复字符出现
{ //若重复,该字符个数加1,并跳出循环
int k; //若不重复,则压入该字符,并跳出循环
k = search(pt, str[i], pt.size());
if (k >= 0)
{
pt[k].num++;
break;
}
else
{
pt.push_back(temp);
break;
}
}
}
for (int i = 0; i//计算不重复字符出现的概率
{
pt[i].f = double(pt[i].num) / count;
}
int number = pt.size(); //计算不重复字符出现的次数
cout << "各字符概率如下:\n";
for (int i = 0; i//显示所得的概率,验证是否正确
{
if (count == 0)
{
cout << "NO sample!\n";
}
else
{
c[i] = pt[i].ch;
p[i] = pt[i].f;
cout << c[i] << "的概率为:" << p[i] << endl;
}
}
return number; //返回不重复字符的个数
}
//求概率的辅助函数
//若搜索发现有重复字符返回正数
//否则,返回-1
int search(vector arch, char ch1, int n)
{
for (int i = 0; iif (ch1 == arch[i].ch) return i;
}
return -1;
}
//编码函数
long double bma(char c[], long double p[], string str, int number, int size)
{
long double High = 0.0, Low = 0.0, high, low, range;
//High--下一个编码区间的上限,Low--下一个编码区间的下限;
//high--中间变量,用来计算下一个编码区间的上限;
//low--中间变量,用来计算下一个编码区间的下限;
//range--上一个被编码区间长度
int i, j = 0;
for (i = 0; iif (str[0] == c[i]) break; //编码第一个字符
}
while (j//寻找该字符的概率区间下限
}
range = p[j]; //得到该字符的概率长度
High = Low + range; //得到该字符概率区间上限
for (i = 1; i//开始编码第二个字符
{
for (j = 0; j//寻找该字符在c数组中的位置
{
if (str[i] == c[j])
{
if (j == 0) //若该字符在c数组中的第一个字符
{
low = Low; //此时该字符的概率区间下限刚好为零
high = Low + p[j] * range;
High = high;
range *= p[j]; //求出该字符的编码区间长度
}
else //若该编码字符不是c数组中的第一个
{
float proba_next = 0.0;
for (int k = 0; k <= j - 1; k++)
proba_next += p[k]; //再次寻找字符的概率区间下限
low = Low + range*proba_next; //编码区间下限
high = Low + range*(proba_next + p[j]);//编码区间上限
Low = low; //编码区间下限
High = high; //编码区间上限
range *= p[j]; //编码区间长度
}
}
else continue; //i++,编码下一个字符
}
}
cout << endl;
cout << "输入字符串的编码为:" << Low << endl;
return Low;
}
//译码函数
void yma(string str, char c[], long double p[], int number, int size, long double input)
{
vector<char> v; //定义char类型向量v
long double temp; //中间变量
long double sum[N]; //存储不重复字符概率区间的下限
sum[0] = 0.0; //数组第一个元素为0
for (int i = 1; i1; i++) //计算数组各元素的值
{
sum[i] = sum[i - 1] + p[i - 1];
}
for (int j = 0; jfor (int k = 0; k//确定被编码字符的下限属于【0,1】之间的哪一段
if ((input>sum[k]) && (input1])) //发现在哪就将属于该段的字符压入向量v
{
v.push_back(str[j]);
temp = (input - sum[k]) / (sum[k + 1] - sum[k]);//计算下一个被编码字符的下限
input = temp;
break;
}
else
continue;
}
}
cout << endl;
cout << "译码输出为:"; //将译码结果输出
for (int m = 0; mcout << v[m];
}
cout << endl;
}
经过查找资料,LZ编码并不是一种编码,而是一组编码,由LZ-77和LZ-78演变而来有很多种变形。所以这里我只选取了一种比较简单的算法LZ-78算法。
LZ-78编码算法是一种分段编码算法。算法的压缩过程非常简单。在压缩时维护一个动态词典Dictionary,其包括了历史字符串的index与内容。设信源符号集 A=a0,a1,a2...aq−1 共q个字符,设输入信源序列为
LZ-78编码在压缩时维护一个动态词典Dictionary,其包括了历史字符串的index与内容;压缩情况分为三种:
1. 若当前字符c未出现在词典中,则编码为(0, c);
2. 若当前字符c出现在词典中,则与词典做最长匹配,然后编码为(prefixIndex,lastChar),其中,prefixIndex为最长匹配的前缀字符串,lastChar为最长匹配后的第一个字符;
3. 为对最后一个字符的特殊处理,编码为(prefixIndex,)
举例
以字符串“ABBCBCABABCAABCAAB”压缩编码构造字典的过程如下
1. A 不在字典中; 插入A
2. B不在字典中; 插入B
3. B 在字典中.
BC 不在字典中; 插入BC
4. B在字典中.
BC在字典中.
BCA不在字典中.;插入BCA
5. B在字典中.
BA不在字典中; 插入BA.
6. B在字典中.
BC在字典中.
BCA在字典中.
BCAA不在字典中;插入BCAA
7. B在字典中.
BC在字典中.
BCA在字典中.
BCAA在字典中.
BCAAB 不在字典中; 插入BCAAB.
LZ-78算法我构建了一个LZ78类。类的定义如下
class LZ78
{
public:
struct Dictionary
{
unsigned int Index;
int preIndex;
unsigned char lastChar;
vector<unsigned char> stringInDic;
};
public:
struct OutData
{
unsigned int preIndex;
unsigned char lastChar;
};
public:
string fileAddress;
LZ78(); //构造函数
void open(string);
void Press();
void Decode(string sourcefile, string dstfile);
private:
bool IfStringInDic(vector CurrentString, vector<unsigned char> DataDic, unsigned int &Index);
private:
vector<unsigned char> FindPreString(vector DataDic, unsigned int);
};
核心属性
1. struct Dictionary: 存储字典信息
2. struct OutData: 存储输出信息即上面所提到的二元组(preIndex, lastChar);
3. string fileAddress: 需要压缩的文件的路径名称
核心函数:
1. void open(string address) : 打开待压缩文件
2. void Press(): 压缩文件操作
3. void Decode(string sourcefile, string dstfile): 解码操作
正式操作见下面主函数:
主函数:
int main()
{
LZ78 haha;
clock_t start, end;
start = clock();
haha.open("./KongFu.jpg"); //打开文件
haha.Press(); //压缩文件
end = clock();
cout << "压缩文件用时:" << endl << endl;
cout << double((end - start) / CLOCKS_PER_SEC) << "/s" << endl << endl;
start = clock();
LZ78 nothaha;
nothaha.Decode("./KongFu.jpg.lz", "KongFuout.jpg");
cout << "解压用时:" << endl << endl;
cout << double((start - end) / CLOCKS_PER_SEC) << "/s" << endl << endl;
getchar();
}
文件压缩步骤
第一步:建立haha对象为LZ78类型
第二步:打开待压缩的文件
第三步:压缩文件
第四部:压缩结束
文件解压步骤
第一步:建立nothaha对象为LZ78类型
第二步:解压文件
第三步:解压结束
LZ-78编码的性能测试见下表
表4.1 LZ-78编码性能测试
原始文件 | 890Bytes(文本) | 46.7kb(图像) | 7.82M(视频) |
---|---|---|---|
压缩文件 | 3133Bytes | 47.9kb | 7.81M |
压缩率 | 1.29 | 1.025 | 99.8% |
压缩用时 | 6/s | 20/s | / |
解码用时 | 0/s | 0/s | / |
看到自己程序跑的结果自己都想笑,这也太慢了点了,有时候想安慰一下自己,可能是C++读取文件的API很耗时,要想速度快,有朝一日自己写读取文件的API,励志。
最后附上LZ-78程序的C++源代码
LZ78.h
#include
#include
#include
using namespace std;
class LZ78
{
public:
struct Dictionary
{
unsigned int Index;
int preIndex;
unsigned char lastChar;
vector<unsigned char> stringInDic;
};
public:
struct OutData
{
unsigned int preIndex;
unsigned char lastChar;
};
public:
string fileAddress;
LZ78(); //构造函数
void open(string);
void Press();
void Decode(string sourcefile, string dstfile);
private:
bool IfStringInDic(vector CurrentString, vector<unsigned char> DataDic, unsigned int &Index);
private:
vector<unsigned char> FindPreString(vector DataDic, unsigned int);
};
LZ78::LZ78()
{
}
void LZ78::open(string input)
{
fileAddress = input;
}
void LZ78::Press()
{
ifstream read;
read.open(fileAddress, ios::in|ios::binary);
if (!read )
{
cout << "文件读取错误" << endl << endl;
return;
}
ofstream write;
write.open(fileAddress + ".lz", ios::out|ios::binary);
if (!write)
{
cout << "输出文件不能建立(*.lz)" << endl << endl;
}
unsigned char *firstchar = new unsigned char;
read.read((char*)firstchar, sizeof(unsigned char));
vector DataDic; //建立字典
while (!read.eof())
{
if (DataDic.size() == 0)
{
Dictionary firstDic;
OutData *firstout = new OutData;
firstDic.Index = 1;
firstDic.preIndex = 0;
firstDic.lastChar = *firstchar;
firstDic.stringInDic.push_back(*firstchar);
DataDic.push_back(firstDic);
firstout->lastChar = *firstchar;
firstout->preIndex = 0;
write.write((char*)firstout, sizeof(OutData));
}
else
{
unsigned char *now = new unsigned char; //用于读取的字符
unsigned char *reallast = new unsigned char;
vector<unsigned char> CurrentString;
unsigned int index = 0; //字符串存在在字典中的位置, 初始设为0
Dictionary currentInfo; //存储当前的单词到字典中
OutData *currentOutdata = new OutData; //存储当前编码信息,一会压缩进如压缩文件。
int EOFflag = 0;
do
{
read.read((char*)now, sizeof(unsigned char));
if (read.eof())
{
EOFflag = 1; //标记是否到文件的结尾
break;
}
else
{
CurrentString.push_back(*now);
}
}
while (IfStringInDic(DataDic, CurrentString, index));
if (EOFflag == 1)
{
if (CurrentString.size() == 0)
{
break; //如果当前字符串中没有字符,直接跳出循环
}
else
{ //如果当前字符串中有字符,对这段字符进行压缩。
*reallast = CurrentString[CurrentString.size() - 1];
CurrentString.erase(CurrentString.end() - 1);
IfStringInDic(DataDic, CurrentString, index);
}
}
else
{
*reallast = *now;
}
currentInfo.Index = DataDic.size() + 1;
currentInfo.lastChar = *reallast;
currentInfo.preIndex = index;
currentInfo.stringInDic = CurrentString;
DataDic.push_back(currentInfo);
currentOutdata->lastChar = *reallast;
currentOutdata->preIndex = index;
write.write((char*)currentOutdata, sizeof(OutData));
}
}
read.close();
write.close();
}
bool LZ78::IfStringInDic(vector DataDic, vector<unsigned char> CurrentString, unsigned int &Index)
{
int flag = 0;
for (int i = 0; i < DataDic.size(); i++)
{
if (CurrentString == DataDic[i].stringInDic)
{
Index = DataDic[i].Index;
flag = 1;
return true;
}
}
if (flag == 0)
{
return false;
}
}
void LZ78::Decode(string sourcefile, string dstfile)
{
ifstream readfile;
ofstream putfile;
readfile.open(sourcefile, ios::in | ios::binary);
putfile.open(dstfile, ios::out | ios::binary);
OutData *getdata = new OutData;
readfile.read((char*)getdata, sizeof(OutData));
vector DataDic; //建立字典
Dictionary *spacefirst = new Dictionary;
spacefirst->Index = 0;
spacefirst->lastChar = '0';
spacefirst->preIndex = 0;
//spacefirst->stringInDic
DataDic.push_back(*spacefirst);
while (!readfile.eof())
{
Dictionary *now = new Dictionary;
now->lastChar = getdata->lastChar;
now->Index = DataDic.size();
now->preIndex = getdata->preIndex;
vector<unsigned char> preString; //存储前一个的字符串
if (now->preIndex != 0)
{ //如果preIndex等于0那么此字符是新出现的字符,否则在前面字典中找
preString = FindPreString(DataDic, now->preIndex);
}
preString.push_back(now->lastChar);
now->stringInDic = preString; //获取此单词的字符串。
DataDic.push_back(*now);
for (int i = 0; i < preString.size(); i++)
{
putfile.write((char*)&preString[i], sizeof(unsigned char));
}
readfile.read((char*)getdata, sizeof(OutData));
}
readfile.close();
putfile.close();
}
vector<unsigned char> LZ78::FindPreString(vector DataDic, unsigned int preindex)
{
if (DataDic.size() < 1)
{
cout << "不能找到前一个字符串" << endl;
}
else
{
for (int i = 0; i < DataDic.size(); i++)
{
if (preindex == DataDic[i].Index)
{
return DataDic[i].stringInDic;
}
}
}
}
LZ-78Main.cpp
#include "LZ78.h"
#include
#include
int main()
{
LZ78 haha;
clock_t start, end;
start = clock();
haha.open("./KongFu.jpg"); //打开文件
haha.Press(); //压缩文件
end = clock();
cout << "压缩文件用时:" << endl << endl;
cout << double((end - start) / CLOCKS_PER_SEC) << "/s" << endl << endl;
start = clock();
LZ78 nothaha;
nothaha.Decode("./KongFu.jpg.lz", "KongFuout.jpg");
cout << "解压用时:" << endl << endl;
cout << double((start - end) / CLOCKS_PER_SEC) << "/s" << endl << endl;
getchar();
}
经过此次试验真的是收获了很多,首先是对各种编码的熟悉,从Huffman编码、Shannon Fano码、算数编码到LZ编码家族,了解了Zip,winrar等压缩软件的压缩原理,以及压缩算法从兴起到应用,里面还有一些版权纠纷的故事(LZW版权问题)。到现在的操作系统使用的压缩软件的情况,Windows主要的软件是winrar和zip, linux系统下主要是.gz和gzip软件。其原理都是基于DELLATE算法的。DELLATE算法是以Huffman编码和LZW编码为核心的。另一方面,从信息论角度细致的学习了各种编码的编码原理和译码方式,这些收获相信在今后的学习中都会有用的。这次实验不仅锻炼了编程能力,而且学习到很多编码领域的新知识。
参考文献:
《信息论与编码》傅祖芸、赵建中 电子工业出版社
C++实现Huffman文件编码和解码:http://www.cnblogs.com/matrix-r/p/3354887.html
zip压缩原理 http://blog.jobbole.com/76676/
Huffman对文件编码和解码http://blog.csdn.net/computer_liuyun/article/details/41446773
huffman编码原理与实现:http://blog.csdn.net/abcjennifer/article/details/8020695
二进制和ASCII编码: http://blog.csdn.net/sxhelijian/article/details/29594687
压缩编码的历史:http://blog.csdn.net/kimylrong/article/details/39405981
Lz78算法:http://www.cnblogs.com/aeexiaoqiang/p/6529375.html
算数编码: http://blog.csdn.net/adam_tu/article/details/7696455