数据压缩_实验三_LZW编解码器

LZW编解码算法实验

一、实验原理

1、基本思想

LZW的编码思想是不断地从字符流中提取新的字符串,通俗地理解为新“词条”,然后用“代号”也就是码字表示这个“词条”。这样一来,对字符流的编码就变成了用码字去替换字符流,生成码字流,从而达到压缩数据的目的。LZW编码是围绕称为词典的转换表来完成的。

LZW编码器和解码器会在接受数据时动态地创建字典,编码器和解码器也会产生相同的字典。

2、LZW编码原理

在这里插入图片描述

注意:如果待压缩的数据没有任何可重复的结构,那么使用字典条目中的新编码的可能性很小,这会导致数据膨胀而不是压缩。

3、LZW解码原理

在这里插入图片描述

注意:若出现形如“abba”的特殊情况,解码时CW可能不存在于当前词典中,但观察此类情况可以发现,索引对应字符串具有最后字符为前缀字符串首字符特点。对此,可依据流程图中第二分支进行操作。x

4、数据结构分析

截屏2020-04-26上午8.46.37

树是动态建立的,且树种每个节点可能存在多个子节点。因此数据结构应设计成一个节点可拥有任意个子节点,但无需为其预留空间。

截屏2020-04-26上午8.26.20

树用数组dict[]表示,数组下标用pointer表示,所以dict[pointer]表示一个节点。树的结构体定义如下:

struct {
	int suffix;
	int parent, firstchild, nextsibling;
} dictionary[MAX_CODE + 1];

二、主要模块分析

1、词典初始化

void InitDictionary(void) {
	int i;
	// 将单个字符以Ascii形式存入词典
	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、编码中查找字符串是否存在词典内

三种情况:

  • 无前缀字符,即字符串内仅单个字符,直接返回尾缀字符;
  • 有前缀字符,找出前缀字符对应的第一个孩子节点,逐个匹配兄弟节点直至找到为止,返回索引值;
  • 有前缀字符,上一种情况遍历所有兄弟节点后都没有可以匹配的兄弟节点。返回-1,即字符串不存在词典内。
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;
	}
	return -1;
}

3、添加新字符串至词典

void AddToDictionary(int character, int string_code) {
	int firstsibling, nextsibling;
	if (0 > string_code) return;
	//设置新结点,从256开始
	dictionary[next_code].suffix = character;// 尾缀字符W
	dictionary[next_code].parent = string_code;// 前缀字符p
	dictionary[next_code].nextsibling = -1;
	dictionary[next_code].firstchild = -1;
	// 前缀字符p的第一个孩子结点
	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++;
}

4、解码时存储字符串

由于编码后文件存储字符串的索引,故输出对应字符串时应先自叶节点至根结点倒序将逐个字符存入d_stack[](输出时由数组最后一个成员开始打印)。

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;
}

5、LZW编码

void LZWEncode(FILE* fp, BITFILE* bf) {
	int character;
	int string_code;
	int index;
	unsigned long file_length;
	//用fseek函数把位置指针移到文件尾
	//再用ftell函数获得这时位置指针距文件头的字节数
	//这个字节数就是文件的长度
	fseek(fp, 0, SEEK_END);
	file_length = ftell(fp); //ftell用来获取文件的当前读写位置
	fseek(fp, 0, SEEK_SET);
	BitsOutput(bf, file_length, 4 * 8);
	InitDictionary(); // 词典初始化
	string_code = -1;
  // 循环至数据流结束
	while (EOF != (character = fgetc(fp))) {
		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);	// 输出前缀字符对应索引
			if (MAX_CODE > next_code) {	// free space in dictionary
				// add string+character to dictionary
				AddToDictionary(character, string_code);
			}
			string_code = character;	// P <- C
		}
	}
  // 输出最后一个字符串
	output(bf, string_code);
}

6、LZW解码

void LZWDecode(BITFILE* bf, FILE* fp) {
	int character;
	int new_code, last_code;
	int phrase_length;
	unsigned long file_length;
  
	file_length = BitsInput(bf, 4 * 8);	// 获取文件长度
	if (-1 == file_length) file_length = 0;
	InitDictionary(); // 字典初始化
	last_code = -1;
	while (file_length > 0) {
		new_code = input(bf);	// 读入一个新字符
		if (new_code<next_code) {	// 存在字典中
			phrase_length = DecodeString(0, new_code);	// 将字符串压入堆栈
		}
		else {	// 不存在字典中 abba情况
			d_stack[0] = character;	// 将上一个字符串末尾字符作为作为新字符串末尾字符
			phrase_length = DecodeString(1, last_code);
		}
		character = d_stack[phrase_length - 1];	// 每次循环均更新character为上一个字符串末尾字符
		while (phrase_length > 0) {	//	从堆栈逐个压出字符,打印至输出文件
			phrase_length--;
			fputc(d_stack[phrase_length], fp);
			file_length--;
		}
		if (next_code < MAX_CODE) {	// add the new phrase to dictionary
			AddToDictionary(character, last_code);
		}
		last_code = new_code;	// 	PW <- CW
	}
}

三、实验结果

1、编码简单字符串

编码前截屏2020-04-26上午9.10.55
编码后截屏2020-04-26上午9.11.03
解码后截屏2020-04-26上午9.11.11

2、编码其他文件类型及压缩率分析

截屏2020-04-26上午9.46.15

文件类型编码前/KB编码后/KB压缩率
png(图像)9301151123.76%
jpg(图像)6798146.27%
avi(视频)11621388119.45%
cif(视频)3564211159.23%
pdf(文档)1818418330100.80%
ppt(文档)11391228107.81%
txt(文档)2288143462.67%
doc(文档)63613921.86%
xls(文档)873944.83%
caj(文档)54386329116.38%

由表可知,对于不同文件类型,LZW编码的压缩率各不相同。其中,对于doc、txt、xls等文本文件,LZW编码压缩效果最好。对于图像、视频文件,LZW编码压缩效果较差。

分析原因,LZW编码基于重复字符,对于数据中重复率较低的文件,需要不断创建大量新词条存入词典,实际输出压缩文件即便存储的是词条索引,也与源文件数据量差别不大。

与此同时,程序中的LZW编解码器,读入8bit输出16bit索引,在压缩效率不高的情况下,压缩后文件大小反而可能增加。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值