[实验三]LZW 编解码算法实现与分析

2023-11-19

目录

一、LZW算法

1.1 编码步骤

1.2 解码步骤

1.3 关于有可能出现当前码字CW不在词典中的情况说明

二、代码实现

2.1 程序说明

2.2 数据结构

2.3 bitio.h

2.4 bitio.c

2.5 lzw.c

三、实验测试

3.1 文本文档测试

 3.2 多类型文件测试

四、总结

一、LZW算法

LZW属于第二类词典编码,第二类词典编码的思想是企图从输入的数据中创建一个“短语词典 (dictionary of the phrases)”,这种短语可以是任意字符的组合。编码数据过程中当遇到已经在词典中出现的“短语”时,编码器就输出这个词典中的短语的“索引号”,而不是短语本身。

LZW算法的编码过程中不断读取被编码的字符串内容,使用词典中的索引来替换连续的字符串,词典一开始初始化后必须包含可能在字符流出现的所有单个字符,即在编码匹配时,至少可以在词典中找到长度为1的匹配串。在编码的过程中逐渐向词典中加入字符串,而后遇到相同的字符串只需用该字符串在词典中的索引来替代即可,索引即为编码后得到的码字,这就是LZW算法的核心思想,在压缩有大量相同的字符串内容的文件时非常有效。

LZW的解码过程与编码过程一样需要先初始化词典,在解码的过程中一边从词典中找到码字对应的字符串一边根据解码得到的字符串更新词典。LZW的编解码过程都是在同步建立完善词典的过程。

下面介绍具体的编解码步骤。

1.1 编码步骤

步骤1:将词典初始化为包含所有可能的单字符,当前前缀字符串P初始化为空。

步骤2:当前字符C=字符流中的下一个字符。

步骤3:判断P+C是否在词典中:

                如果“是”,令P=P+C,返回到步骤2。

                如果“否”,则:

                        输出与当前前缀P相对应的码字W;

                        将P+C添加到词典中;

                        令P=C,并返回到步骤2。

1.2 解码步骤

步骤1:将词典初始化为包含所有可能的单字符。

步骤2:令当前码字CW=码字流中的第一个码字。

步骤3:输出当前码字CW对应的字符串string.CW到码字流。

步骤4:令先前码字PW=当前码字CW。

步骤5:令当前码字CW=码字流中的下一个码字。

步骤6:判断当前码字CW是否在词典中:

                如果“是”,则:

                        当前前缀字符串P=先前码字PW对应的字符串string.PW;

                        当前字符C=当前码字CW对应的字符串string.CW的第一个字符;

                        输出当前码字CW对应的字符串string.CW到字符流;

                        将字符串P+C添加到词典中。

                如果“否”,则:

                        当前前缀字符串P=先前码字PW对应的字符串string.PW;

                        当前字符C=先前码字PW对应的字符串string.PW的第一个字符;

                        输出P+C到字符流;

                        将字符串P+C添加到词典中。

步骤7:判断码字流中是否还有码字要译:

                如果“是”,则返回步骤4。

                如果“否”,则结束。

1.3 关于有可能出现当前码字CW不在词典中的情况说明

考虑对以下字符串进行编码

位置 1 2 3 4 5 6 7 8 9
字符 a b b a b a b a c

假设一开始词典已经初始化了所有单字符,对其进行编码,这里直接用码字对应的字符串表示码字,则编码后的码字应为:

位置 1 2 3 4 5 6
码字 a b b ab aba c

LZW的解码和编码过程一样需要动态更新词典,例如当解码位置2后,就会在词典中添加字符串"ab",解码位置3后,会在词典中添加字符串"bb"。

而当读到位置5时,会发现词典中没有对应的码字,因为此时"aba"还没有添加到字典中,也就无法对位置5解码。按照正常逻辑,只有当解码位置5后字符串"aba"才会添加到词典中

原因是位置5字符串的最后一个字符与第一个字符相同,而位置5字符串的第一个字符和位置4字符串连接起来恰好和位置5字符串完全相同。

也就是说编码器在编码原始字符串的第4到第8个字符时,刚把"aba"添加到词典后立刻就又从字符串中读到了"aba",因此就直接用上了"aba"的码字,然而解码器始终慢一步,因此造成了解码码字不在词典中的情况。

