上一篇文章给出了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;
int number = 0, size = 0;
char c[N];
long double 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
struct L
{
char ch;
int num;
double f;
};
void disp();
int search(vector<L> 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);
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<L>pt;
L temp;
temp.ch = str[0];
temp.num = 1;
temp.f = 0.0;
pt.push_back(temp);
for (int i = 1; i<count; i++)
{
temp.ch = str[i];
temp.num = 1;
temp.f = 0.0;
for (int j = 0; j<pt.size(); j++)
{
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.size(); i++)
{
pt[i].f = double(pt[i].num) / count;
}
int number = pt.size();
cout << "各字符概率如下:\n";
for (int i = 0; i<number; 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;
}
int search(vector<L> arch, char ch1, int n)
{
for (int i = 0; i<n; i++)
{
if (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;
int i, j = 0;
for (i = 0; i<number; i++)
{
if (str[0] == c[i]) break;
}
while (j<i)
{
Low += p[j++];
}
range = p[j];
High = Low + range;
for (i = 1; i<size; i++)
{
for (j = 0; j<number; j++)
{
if (str[i] == c[j])
{
if (j == 0)
{
low = Low;
high = Low + p[j] * range;
High = high;
range *= p[j];
}
else
{
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;
}
}
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;
long double temp;
long double sum[N];
sum[0] = 0.0;
for (int i = 1; i<number + 1; i++)
{
sum[i] = sum[i - 1] + p[i - 1];
}
for (int j = 0; j<size; j++)
{
for (int k = 0; k<number; k++)
{
if ((input>sum[k]) && (input<sum[k + 1]))
{
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; m<v.size(); m++)
{
cout << v[m];
}
cout << endl;
}
第四章:LZ编码的实现之LZ-78编码
一. LZ-78编码原理
经过查找资料,LZ编码并不是一种编码,而是一组编码,由LZ-77和LZ-78演变而来有很多种变形。所以这里我只选取了一种比较简单的算法LZ-78算法。
LZ-78编码算法是一种分段编码算法。算法的压缩过程非常简单。在压缩时维护一个动态词典Dictionary,其包括了历史字符串的index与内容。设信源符号集
A=a0,a1,a2...aq−1
共q个字符,设输入信源序列为
S1S2...Sn
编码就是将此序列分成不同的X段。分段的原则是:
1. 先取第一个符号作为第一段,然后再继续分段
2. 若出现有与前面相同符号时,就在添加进跟随后面的一个符号一起组成一个段,以使与前面的段不同。
3. 尽可能取最少个连通着的信源符号,并保证隔断都不相同。直至信源符号徐立结束。
这样,不同的段内的信源符号可看成一短语,可得不同段所对应的短语字典表。若编成二元码,段号用二进制表示,段号所需码长为
l=⌈logX(n)⌉
,X(n)是段号数。
二. LZ-78编码过程
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编码编程实现和性能分析
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<Dictionary> CurrentString, vector<unsigned char> DataDic, unsigned int &Index);
private:
vector<unsigned char> FindPreString(vector<Dictionary> 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 <iostream>
#include <fstream>
#include <vector>
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<Dictionary> CurrentString, vector<unsigned char> DataDic, unsigned int &Index);
private:
vector<unsigned char> FindPreString(vector<Dictionary> 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<Dictionary> 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;
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<Dictionary> 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<Dictionary> DataDic;
Dictionary *spacefirst = new Dictionary;
spacefirst->Index = 0;
spacefirst->lastChar = '0';
spacefirst->preIndex = 0;
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)
{
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<Dictionary> 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 <string>
#include <time.h>
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
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)