【数据压缩5】Lab3 LZW编解码算法实现与分析
文章目录
一、实验名称
LZW 编解码算法实现与分析
二、实验目的
掌握词典编码的基本原理,用C/C++/Python等语言编程实现LZW解码器并分析编解码算法。
三、主要设备
安装 Windows 和 Visual Studio 等编程平台的个人计算机
四、实验内容和原理
1. LZW编码原理和实现算法
(1)原理
代号代替短语:LZW的编码思想是不断地从字符流中提取新的字符串,通俗地理解为新“词条”,然后用“代号”也就是码字表示这个“词条”。这样一来,对字符流的编码就变成了用码字code word去替换字符String,生成码字流且只输出码字流,从而达到压缩数据的目的。
动态生成词典,新词条等于旧词条加新字符:LZW编码需要从输入的数据中创建短语词典,LZW编码器通过管理这个词典完成输入(短语)与输出(短语的索引号)之间的转换。
词典在开始时初始化不能为空,必须包含字符流中所有单个字符,即在编码匹配时至少能找到长度为1的匹配串。
输入字符输出码字:LZW编码器的输入是字符流,字符流可以是用8位ASCII字符组成的字符串,而输出是用n位(例如12位)表示的码字流。
(2)算法实现流程图
(3)代码分析
输入:字符流fp
——> 输出:码字流bf
void LZWEncode(FILE* fp, BITFILE* bf) {
int character;// 当前字符C
int string_code;// 当前前缀P
int index;// 词典中的索引
unsigned long file_length;
fseek(fp, 0, SEEK_END);// 找到输入文件指针的位置
file_length = ftell(fp);
fseek(fp, 0, SEEK_SET);
BitsOutput(bf, file_length, 4 * 8);
// 1.初始化词典,前缀P初始化为空
InitDictionary();
string_code = -1;
// 2.当前字符C=字符流fp的下一个字符
while (EOF != (character = fgetc(fp))) {
// 3.查找P+C的代号
index = InDictionary(character, string_code);
if (0 <= index) { // string+character in dictionary
string_code = index;//更新前缀P=P+C
}
else { // string+character not in dictionary
output(bf, string_code);//输出当前前缀P相应的码字
if (MAX_CODE > next_code) { // free space in dictionary
AddToDictionary(character, string_code);// add string+character to dictionary
}
string_code = character;//P=C
}
}
// 输出当前前缀P相应的码字
output(bf, string_code);
}
2. LZW解码原理和实现算法
(1)原理
LZW解码算法开始时,译码词典和编码词典相同,包含所有可能的前缀根。
边解码边生成新词条,新词条等于旧词条加新字符
(2)算法实现流程图
(3)代码分析
输入:码字流bf
——> 输出:字符流fp
void LZWDecode(BITFILE* bf, FILE* fp) {
int character;// 当前string.CW的第一个字符C
int new_code, last_code;// 当前码字CW和前码字PW
int phrase_length;
unsigned long file_length;
file_length = BitsInput(bf, 4 * 8);//八位ASCII字符
if (-1 == file_length)
file_length = 0;
// 初始化词典
InitDictionary();
last_code = -1;
// 判断码字流中是否还有码字要译
while (0 < file_length) {
// 1.当前码字CW = 码字流的下一个(第一个)码字
new_code = input(bf);
// 2.判断 当前码字CW是否在词典中,并将相应内容写入输出字符流d_stack
// 不在 输出P+C
if (new_code >= next_code) {
d_stack[0] = character; // d_stack[0]=当前字符串string.CW 的第一个字符C
phrase_length = DecodeString(1, last_code);//d_stack[1]开始=P==string.PW
}
// 在 输出C
else {
phrase_length = DecodeString(0, new_code);// d_stack[0]开始=string.CW
}
// 3.当前字符C = string.CW的第一个字符
character = d_stack[phrase_length - 1];
// 4.输出 字符流d_stack到文件fp
while (0 < phrase_length) {
phrase_length--;
fputc(d_stack[phrase_length], fp);
file_length--;
}
// 5.新词条加入词典P+C == string.PW+C
if (MAX_CODE > next_code) {
AddToDictionary(character, last_code);
}
// 6.更新前码字PW=CW
last_code = new_code;
}
}
3. 词典的数据结构分析
词典树:树是动态建立的,每个节点但可拥有任意多个子节点
struct {
int suffix;//尾缀字符
int parent;//母节点
int firstchild;//第一个子节点
int nextsibling;//下一个兄弟节点
} dictionary[MAX_CODE + 1];
全局变量
int next_code;// 下一个节点
int d_stack[MAX_CODE]; // stack for decoding a phrase
(1)初始化词典
void InitDictionary(void) {
int i;
for (i = 0; i < 256; i++) {
dictionary[i].suffix = i;
dictionary[i].parent = -1;
dictionary[i].firstchild = -1;
dictionary[i].nextsibling = i + 1;
}
dictionary[255].nextsibling = -1;
next_code = 256;
}
(2)查找字典里是否有字符串
输入:当前字符C(character)和前缀P(string_code)
输出:在词典中的位置
int InDictionary(int character, int string_code) {
int sibling;
// 单个字符没有前缀,直接返回该字符
if (0 > string_code)
return character;
// 非单个字符,找第一个孩子节点
sibling = dictionary[string_code].firstchild;
// 有孩子或右侧兄弟节点
while (-1 < sibling) {
// 当前字符==孩子节点的尾缀字符(找到了)
if (character == dictionary[sibling].suffix)
return sibling;
// 没找到,就一直沿着下一个兄弟节点查下去(向右查)
sibling = dictionary[sibling].nextsibling;
}
// 没找到返回-1
return -1;
}
(3)新串加入词典
输入当前字符C(character)和前缀P(string_code)
//加入词典函数
void AddToDictionary(int character, int string_code) {
int firstsibling, nextsibling;
// 1. 单个字符已经有了,不用加入词典,直接退
if (0 > string_code)
return;
// 2. 添加
dictionary[next_code].suffix = character;// 尾缀字符=当前字符
dictionary[next_code].parent = string_code;// 母节点=前缀P
dictionary[next_code].nextsibling = -1;// 新词条没有右邻居
dictionary[next_code].firstchild = -1;// 新词条没有孩子
// 3. 新节点是左侧邻居的下一个节点,建立连接
firstsibling = dictionary[string_code].firstchild;
// 母节点(前缀)有孩子
if (-1 < firstsibling) {// the parent has child
nextsibling = firstsibling;
// 沿着右邻居查找下去
while (-1 < dictionary[nextsibling].nextsibling)
nextsibling = dictionary[nextsibling].nextsibling;
dictionary[nextsibling].nextsibling = next_code;
}
// 母节点(前缀)没有孩子,新节点第一个孩子
else {// no child before, modify it to be the first
dictionary[string_code].firstchild = next_code;
}
// 更新next_code,准备下一次
next_code++;
}
//解码的字符流
int DecodeString(int start, int code) {
int count;
count = start;// 开始下标
while (0 <= code) {// 有母节点循环
d_stack[count] = dictionary[code].suffix;// 当前下标的尾缀字符
code = dictionary[code].parent;// 跟新字典下标为其母节点
count++;
}
return count;// 输出字符长度
}
4. 当前码字在词典中不存在时应如何处理并解释原因
特殊情况:
此时:P=ab;C=a
判断:P+C = aba不在词典中
不在:P.index——>码流
P+C=aba——>词典
P=C=a
P+C=ab在词典中,继续累积P=ab,没有码字输出,没有新词条入词典
P+C=aba在词典中,继续累积P=aba,没有码字输出,没有新词条入词典
注意此时:词典中最后加入的词条
aba
,下一个码字输出就是aba
,刚生成就使用了
因此在解码端,遇到码字259
时,词典中并没有对应字符串,无法解码。
解决方法:
观察这类字符串的特点:首尾字符相同。
解决:将PW
对应字符串和CW
对应字符串的第一个字符拼接即可解出,并将其加入词典。
P=string.PW=ab;
C=string.CW=a; (aba 取第一个字符a)
P+C = aba——> 字符流
P+C = aba——> 词典
PW=CW=aba
五、实验步骤
-
首先调试LZW的编码程序,以一个文本文件作为输入,得到输出的LZW编码文件。
-
以实验步骤一得到的编码文件作为输入,编写LZW的解码程序。在写解码程序时需要对关键语句加上注释,并说明进行何操作。在实验报告中重点说明当前码字在词典中不存在时应如何处理并解释原因。
-
选择至少十种不同格式类型的文件,使用LZW编码器进行压缩得到输出的压缩比特流文件。对各种不同格式的文件进行压缩效率的分析。
主函数
int main(int argc, char** argv) {
FILE* fp;
BITFILE* bf;
// 参数数量小于4
if (4 > argc) {
fprintf(stdout, "usage: \n%s <o> <ifile> <ofile>\n", argv[0]);
fprintf(stdout, "\t<o>: E or D reffers encode or decode\n");
fprintf(stdout, "\t<ifile>: input file name\n");
fprintf(stdout, "\t<ofile>: output file name\n");
return -1;
}
// do encoding
if ('E' == argv[1][0]) {
fp = fopen(argv[2], "rb");//输入字符流
bf = OpenBitFileOutput(argv[3]);//输出码字流
if (NULL != fp && NULL != bf) {
LZWEncode(fp, bf);
fclose(fp);
CloseBitFileOutput(bf);
fprintf(stdout, "encoding done\n");
}
}
// do decoding
else if ('D' == argv[1][0]) {
bf = OpenBitFileInput(argv[2]);// 输入码字流
fp = fopen(argv[3], "wb");、// 输出字符流
if (NULL != fp && NULL != bf) {
LZWDecode(bf, fp);
fclose(fp);
CloseBitFileInput(bf);
fprintf(stdout, "decoding done\n");
}
}
// 其他格式不支持
else {
fprintf(stderr, "not supported operation\n");
}
return 0;
}
六、实验结果
1. LZW编码器
输入文本文件1.txt
输出编码文件1e.dat
编码前后文件大小对比,压缩成功
2. LZW解码器
输入上一步的编码文件e1.data
,输出解码文件d1.txt
3. 不同类型的文件压缩效率比较
输入原始文件
输出的编码文件
编码效率
类型 | 压缩前(KB) | 压缩后(KB) | 压缩效率 |
---|---|---|---|
txt | 9 | 6 | 0.666666667 |
xlsx | 36 | 52 | 1.444444444 |
rgb | 192 | 179 | 0.932291667 |
yuv | 96 | 77 | 0.802083333 |
doc | 337 | 290 | 0.860534125 |
284 | 268 | 0.943661972 | |
md | 11 | 8 | 0.727272727 |
mp4 | 350 | 256 | 0.731428571 |
bmp | 1013 | 1298 | 1.281342547 |
svg | 27 | 14 | 0.518518519 |
七、总结
LZW编码效率问题
- 某些类型的文件经过词典编码后,大小反而增加,比如实验样例中xlsx格式的表格。原因可能是因为文件的重复度不高,编码后的文件存储字典的标号,其需要的空间比源数据更大。
- 实验样例中svg格式压缩效率最好,文件内容就是前文“LZW解码的流程图”,图像构成、颜色等比较简单,空白较多,重复性较大,因此效果较好。
LZW编码的特点
优点
- LZW只需一遍扫描,具有自适应的特点
- 算法简单,便于快速实现(数字查找树/键树)
缺点
- 字符串重复概率低时,影响压缩效率,由输入字符流的统计特性决定,很难解决
- 词典中的字符串不再出现,影响压缩效率,需要继续探究如何更有效地更新词典
- 从词典中查找词条是算法中最费时的工作
遇到问题:
C4996 ‘fopen’: This function or variable may be unsafe. Consider using fopen_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
解决:
加 #define _CRT_SECURE_NO_DEPRECATE