而幸运的是,LZW解码过程中的特殊情况只有这一种,因此在遇到码字不在词典中的情况可以肯定一定是遇上了当前编码字符串刚好是上一个被编码字符串再拼接上第一个字符的情况,因此只用在解码流程中的步骤6添加对这种情况的特殊处理即可。

二、代码实现

2.1 程序说明

本实验程序采用C语言编写,共包含三个文件(bitio.h、bitio.c、lzw.c),编译完成后会得到对应的可执行文件lzw.exe,打开cmd控制台,切换至lzw.exe文件所在目录,通过输入命令对文件进行编解码操作。

命令格式为:

lzw.exe 参数1 参数2 参数3

参数1的值为“E”或“D”,表示执行编码或解码操作

参数2为输入文件路径,即被编码或解码的文件

参数3为输出文件路径,即编码或解码后的文件

注意事项:

  • 本程序中设置每个码字占用16bit,即词典最多支持保存2^16=65536个字符串
  • 编码后的文件前32bit用于存放文件编码前大小,即最大支持2^32=4,294,967,296B(约4096GB)的文件

2.2 数据结构

为方便对词典进行添加和查询的操作,本实验程序中为词典定义了树状的数据结构,如下图所示。在词典树中,每个节点上会存放一个字符,每一个词典的索引会对应到树中的一个节点,由该节点的父母节点的字符及该节点本身的字符依次连接构成索引对应的字符串,例如在下图中左下角的节点"e"所对应的字符串即为"aae"。

 代码中定义了如下的结构体来构建词典树

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

suffix表示该节点本身的字符

parent表示父母节点

firstchild表示子节点

nextsibling表示下一个兄弟节点

2.3 bitio.h

该文件为头文件,用于声明函数的信息。

#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.4 bitio.c

该文件定义了用于读写码字的函数。

BitsInput()函数用于读取文件中的码字。

BitsOutput()函数用于将码字写入文件。

#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

2.5 lzw.c

该文件为程序中的核心文件,定义了编解码的相关函数,详细的过程已经在代码中注释。

#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]; // stack for decoding a phrase

#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) {//该函数将字符串写入d_stack,便于后续写入至输出文件,同时返回字符串长度
    int count = start;
    while(code>=0){
        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;
    while (-1 < sibling) {
        if (character == dictionary[sibling].suffix) return sibling;
        sibling = dictionary[sibling].nextsibling;
    }
    return -1;
}

void AddToDictionary(int character, int string_code) {
    int firstsibling, nextsibling;
    if (0 > string_code) return;
    dictionary[next_code].suffix = character;
    dictionary[next_code].parent = string_code;
    dictionary[next_code].nextsibling = -1;
    dictionary[next_code].firstchild = -1;
    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++;
}

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);//将文件大小写入文件bf,文件大小共占32个bit
    InitDictionary();//初始化词典树
    string_code = -1;//当前还没有开始读取文件数据,前缀字符串为空,值记为-1
    while (EOF != (character = fgetc(fp))) {//依次读取输入文件中的字符,直到读到文件结束符
        index = InDictionary(character, string_code);//获取P+C在词典中的索引
        if (0 <= index) {    // P+C在词典中,将前缀字符串置为P+C
            string_code = index;
        } else {    // P+C不在词典中
            output(bf, string_code);//将前缀字符串的词典索引写入文件bf,每个索引占16bit
            if (MAX_CODE > next_code) {    // 由于每个索引占16bit,词典最多可以存放2^16=65536个词,判断词典是否写满
                // 若词典还没有写满,将P+C写入词典
                AddToDictionary(character, string_code);
            }
            string_code = character;//将前缀字符串置为C
        }
    }
    output(bf, string_code);//读取到文件结束符,将前缀字符串的词典索引写入文件bf
}

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);
    if (-1 == file_length) {
        file_length = 0;
    }
    InitDictionary();
    last_code = -1;//先前码字为空
    while (0 < file_length) {//判断是否已经译码完成
        new_code = input(bf);
        if (new_code >= next_code) {//若码字不在字典中
            d_stack[0] = character;//输出的字符串最后一个字符为C
            phrase_length = DecodeString(1, last_code);//将
        } else {
            phrase_length = DecodeString(0, new_code);
        }
        character = d_stack[phrase_length - 1];//当前字符为先前码字PW对应的字符串的第一个字符
        while (0 < phrase_length) {//将字符串写入文件
            phrase_length--;
            fputc(d_stack[phrase_length], fp);//每次写入字符串中的一个字符
            file_length--;
        }
        if (MAX_CODE > new_code) {
            AddToDictionary(character, last_code);//将字符串添加到词典中
        }
        last_code = new_code;//令先前码字PW=当前码字CW
    }
}


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

