LZW编解码算法实现与分析
文章目录
前言
LZW编码即为第二类词典编码,其基本思路是从输入的数据中创建一个“短语词典 (dictionary)”。通过短语词典中对应的索引,将文件数据中出现的短语(字符或字符串)与词典的特定索引号进行映射表示。编码数据过程中当遇到已经在词典中出现的“短语”时,编码器就输出这个词典中的短语的“索引号”,而不是短语本身,从而达到压缩编码的目的。而在解码时,则与编码过程相同,在解码过程的同时逐步进行本地词典的更新,以达到解码的效果。
因此,LZW编解码方法可以在不需要将词典进行传输的情况下实现编解码,在文件数据重复率高的情况下实现压缩与编解码。
本实验则使用C语言实现LZW基本编解码过程,在给定的代码基础上进行理解,并补充解码步骤。
一、LZW编解码
1、概述
LZW算法就是通过建立一个词典(字符串表),用较短的代码来表示较长的字符串来实现压缩。其基本原理是:提取原始文本文件数据中的不同字符,基于这些字符创建一个编译表,然后用编译表中的字符的索引来替代原始文本文件数据中的相应字符,减少原始数据大小。应该注意到的是,我们这里的编译表不是事先创建好的,而是根据原始文件数据动态创建的,解码时还要从已编码的数据中还原出原来的编译表。
2、编码原理
LZW算法的编码原理在于对词典的更新和根据最词典进行映射变化的同步进行。其步骤包括:
① 将词典初始化为包含所有可能的单字符,同时定义前缀字符P。当前前缀字符串P初始化为空。
② 取下一个字符存入C,直到数据全部遍历完成。
③ 判断字符串P+C是否在词典中。
\qquad
■ P+C包含于词典中,则令前缀P更新为P+C,并返回步骤 ②
\qquad
■ P+C不包含于词典中,则输出与P相对应的码字W,并将P+C添加到词典中。最后更新P为C(P=C),返回步骤 ②。
3、解码原理
LZW算法的解码原理在于对词典的更新要和根据当前词典进行解码的同步进行。其步骤包括:
① 将词典初始化为包含所有可能的单字符。
② 取第一个码字作为CW。
③ 输出当前码字所对应的字符(串)。
④ 先前码字PW设置为PW=CW。
⑤ 读取下一个码字作为CW。
⑥ 判断当前码字CW是否在词典中。
\qquad
■ 当前码字包含于词典中,则设置先前字符P为PW对应的字符(串),设置当前字符C为CW对应字符串的第一个字符,并将P+C更新至词典。
\qquad
■ 当前码字不包含于词典中,则设置先前字符P为PW对应的字符(串),设置当前字符C为CW对应字符串的第一个字符,并将P+C输出为字符流,同时更新至词典。
二、实验步骤
1、词典映射函数
首先对在解码过程中,按照词典解码并将所得字符流进行存储的函数进行编写,同时函数返回解码所得字符流的长度,如下。
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;
}
\qquad
2、解码过程及问题分析
若使用最为基本的词典映射进行解码,基本代码行如下。
phrase_length = DecodeString(0, new_code); //正常按照词典索引解码
但在解码过程中,如果遇到类似ababa的穿插重复的情况,即当前编码应得的字符串与之前相邻的祖父穿构成了拼接与重复的关系,则会出现编码过程中当前码字在词典中无法找到的情况。所以在解码过程中,需要对词典映射解码进行一个判断。改良后的代码模块如下。
if (new_code < next_code) { //对词典中是否存在当前码字做判断
phrase_length = DecodeString(0, new_code); //正常按照词典索引解码
}else {
d_stack[0] = character; //对词典中不存在码字的情况,选取前一码字代表字符(串)的最后一位为当前解码字符的第一位
phrase_length = DecodeString(1, last_code); //再对当前码字代表的字符(串)的剩余字符进行解码
}
故完整的解码函数如下。
void LZWDecode(BITFILE* bf, FILE* fp) {
//填充:解码函数
int character; //要解码得到的字符C
int new_code; //压缩文件中待解码的当前码字CW
int last_code; //压缩文件中待解码的先前码字PW
int phrase_length; //解码后字符的长度
unsigned long file_length; //解码文件的大小
file_length = BitsInput(bf, 4 * 8); //因编码过程中前32bit为文件编码前大小,故从特定位置开始读
if (-1 == file_length)
file_length = 0;
//初始化解码词典(树)
InitDictionary();
last_code = -1;
//开始解码
while (0 < file_length) {
new_code = input(bf); //读入当前位置码字
if (new_code < next_code) { //对词典中是否存在当前码字做判断
phrase_length = DecodeString(0, new_code); //正常按照词典索引解码
}else {
d_stack[0] = character; //对词典中不存在码字的情况,选取前一码字代表字符(串)的最后一位为当前解码字符的第一位
phrase_length = DecodeString(1, last_code); //再对当前码字代表的字符(串)的剩余字符进行解码
}
character = d_stack[phrase_length - 1]; //存储当前字符
//将此轮解码后的字符写入文件(phrase_length与file_length分别起到当前字符串内容和文件应有大小的计数功能)
while (0 < phrase_length) {
phrase_length--;
fputc(d_stack[phrase_length], fp);
file_length--;
}
//将码字与字符映射更新至词典中
if (MAX_CODE > next_code) {
AddToDictionary(character, last_code);
}
last_code = new_code; //更新PW,解码过程继续推进
}
三、程序运行
1、简单txt文件
编写txt_test.txt文件,其大小为42字节。
使用FlexHEX程序对文件进行16进制查看。
可以看到此时文件中的每个字符均由一个字节构成,同时重复率较高
项目生成后,调用exe文件进行编码,并编码至txt_encode.txt文件中。
对编码后的文件进行十六进制查看,此时存储的即为词典映射的结果。
而压缩后文件大小为54字节,应该是文件中的重复短语还未到达一定数量,使得其编码后大小不降反增。
对编码后的文件再进行解码。
得到与原文件相同的文件。
2、改进txt文件
对于编写txt文件较小,且重复率较少造成的反压缩效应,添置了众多重复元素,并加入了中文字符进行实验。如图所示。源文件大小为552字节。
进行压缩后,压缩的文件大小为532字节,达到了压缩的效果。
\qquad
之后又将txt文件内容进行了进一步扩充,并进行了其他格式文件的实验。
\qquad
3、其他类型文件测试
文件名 | 文件格式 | 源文件大小 | 输出文件大小 | 压缩比 |
---|---|---|---|---|
txt_test | txt | 778字节 | 726字节 | 1.072 |
docx_test | docx | 14414 字节 | 21882 字节 | 0.659 |
py_test | py | 4421 字节 | 3646 字节 | 1.213 |
pdf_test | 209968 字节 | 268088 字节 | 0.783 | |
pptx_test | pptx | 1355015 字节 | 1665844 字节 | 0.813 |
csv_test | csv | 307793 字节 | 90346 字节 | 3.407 |
m4a_test | m4a | 173982 字节 | 229234 字节 | 0.759 |
mp3_test | mp3 | 9407817 字节 | 11415758字节 | 0.824 |
png_test | png | 3182842 字节 | 3967256 字节 | 0.802 |
jpg_test | jpg | 454710 字节 | 564098 字节 | 0.806 |
yuv_test | yuv | 622080000 字节 | 371583976 字节 | 1.674 |
mp4_test | mp4 | 3363404 字节 | 3375860 字节 | 0.996 |
在对不同格式文件的测试中可以看到,LZW方法对于实验中所选文件,除了内部数据的重复性较高的自己编辑的txt文件、csv文件、py文件和yuv文件之外,其他数据重复性较低的文件均会产生反压缩的效果。
\qquad
实验代码
1、bitio.h 头文件
#pragma once
#ifndef __BITIO__
#define __BITIO__
#include <stdio.h>
typedef struct {
FILE* fp;
unsigned char mask;
int rack;
}BITFILE;
BITFILE* OpenBitFileInput(char* filename);
BITFILE* OpenBitFileOutput(char* filename);
void CloseBitFileInput(BITFILE* bf);
void CloseBitFileOutput(BITFILE* bf);
int BitInput(BITFILE* bf);
unsigned long BitsInput(BITFILE* bf, int count);
void BitOutput(BITFILE* bf, int bit);
void BitsOutput(BITFILE* bf, unsigned long code, int count);
#endif // __BITIO__
2、bitio.c 读写文件
#pragma once
#pragma warning(disable : 4996)
#include <stdlib.h>
#include <stdio.h>
#include "bitio.h"
BITFILE* OpenBitFileInput(char* filename) {
BITFILE* bf;
bf = (BITFILE*)malloc(sizeof(BITFILE));
if (NULL == bf) return NULL;
if (NULL == filename) bf->fp = stdin;
else bf->fp = fopen(filename, "rb");
if (NULL == bf->fp) return NULL;
bf->mask = 0x80;
bf->rack = 0;
return bf;
}
BITFILE* OpenBitFileOutput(char* filename) {
BITFILE* bf;
bf = (BITFILE*)malloc(sizeof(BITFILE));
if (NULL == bf) return NULL;
if (NULL == filename) bf->fp = stdout;
else bf->fp = fopen(filename, "wb");
if (NULL == bf->fp) return NULL;
bf->mask = 0x80;
bf->rack = 0;
return bf;
}
void CloseBitFileInput(BITFILE* bf) {
fclose(bf->fp);
free(bf);
}
void CloseBitFileOutput(BITFILE* bf) {
// Output the remaining bits
if (0x80 != bf->mask) fputc(bf->rack, bf->fp);
fclose(bf->fp);
free(bf);
}
int BitInput(BITFILE* bf) {
int value;
if (0x80 == bf->mask) {
bf->rack = fgetc(bf->fp);
if (EOF == bf->rack) {
fprintf(stderr, "Read after the end of file reached\n");
exit(-1);
}
}
value = bf->mask & bf->rack;
bf->mask >>= 1;
if (0 == bf->mask) bf->mask = 0x80;
return((0 == value) ? 0 : 1);
}
unsigned long BitsInput(BITFILE* bf, int count) {
unsigned long mask;
unsigned long value;
mask = 1L << (count - 1);
value = 0L;
while (0 != mask) {
if (1 == BitInput(bf))
value |= mask;
mask >>= 1;
}
return value;
}
void BitOutput(BITFILE* bf, int bit) {
if (0 != bit) bf->rack |= bf->mask;
bf->mask >>= 1;
if (0 == bf->mask) { // eight bits in rack
fputc(bf->rack, bf->fp);
bf->rack = 0;
bf->mask = 0x80;
}
}
void BitsOutput(BITFILE* bf, unsigned long code, int count) {
unsigned long mask;
mask = 1L << (count - 1);
while (0 != mask) {
BitOutput(bf, (int)(0 == (code & mask) ? 0 : 1));
mask >>= 1;
}
}
#if 0
int main(int argc, char** argv) {
BITFILE* bfi, * bfo;
int bit;
int count = 0;
if (1 < argc) {
if (NULL == OpenBitFileInput(bfi, argv[1])) {
fprintf(stderr, "fail open the file\n");
return -1;
}
}
else {
if (NULL == OpenBitFileInput(bfi, NULL)) {
fprintf(stderr, "fail open stdin\n");
return -2;
}
}
if (2 < argc) {
if (NULL == OpenBitFileOutput(bfo, argv[2])) {
fprintf(stderr, "fail open file for output\n");
return -3;
}
}
else {
if (NULL == OpenBitFileOutput(bfo, NULL)) {
fprintf(stderr, "fail open stdout\n");
return -4;
}
}
while (1) {
bit = BitInput(bfi);
fprintf(stderr, "%d", bit);
count++;
if (0 == (count & 7))fprintf(stderr, " ");
BitOutput(bfo, bit);
}
return 0;
}
#endif
3、lzw.c 编解码主函数文件
#pragma once
/*
* Definition for LZW coding
*
* vim: ts=4 sw=4 cindent nowrap
*/
#pragma warning(disable : 4996)
#include <stdlib.h>
#include <stdio.h>
#include "bitio.h"
#define MAX_CODE 65535
struct {
int suffix;
int parent, firstchild, nextsibling;
} dictionary[MAX_CODE + 1];
int next_code; //当前词典中的最大码字下标
int d_stack[MAX_CODE]; // 解码时的缓存
#define input(f) ((int)BitsInput( f, 16))
#define output(f, x) BitsOutput( f, (unsigned long)(x), 16)
int DecodeString(int start, int code);
void InitDictionary(void);
void PrintDictionary(void) {
int n;
int count;
for (n = 256; n < next_code; n++) {
count = DecodeString(0, n);
printf("%4d->", n);
while (0 < count--) printf("%c", (char)(d_stack[count]));
printf("\n");
}
}
int DecodeString(int start, int code) {
//填充:对当前的码字进行解码,并返回解码后的字符(串)长度
int count;
count = start;
//在未到达根节点的前提下,逐个取当前码字回退过程中的字符(并组成字符串),即得到编码字符(串)
while (0 <= code) { //d_stack中所存的是倒序的解码字符串
d_stack[count] = dictionary[code].suffix;
code = dictionary[code].parent;
count++;
}
return count;
}
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; //词典编码所处的下一个序号
}
/*
* Input: string represented by string_code in dictionary,
* Output: the index of character+string in the dictionary
* index = -1 if not found
*/
int InDictionary(int character, int string_code) {
int sibling;
if (0 > string_code) return character; //如果为初始化时,直接单个字符返回
sibling = dictionary[string_code].firstchild; //寻找当前P的第一个孩子节点
while (-1 < sibling) { //从其兄弟节点中进行查找
if (character == dictionary[sibling].suffix) return sibling; //找到了直接返回
sibling = dictionary[sibling].nextsibling; //找不到再取下一个兄弟节点
}
return -1; //完全没找到,返回小于0的数
}
void AddToDictionary(int character, int string_code) {
int firstsibling, nextsibling;
if (0 > string_code) return;
dictionary[next_code].suffix = character; //自身为C,填入最大编码序号的位置
dictionary[next_code].parent = string_code; //母亲节点为P
dictionary[next_code].nextsibling = -1;
dictionary[next_code].firstchild = -1; //因为是最新,其下一个兄弟节点和孩子节点为不存在
firstsibling = dictionary[string_code].firstchild; // 取其母节点的第一个孩子节点,来判断是否有前面的兄弟节点
if (-1 < firstsibling) { //若本来就有
nextsibling = firstsibling;
while (-1 < dictionary[nextsibling].nextsibling)
nextsibling = dictionary[nextsibling].nextsibling;
dictionary[nextsibling].nextsibling = next_code; //新字符为前缀母节点的最后一个子节点
}
else {
dictionary[string_code].firstchild = next_code; //如果没有,那其本身就是最大序号
}
next_code++;
}
/* 编码 */
void LZWEncode(FILE* fp, BITFILE* bf) {
int character; //当前字符C
int string_code; //编码结果,也充当前向字符P
int index; //当前编码的位置(一开始从256开始)
unsigned long file_length;
fseek(fp, 0, SEEK_END);
file_length = ftell(fp);
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); //看P+C是否已经在表中,index存放查找结果
if (0 <= index) {
string_code = index; //P+C在词典中,使P=P+C,回到while继续往后看
}
else { //P+C不在词典中
output(bf, string_code); //输出P的编码
if (MAX_CODE > next_code) {
AddToDictionary(character, string_code); //将P+C增加到词典中
}
string_code = character; //P=C,回到while继续往后看
}
}
output(bf, string_code); //最后一个输出即可
}
/* 解码 */
void LZWDecode(BITFILE* bf, FILE* fp) {
int character; //要解码得到的字符C
int new_code; //待解码的当前码字CW
int last_code; //压缩文件中待解码的先前码字PW
int phrase_length; //解码后字符的长度
unsigned long file_length;
file_length = BitsInput(bf, 4 * 8); //因编码过程中前32bit为文件编码前大小,故从特定位置开始读
if (-1 == file_length)
file_length = 0;
//初始化解码词典(树)
InitDictionary();
last_code = -1;
//开始解码
while (0 < file_length) {
new_code = input(bf); //读入当前位置码字
if (new_code < next_code) { //对词典中是否存在当前码字做判断
phrase_length = DecodeString(0, new_code); //正常按照词典索引解码
//此时d_stack为倒序所存的已知解码字符串
}else {
d_stack[0] = character; //对词典中不存在码字的情况,选取前一码字代表字符(串)的第一位为当前解码字符的最后一位(PW+C,C=PW[1])
phrase_length = DecodeString(1, last_code); //再对当前码字代表的字符(串)的剩余字符进行解码(取PW)
}
character = d_stack[phrase_length - 1]; //C为CW[1]或PW[1]
//将此轮解码后的字符写入文件(phrase_length与file_length分别起到当前字符串内容和文件应有大小的计数功能)
while (0 < phrase_length) { //注:在d_stack缓存中,当前解码的字符串应是倒序存入的
phrase_length--;
fputc(d_stack[phrase_length], fp);
file_length--;
}
//将码字与字符映射更新至词典中
if (MAX_CODE > next_code) {
AddToDictionary(character, last_code);
}
last_code = new_code; //更新PW,解码过程继续推进
}
}
int main(int argc, char** argv) {
FILE* fp;
BITFILE* bf;
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;
}
if ('E' == argv[1][0]) { // do encoding
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");
}
}
else if ('D' == argv[1][0]) { // do decoding
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 { // otherwise
fprintf(stderr, "not supported operation\n");
}
return 0;
}