实验目的:
1. 掌握熵编码的原理和方法
2. 掌握霍夫曼编码的原理
3. 了解霍夫曼编码的优缺点
4. 掌握和熟悉C
一、背景知识及相关公式
1.熵,又称为“信息熵”(Entropy)
1.1 在信息论中,熵是信息的度量单位。信息论的创始人Shannon在其著作《通信的数学理论》中提出了建立在概率统计模型上的信息度量。他把信息定义为“用来消除不确定性的东西”。
1.2 一般用符号 H 表示,单位是比特。对于任意一个随机变量 X,它的熵定义如下:
1.3 变量的不确定性越大,熵也就越大。换句话说,了解它所需要的信息量也就越大。
2. Huffman编码
1.4 Huffman Coding (霍夫曼编码)是一种无失真编码的编码方式,Huffman编码是可变字长编码(VLC)的一种。
1.5 Huffman编码基于信源的概率统计模型,它的基本思路是,出现概率大的信源符号编长码,出现概率小的信源符号编短码,从而使平均码长最小。
1.6 在程序实现中常使用一种叫做树的数据结构实现Huffman编码,由它编出的码是即时码。
3. Huffman 编码的方法
1.7 统计符号的发生概率;
1.8 把频率按从小到大的顺序排列
1.9 每一次选出最小的两个值,作为二叉树的两个叶子节点,将和作为它们的根节点,这两个叶子节点不再参与比较,新的根节点参与比较;
1.10 重复3,直到最后得到和为1的根节点;
1.11 将形成的二叉树的左节点标0,右节点标1,把从最上面的根节点到最下面的叶子节点途中遇到的0,1序列串起来,就得到了各个符号的编码。
二、数据结构
1.huffman树节点
typedef struct huffman_node_tag
{
unsigned char isLeaf; //是否是叶节点
unsigned long count; //字母出现的频率
struct huffman_node_tag *parent; //父节点指针
union //联合体:如果是叶节点,则只能有symbol,如果是非叶节点,只能有左右孩子指针
{
struct
{
struct huffman_node_tag *zero, *one; //左右孩子指针
};
unsigned char symbol; //该节点对应的字母
};
} huffman_node;
2.huffman码字节点
typedef struct huffman_code_tag
{
//以位为单位的码字长度
unsigned long numbits;
/*码字(二进制):码字的第1位位于bits[0]的第1位;
码字的第2位位于bits[0]的第2位
……
码字的第8位位于bits[0]的第8位
码字的第9位位于bits[1]的第1位 */
unsigned char *bits;
} huffman_code;
3.输出缓冲结构体
typedef struct buf_cache_tag /*内存编码时,结构体存放输出内存及缓存的指针*/
{
//cache:缓存作用
//如果待存入数据大小合适,则放入*cache;
/*如果待存入数据与*cache中原有数据大小之和超出cache_len,则将原有数据与待存入数据一起放入输出内存*pbufout,最后将*cache内容清空*/
unsigned char *cache;
//缓存区*cache的大小,本程序将其设为1024字节
unsigned int cache_len;
//缓冲区*cache当前已缓存数据的大小(当前已缓存大小)
unsigned int cache_cur;
//最终所有输出数据存放的内存区域,即输出内存的二级指针
unsigned char **pbufout;
//最终所有输出数据的大小之和,即*pbufout所指向的内存大小
unsigned int *pbufoutlen;
} buf_cache;
思考:
为什么使用pbufout二级指针?输出内存**pbufout是通过malloc后多次realloc获得,malloc后内存地址一定会变,realloc后内存地址有时会变有时不变(MSDN上说,*realloc returns a void pointer to the reallocated (and possiblymoved) memory block.),所以输出内存地址(指向输出内存的指针)是不断变化的,即指针内容会发生改变,因此要想通过函数改变指针内容,并使该内容可以被函数外环境使用,只能操作二级指针。
为什么使用pbufoutlen指针?要想通过函数改变输出内存大小的值,并使该内容可以被函数外环境使用,只能操作指针。
三、主函数分析
1.getopt()分析命令行参数
头文件:#include<unistd.h> (unix standard header缩写 unix 标准头文件)
原型:
int getopt(int argc,char * const argv[],const char * optstring);
参数argc和argv是由main()传递的参数个数和内容。参数optstring 则代表预处理选项字符串。
什么是选项?什么是参数?
字符串optstring可以下列元素
1.单个字符,表示选项。
2.单个字符后接一个冒号:表示该选项后必须跟一个参数。参数紧跟在选项后或者以空格隔开。该参数的指针赋给optarg。
3.单个字符后跟两个冒号,表示该选项后必须跟一个参数。参数必须紧跟在选项后不能以空格隔开。该参数的指针赋给optarg。
调用原理:
调用一次,返回一个选项。如果选项字符串里的字母后接着冒号“:”,则表示还有相关的参数,char* optarg指向该参数。在命令行选项参数再也检查不到optstring中包含的选项时,返回-1,同时optind储存第一个不包含选项的命令行参数。
相关变量:
optarg是char*型变量,会指向此额外参数。
返回值:
getopt()每次调用会逐次返回命令行中符合的选项。
当没有参数的最后的一次调用时,getopt()将返回-1。
当解析到一个不在optstring里面的参数,或者一个必选值参数不带值时,返回'?'。
注意三点:
(1). 不带值的参数可以连写,象1和a是不带值的参数,它们可以-1-a分开写,也可以-1a或-a1连写。
(2). 参数不分先后顺序,'-1a -ccvalue -ddvalue'和'-d -c cvalue -a1'的解析结果是一样的。
(3). 要注意可选值的参数的值与参数之间不能有空格,必须写成-ddvalue这样的格式,如果写成-d dvalue这样的格式就会解析错误。
本程序应用:
(1).文件编码:
getopt处理以'-’开头的命令行参数,如图optstring=”i:o:cdhvm”,命令行为huff_run.exe–i test.doc –o 1.huf –c。在这个命令行参数中,-i、-o和-c就是选项元素,去掉'-',i、o和c就是选项。test.doc是i的参数,1.huf是o的参数。其中顺序可以改变,增加了程序的灵活性。
(2).文件解码:
(3).内存编码:
(4).内存解码:
2.main()函数分析
int main(int argc, char** argv) //argc:命令行参数个数
//argv:字符指针数组:命令行参数
{
char memory = 0; //memory缺省值为0,即默认为文件编解码,而非内存
char compress = 1; //compress缺省值为1,即默认为编码
int opt; //接收getopt()返回值,为选项或-1
const char *file_in = NULL, *file_out = NULL; //输入输出文件路径及文件名
//缺省目录则表明为当前目录
FILE *in = stdin; //缺省值为标准输入文件
FILE *out = stdout; //缺省值为标准输出文件
//得到命令行参数
while((opt = getopt(argc, argv, "i:o:cdhvm")) != -1)
{
switch(opt) //opt为iocdhvm字母之一
{
case 'i': //-i后接输入文件
file_in = optarg; //optarg为选项参数缩写,该变量存放参数
//注意:optarg无须另设
break;
case 'o': //-o后接输出文件
file_out = optarg;
break;
case 'c': //-c表明程序功能为文件压缩
compress = 1;
break;
case 'd': //-d表明程序功能为文件解压
compress = 0;
break;
case 'h': //-h表明需要显示help使用方法:
/*fputs("Usage: huffcode [-i<input file>] [-o<output file>] [-d|-c]\n"
"-i - input file (default is standard input)\n"
"-o - output file (default is standard output)\n"
"-d - decompress\n"
"-c - compress (default)\n"
"-m - read file into memory, compress, then write to file
(not default)\n", out);*/
usage(stdout); //输出上述信息到屏幕上
return 0;
case 'v':
version(stdout); //输出版本版权信息
return 0;
case 'm': //-m表明为内存编码或内存解码
memory = 1;
break;
default: //如果是其他情况,则将使用方法信息送到标准错误文件
usage(stderr);
return 1;
}
}
//如果给出输入文件,则打开该文件
if(file_in)
{
in = fopen(file_in, "rb");
if(!in)
{
fprintf(stderr,"Can't open input file '%s': %s\n",file_in, strerror(errno)); //strerror(errono);返回值为错误的字符串信息
return 1;
}
}
//如果输出文件名给出,则创建该文件
if(file_out)
{
out = fopen(file_out, "wb");
if(!out)
{
fprintf(stderr,
"Can't open output file '%s': %s\n", file_out, strerror(errno));
return 1;
}
}
// memory为1时,说明是内存编解码
if(memory)
{
return compress ? //compress为1时内存编码,为0时内存解码
memory_encode_file(in, out) : memory_decode_file(in, out);
}
//若执行到此,说明是文件编解码
return compress ? //compress为1时文件编码,为0时文件解码
huffman_encode_file(in, out) : huffman_decode_file(in, out);
}
3. errno变量和strerror()函数
3.1 errno:(Error No. 的缩写)
概念:是一个int型变量------记录系统最后一次错误代码。
头文件:#include<errno.h>
部分输出错误原因定义:
#define EPERM 1 //Operation not permitted
#define ENOENT 2 //No such file or directory
#define ESRCH 3 //No such process
……
3.2 strerror():
函数作用:获取系统错误信息,将单纯的标号转为字符串描述。
头文件:#include<string.h>
补充:常配合errno使用,即strerror(errno)
举例:
四、huffman_encode_file
1.文件编码流程