三、实验测试

3.1 文本文档测试

新建文本文档input.txt用于测试,文档内容如下:

该文档大小为104字节,将其压缩以测试压缩效果。

输入指令:

LZW.exe E input.txt code.txt

而后程序对input.txt进行编码,得到code.txt文件

编码后的code.txt文件大小为70字节,相比源文件input.txt数据量有减少。

打开code.txt,由于此时的内容为编码后的符号,因此无法直接阅读。

接下来将code.txt解码,输入指令:

LZW.exe D code.txt output.txt

而后程序对code.txt解码,得到output.txt文件,打开output.txt文件,内容与编码前的input.txt文件内容一致。

 3.2 多类型文件测试

下面将多种不同类型文件进行编码,检查编码后的压缩效果,这里用压缩比来作为评价压缩效果的客观指标,压缩比的计算方式为:

压缩比 = 输入文件大小/输出文件大小

各文件压缩结果如下:

序号 文件格式 输入文件大小 输出文件大小 压缩比
1 pdf 273KB 302KB 0.904
2 xlsx 11KB 16KB 0.688
3

doc

49KB 54KB 0.907
4 png 14KB 24KB 0.583
5 cif 3564KB 2111KB 1.688
6 yuv 732KB 96KB 7.625
7 mp4 97.5MB 119MB 0.819
8 bmp 676KB 510KB 1.325
9 m4a 38KB 68KB 0.5588
10 mp3 16.3MB 19.9MB 0.819
11 wav 20.3MB 25.7MB 0.790

四、总结

LZW是一种简单的词典编码方法,适用于对数据重复率高的文件进行编码,例如yuv或bmp格式的图像文件,这种类型的图像文件由于未经压缩,连续的像素值在文件数据流中经常重复出现,LZW对这一类的文件压缩编码效果较好。

然而对于已经压缩过的文件格式类型,LZW算法的表现稍差,有时非但不能进一步压缩,反而会导致编码后的数据量更大,LZW算法的适用场景比较局限。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

[实验三]LZW 编解码算法实现与分析 的相关文章

随机推荐

  • 成功解决VS编译时提示“已经在 LIBCMT.lib(xxx) 中定义“

    报错信息 解决方法 在项目右击 gt 属性 gt 连接器 gt 命令行 gt 附加选项中 添加 force
  • 【小程序】使用wxParse解析html

    小程序在开发时 读取到服务器的内容是html格式的 因小程序不支持html格式的内容显示的 因此要对html格式的内容进行编译 可以通过wxParse来实现 wxParse下载地址 实现方法 将下载下来的wxParse文件夹复制到开发项目的
  • Unity(纯C语言单元测试框架!不是那个Unity3d)入门文档

    译者注 译者博客 http blog csdn net lin strong 转载请保留这条 此为Unity手册的翻译 仅供学习交流使用 请勿用于商业用途 翻译的资料是公开的 在docs UnityGettingStartedGuide m
  • 计算员工工资

    请编写一个程序 可以读取一名员工的员工编号 本月工作总时长 小时 以及时薪 并输出他的工资条 工资条中包括员工编号和员工月收入 输入格式 输入包含两个整数和一个浮点数 分别代表员工编号 工作时长以及时薪 每个数占一行 输出格式 输出共两行
  • Unreal Engine4蓝图编程学习(一)

    学习内容主要介绍了蓝图进行对象交互 升级玩家技能 升级AI敌人 跟踪游戏状态完成游戏体验等内容 内容来源于 Unreal Engine4蓝图可视化编程 书籍为2017年 与现在版本有一定区别 一 制作移动标靶 1 1 首先 我们想先创建一个
  • mysql database uri,未设置SQLALCHEMY_DATABASE_URI

    I tried to work with CURD operation using Flask and SQLAlchemy But getting Error while connecting to database Here is th
  • springboot+vue教室图书馆预约管理系统、

    下载地址 https download csdn net download ouyangxiaobai123 22176771 项目介绍 springboot vue教室图书馆预约管理系统 系统说明 聪慧物联网教室预定系统 后台系统 项目简
  • 多维数组变成一维数组

    这个问题来源于一个朋友曾经问过我的问题 当时是一个二维数组变成一维数组 后面我想整理一下 整理一个多维 并且是不定维的数组 一 二维数组变成一维数组 1 遍历数组 将元素一个个放入新数组 结果 如果元素不是数组 将会报错 下面是改良版 这样
  • 信号量和自旋锁

    信号量和自旋锁 为了避免并发 防止竞争 内核提供了一组同步方法来提供对共享数据的保护 我们的重点不是介绍这些方法的详细用法 而是强调为什么使用这些方法和它们之间的差别 Linux 使用的同步机制可以说从2 0到2 6以来不断发展完善 从最初
  • python编程实验,模拟聪明版的尼姆游戏设计原理

    实验原理与内容 本实验完成一个模拟聪明版的尼姆游戏功能 尼姆游戏是个著名的游戏 有很多变种玩法 两个玩家轮流从一堆物品中拿走一部分 在每一步中 玩家可以自由选择拿走多少物品 但是必须至少拿走一个并且最多只能拿走一半物品 然后轮到下一个玩家
  • Python SQLAlchemy ( ORM )、dictalchemy、Flask-SQLAlchemy、Flask-migrate、flask-script、flask-upload

    From Python中强大的通用ORM框架 SQLAlchemy https zhuanlan zhihu com p 444930067 Python ORM之SQLAlchemy全面指南 https zhuanlan zhihu co
  • ubuntu 18.04安装wireshark及网卡接口权限问题

    1 安装 sudo apt fast install wireshark 第一次安装过程中可能会提示Should non superusers be able to capture packets 选是即可 默认是否 2 待安装成功后 你会
  • MFC 菜单栏的使用

    MFC 菜单栏的使用 主要介绍两种比较简单和常用的创建方法 一 在资源视图中添加菜单资源 通过鼠标点击添加菜单项 菜单栏设计好 以后就是添加了 介绍两种方法 1 很简单 鼠标右击想显示菜单栏的对话框属性 可以看到有一个menu的属性 点击就
  • Linux 宝塔面板的安装

    Ptw cwl 登录宝塔官网 查看宝塔的详情 www bt cn 安装 linux服务器图形化界面管理器 安装 宝塔面板 在xshell当中执行宝塔面板的安装命令 yum install y wget wget O install sh h
  • python批量处理

    python opencv图像二值化批量处理 from skimage import data dir io transform color filters import numpy as np import cv2 def convert
  • 红帽Redhat—Linux网卡聚合

    文章目录 一 实验环境设置 二 网卡聚合nmcli 配置步骤 1 创建聚合接口 2 配置网络属性 3 添加物理接口 4 激活端口 5 查看聚合接口状态 一 实验环境设置 在已经安装好的RHEL8 3添加两个新网卡 1 点击虚拟机 gt 设置
  • 机器学习笔记----Fuzzy c-means(FCM)模糊聚类详解及matlab实现

    前言 这几天一直都在研究模糊聚类 感觉网上的文档都没有一个详细而具体的讲解 正好今天有时间 就来聊一聊模糊聚类 一 模糊数学 我们大家都知道计算机其实只认识两个数字0 1 我们平时写程序其实也是这样if 1 then do 永远这种模式 在
  • C++实现String类

    C 实现String类 还没有完成 待继续 有以下注意的点 1 赋值操作符返回的是一个MyString 而重载的 返回的是一个MyString 其中的原因参看 effective c 主要是返回引用的时候 必须返回必须在此函数之前存在的引用
  • Android Studio安装教程+打包APK

    前言 这是一篇给新人的教程 如果你觉得简单啰嗦请保持冷静 同时如果本篇能给予到你帮助 是我的荣幸 Android Studio安装教程 点击链接下载Android Studio Android Studio官网下载 下载完成后双击 exe文
  • [实验三]LZW 编解码算法实现与分析

    目录 一 LZW算法 1 1 编码步骤 1 2 解码步骤 1 3 关于有可能出现当前码字CW不在词典中的情况说明 二 代码实现 2 1 程序说明 2 2 数据结构 2 3 bitio h 2 4 bitio c 2 5 lzw c 三 实验