实验目的:

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   (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] [-o] [-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

部分输出错误原因定义:

#define EPERM 1  //Operation not permitted

#define ENOENT 2  //No such file or directory

#define ESRCH 3  //No such process

……

3.2  strerror():

函数作用:获取系统错误信息,将单纯的标号转为字符串描述。

头文件:#include

补充:常配合errno使用,即strerror(errno)

举例:

四、huffman_encode_file

1.文件编码流程

2. 代码分析

2.1 文件编码总流程的代码实现

//SymbolFrequencies的类型为数组名,数组元素为huffman树叶节点指针

typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS];

//SymbolEncoder的类型为数组名,数组元素为huffman码字节点指针

typedef huffman_code* SymbolEncoder[MAX_SYMBOLS];

int huffman_encode_file(FILE *in, FILE *out)

{

SymbolFrequencies sf; //sf是数组,数组元素为huffman树叶节点的指针

SymbolEncoder *se; //se为指向数组的指针(注意不能看做二级指针)

//数组元素为码字节点的指针

huffman_node *root = NULL; //huffman树的根节点指针

int rc; //return count缩写,返回值

unsigned int symbol_count; //输入文件的总字符数量

//第一次扫描文件,得到每一个符号对应的树叶节点,节点指针顺序存储

//(按照符号的ASCII码)sf数组中,在节点中储存着符号频率

//函数返回值为输入文件字符总数

symbol_count = get_symbol_frequencies(&sf, in);

//1.建立huffman树,根节点指针为sf[0]

//2.由huffman树建立所有的码字节点,码字节点指针存储在se中

se = calculate_huffman_codes(&sf);

root = sf[0];

//第一次扫描信源文件时,文件指针位于文件末尾,重新定位至文件开始,

rewind(in);

//根据码字节点指针数组se,在输出文件中,写出各符号的码表

rc = write_code_table(out, se, symbol_count);

if(rc == 0) //函数返回值为0,说明写码表成功

rc = do_file_encode(in, out, se); //第二次扫描信源文件,

//根据扫描到的符号,依次查表,将对应码字写入输出文件

free_huffman_tree(root); //释放huffman树

free_encoder(se); //释放码字节点指针数组se

return rc;

}

2.2第一次扫描信源文件

2.2.1扫描信源文件并创建每个符号的huffman树叶节点,最多256个,下表对应符号的ASCII码

2.2.2根据符号出现频率,将各符号的频率保存在叶节点count中

static unsigned int

get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in)

{

int c;

unsigned int total_count = 0; //记录扫描到的符号数

init_frequencies(pSF); //初始化*pSE数组所有元素为NULL

while((c = fgetc(in)) != EOF) //扫描输入文件

{

unsigned char uc = c;

if(!(*pSF)[uc]) //如果是一个新符号,则建立一个新的叶节点

(*pSF)[uc] = new_leaf_node(uc); //下标为该符号

++(*pSF)[uc]->count; //频率累加1

++total_count; //总符号数累加1

}

return total_count; //返回输入文件的总符号数

}

static huffman_node*

new_leaf_node(unsigned char symbol)

{ //sizeof()中可以是类型名,也可以是变量名

huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node));

p->isLeaf = 1; //新建的是叶节点,所以isLeaf设为1

p->symbol = symbol;

p->count = 0;

p->parent = 0;

return p;

}

2.3  建立huffman树, 由huffman树建立码字节点

2.3.1 建立huffman树,根节点指针为(*pSF)[0]

static SymbolEncoder*

calculate_huffman_codes(SymbolFrequencies * pSF)

{

unsigned int i = 0;

unsigned int n = 0;

huffman_node *m1 = NULL, *m2 = NULL; //m1为左孩子指针

// m2为右孩子指针

SymbolEncoder *pSE = NULL; //*pSE存放码字节点指针

//以(*pSE)为排序依据,把*pSF数组元素升序排列

qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);

//得到输入文件的符号种类数

for(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n);

//建立huffman树,需要合并n-1次,故循环n-1次

for(i = 0; i < n - 1; ++i)

{

//将m1、m2设置为频率最低的数组元素

m1 = (*pSF)[0];

m2 = (*pSF)[1];

//合并m1、m2为非叶节点,count为二者count之和

//并将该非叶节点的左右孩子设为m1、m2

//将左右孩子的父节点指向该非叶节点

//将(*pSF)[0]指向该非叶节点,将(*pSF)[1]置空

(*pSF)[0] = m1->parent = m2->parent =

new_nonleaf_node(m1->count + m2->count, m1, m2);

(*pSF)[1] = NULL;

//将*pSF数组重新按照升序排序,即将{m1,m2}合并后与其他元素排序

qsort((*pSF), n, sizeof((*pSF)[0]), SFComp);

} //最终(*pSF)[0]为根节点,其count为输入文件大小(单位:字节)

/* Build the SymbolEncoder array from the tree. */

/*分配码字节点指针数组的内存空间(注意:pSE不是二级指针,而是指向整个数组的指针,参考如下:http://www.360doc.com/content/12/0313/18/4186481_194063111.shtml

此外:sizeof(数组名)为整个数组的大小*/

pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));

memset(pSE, 0, sizeof(SymbolEncoder)); //初始化该数组

build_symbol_encoder((*pSF)[0], pSE); //由huffman树建立所有的码字节点

return pSE; //返回码字节点指针数组的地址

}

qsort()函数:  ----quickly sort

头文件:#include

功能:使用快速排序例程进行排序

原型:void qsort(void*base,int nelem,int width,int (*fcmp)(const void *,const void *));

参数:1 待排序数组首地址

2 数组中待排序元素数量

3 各元素的占用空间大小

4 指向函数的指针,用于确定排序的顺序

compare函数原型:

compare( (void *) & elem1, (void *)& elem2 );

Compare 函数的返回值

描述

< 0

elem1将被排在elem2前面

0

elem1 等于 elem2

> 0

elem1 将被排在elem2后面

举例:

(1)    对一个长为1000的数组进行排序时,int a[1000]; 那么base应为a,num应为 1000,width应为 sizeof(int),comp函数随自己的命名。

qsort(a,1000,sizeof(int),comp);

其中comp函数应写为:(注意:参数a,b是指向数组元素的指针。函数体内,需要的是,数组中的元素,所以要加*)

int comp(const void*a,const void*b)

{

return *(int*)a-*(int*)b;

}

上面是由小到大排序,return*(int *)b - *(int *)a; 为由大到小排序。

(2)对一维数组的排序实例(从小到大排序):

int array[5]={4,2,63,1,10};

qsort(array,5,sizeof(int),comp);

int comp(const void*a,const void*b)

{

return *(int*)a-*(int*)b;

}

(3)对字符串进行排序:

int Comp(const void*p1,const void*p2)

{

return strcmp((char*)p2,(char*)p1);

}

int main()

{

char a[MAX1][MAX2];

initial(a);

qsort(a,lenth,sizeof(a[0]),Comp);

}

(4) 按结构体中某个关键字排序(对结构体一级排序):

structNode

{

double data;

int other;

}s[100];

int Comp(constvoid*p1,constvoid*p2)

{

return(*(Node*)p2).data>(*(Node*)p1).data?1:-1;

}

qsort(s,100,sizeof(s[0]),Comp);

(5)按结构体中多个关键字排序(对结构体多级排序)[以二级为例]:

struct Node

{

int x;

int y;

}s[100];

//按照x从小到大排序,当x相等时按y从大到小排序

int Comp(const void*p1,const void*p2)

{

struct Node*c=(Node*)p1;

struct Node*d=(Node*)p2;

if(c->x!=d->x)returnc->x-d->x;

else return d->y-c->y;

}

(6)对结构体中字符串进行排序:

struct Node

{

int data;

char str[100];

}s[100];

//按照结构体中字符串str的字典序排序

int Comp(const void*p1,const void*p2)

{

return strcmp((*(Node*)p1).str,(*(Node*)p2).str);

}

qsort(s,100,sizeof(s[0]),Comp);

huffman编码中应用:

qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);

static int

SFComp(const void *p1, const void *p2)

{

const huffman_node *hn1 = *(const huffman_node**)p1;

const huffman_node *hn2 = *(const huffman_node**)p2;

//把所有的值为NULL的数组元素排到最后

if(hn1 == NULL && hn2 == NULL)

return 0;

if(hn1 == NULL)

return 1;

if(hn2 == NULL)

return -1;

//由小到大排列

if(hn1->count > hn2->count)

return 1; //返回值为正,参数2排在前

else if(hn1->count < hn2->count)

return -1; //返回值为负,参数1排在前

return 0; //返回值为0,参数1==参数2

}

static huffman_node*

new_nonleaf_node(unsigned long count, huffman_node *zero, huffman_node *one)

{

huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node));

p->isLeaf = 0;

p->count = count;

p->zero = zero;

p->one = one;

p->parent = 0;

return p;

} //新建非叶节点

2.3.2递归遍历huffman树,建立所有的码字节点,码字节点指针存储在*pSF数组中

(1)  递归遍历huffman树,找到symbol所对应的叶节点

static void

build_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSF) {

if(subtree == NULL)

return; /*检查首次传递的参数root是否为NULL,是则返回。若root不为NULL,则后面递归永远不会再次运行这一语句。*/

if(subtree->isLeaf) /*如果是1,则说明到达叶节点,建立新节点,并且此次函数调用结束*/

(*pSF)[subtree->symbol] = new_code(subtree);

else

{

/*递归——前序遍历:遍历各节点的isLeaf,并判断是否为1。若为1,则建立新节点,并结束该次函数调用。*/

build_symbol_encoder(subtree->zero, pSF);build_symbol_encoder(subtree->one, pSF);

}

}

(2)  根据找到的symbol,找到相应的码字节点(*pSF)[symbol];从叶节点开始,向上“爬树”直到“树顶”,“途中”位操作取二进制码,写入码字节点(*pSF)[symbol]中

static huffman_code*

new_code(const huffman_node* leaf)

{

unsigned long numbits = 0; //实时记录每次“爬行”的长度(单位:位)

unsigned char* bits = NULL; //编码

huffman_code *p; //码字节点

while(leaf && leaf->parent)/*当leaf==NULL:当前字符为空,无法编码。

当leaf->parent==NULL: 已经到达树根root,此字符编码结束。*/

{

huffman_node *parent = leaf->parent;

//cur_bit为在当前bits[cur_byte]的位置

unsigned char cur_bit = (unsigned char)(numbits % 8);

unsigned long cur_byte = numbits / 8; //cur_byte为第几个字节

/* cur_bit为0代表已写满一个字节,需要下一个数组元素 */

if(cur_bit == 0)

{

size_t newSize = cur_byte + 1;

bits = (char*)realloc(bits, newSize); /*注意两点:1. 扩容后内存地址可能改变,也可能不变,所以bits=不能省略。2.扩容后返回的是void*指针,所以要强制类型转换*/

bits[newSize - 1] = 0; /* 初始化新字节为0 */

}

/*如果leaf是其parent的左孩子,则无需改变bits[cur_byte]中的二进制数字,因为初始化值是0. 如果是右孩子,则需要位运算,在bit[cur_byte]相应位置变为1*/

if(leaf == parent->one)

bits[cur_byte] |= 1 << cur_bit;

++numbits; //增加已“爬行”的长度(单位:位)

leaf = parent; //置为父节点

}

if(bits)

reverse_bits(bits, numbits); //反转bits数组中二进制数

p = (huffman_code*)malloc(sizeof(huffman_code));

p->numbits = numbits; //码位数赋值给码字节点中numbits

p->bits = bits; //码字赋值给码字节点中bits

return p; //返回码字节点

}

reverse_bits(bits,numbits);之前:

reverse_bits(bits,numbits);之后:

思考:为什么要reverse_bits(bits,numbits);?

因为,解码时,读码顺序是,从bit[0]的最右一位向左读,再从bit[1]的最右一位向左读……直到bit[numbytes-1]的最左一位。而解码时,要从树根节点root开始向下遍历,如果读到1,则继续读右孩子,否则读左孩子。所以为了保持一致,先读的bit[0]的最右一位应该是根节点到下一节点的二进制码值,最后读的bit[numbytes-1]的最左一位应该是到叶节点的二进制码值。

static void

reverse_bits(unsigned char* bits, unsigned long numbits)

{

//将numbits除以8上取整,得到numbytes

unsigned long numbytes = numbytes_from_numbits(numbits);

//分配临时字符指针,alloca是在栈(stack)上申请空间,用完马上就释放

unsigned char *tmp =

(unsigned char*)alloca(numbytes);

unsigned long curbit; //记录即将要反转的二进制码的位置

long curbyte = 0; //记录即将要反转的二进制码所在的的数组下标

memset(tmp, 0, numbytes); //初始化tmp[numbytes]所有元素为0

for(curbit = 0; curbit < numbits; ++curbit)

{

unsigned int bitpos = curbit % 8; //要向左移动几位

if(curbit > 0 && curbit % 8 == 0)

++curbyte; /*如果已反转的二进制数字已达到8的倍数,则到下一字节*/

/*get_bit()第二个参数是0时,则为bit[0]最右一位;为numbits-curbit-1时,则为bit[numbytes-1]的最左一位。向左移位,使二进制数对准相应位置*/

tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);

}

memcpy(bits, tmp, numbytes); //将tmp临时数组内容拷贝到bits数组中

}

2.4  在输出文件开始处,写入:字符种类数,输入文件字节数,各字符及其码字长度、对应码字

static int

write_code_table(FILE* out, SymbolEncoder *se, unsigned int symbol_count)

{

unsigned long i, count = 0;

/* 计算字符种类数 */

for(i = 0; i < MAX_SYMBOLS; ++i)

{

if((*se)[i])

++count;

}

/* 主机字节序(host)变成(to)网络字节序(network)(即intel字节序变成motorola字节序) l代表long,参数为32位数字。对应地:htons,s代表short,参数为16位整数*/

i = htonl(count);

if(fwrite(&i, sizeof(i), 1, out) != 1)

return 1; //将字符种类数写入文件

//将输入文件字符数写入输出文件

symbol_count = htonl(symbol_count);

if(fwrite(&symbol_count, sizeof(symbol_count), 1, out) != 1)

return 1;

//写码字

for(i = 0; i < MAX_SYMBOLS; ++i)

{

huffman_code *p = (*se)[i];

if(p)

{

unsigned int numbytes;

/* 写入符号(1字节) */

fputc((unsigned char)i, out);

/*写入该符号的码字长度(1字节) */

fputc(p->numbits, out);

/* 写入码字(numbytes字节) */

/*这里区别将所有字符编码时,码字之间无间距*/

numbytes = numbytes_from_numbits(p->numbits);

if(fwrite(p->bits, 1, numbytes, out) != numbytes)

return 1;

}

}

return 0;

}

2.5  第二次扫描文件,对各字符查表*se,将所有字符的码字写入输出文件,完成文件编码

static int

do_file_encode(FILE* in, FILE* out, SymbolEncoder *se)

{

unsigned char curbyte = 0;

unsigned char curbit = 0;

int c;

while((c = fgetc(in)) != EOF) //遍历每一个字符

{

unsigned char uc = (unsigned char)c;

huffman_code *code = (*se)[uc]; //查表

unsigned long i;

for(i = 0; i < code->numbits; ++i) /*循环完成,则成功写入一个字符的码字,但是不一定被输出*/

{

/*将curbyte字节对应位置变成相应二进制数*/

curbyte |= get_bit(code->bits, i) << curbit;

/*每次写进一位二进制数后,都要判断当前字节curbyte是否写满*/

if(++curbit == 8) /*写满则输出,当前字节初始化为0,当前位的位 置为0*/

{

fputc(curbyte, out);

curbyte = 0;

curbit = 0;

}

}

}

/*当最后一个curbyte没有写满时(写满时curbit==0),不会写入文件。所以当curbit>0时,将最后一个curbyte写入文件*/

if(curbit > 0)

fputc(curbyte, out);

return 0;

} /*注意各码字都是无间距的(之间没有空的二进制位),可能一个字节中前半部分是上个码字,后半部分是下一码字,原因之一就是huffman编码是可变字长编码(VLC)*/

五、huffman_decode_file

1.文件解码流程

2.代码分析

2.1 文件解码总流程的代码实现

int huffman_decode_file(FILE *in, FILE *out)

{

huffman_node *root, *p;

int c;

unsigned int data_count;

/* 读输入文件起始处的码表部分,获得输出文件字符总数,并根据码表建立huffman树 */

root = read_code_table(in, &data_count);

if(!root)

return 1; //建huffman树失败

/* 解码开始:遍历输入文件得到各字符的码字,根据码字,从huffman树根节点root开始,向下直至叶节点,从而获得相应symbol */

p = root;

//data_count>0逻辑上仍有数据,(c = fgetc(in)) != EOF文件中仍有数据

while(data_count > 0 && (c = fgetc(in)) != EOF)

{

unsigned char byte = (unsigned char)c;

unsigned char mask = 1; //mask用来逐位读出二进制数字

//循环一次,mask由0移位至溢出为0,即此byte每一位都已被遍历

while(data_count > 0 && mask)/*data_count>0再次被判断的原因:输入文件的最后一个字节,很有可能前半段是编码,后半段因为所有编码完成而全部为0。此时,若没有data_count>0判断,因mask没有溢出为0,所以此次循环无法终止,但是联合体内p->one和p->zero都不存在(被symbol代替)而无法被访问,从而程序出现错误。*/

{

p = byte & mask ? p->one : p->zero;

/*loop1:mask=00000001,取byte第1位

loop2:mask=00000010,取byte第2位

……

loop8:mask=10000000,取byte第8位 */

mask <<= 1; //loop8以后,mask移位溢出为00000000

if(p->isLeaf) //每次p向下移动后,都要判断其是否为叶节点

{

fputc(p->symbol, out); //是叶节点,则输出symbol

p = root; //重新将p置为根节点,因为要重新从根部向下遍历

--data_count; //还需要被解码的符号个数

}

}

}

free_huffman_tree(root); //解码结束,释放huffman树

return 0;

}

2.2  读输入文件起始处的码表部分,获得输出文件字符总数,并根据码表建立huffman树

static huffman_node*

read_code_table(FILE* in, unsigned int *pDataBytes)

{

huffman_node *root = new_nonleaf_node(0, NULL, NULL);

unsigned int count;

/*读取符号种类数(存储为网络字节序)*/

if(fread(&count, sizeof(count), 1, in) != 1)

{

free_huffman_tree(root);

return NULL;

}

//将网络字节序变为主机字节序

count = ntohl(count);

/*读取字符总数*/

if(fread(pDataBytes, sizeof(*pDataBytes), 1, in) != 1)

{

free_huffman_tree(root);

return NULL;

}

//将网络字节序变为主机字节序

*pDataBytes = ntohl(*pDataBytes);

/*读取符号、对应码位数、码字*/

while(count-- > 0)

{

int c;

unsigned int curbit;

unsigned char symbol;

unsigned char numbits;

unsigned char numbytes;

unsigned char *bytes;

huffman_node *p = root;

if((c = fgetc(in)) == EOF) //读取符号(1字节)

{

free_huffman_tree(root);

return NULL;

}

symbol = (unsigned char)c; //fgetc的返回值是int,所以要强制类型转换

if((c = fgetc(in)) == EOF) //读取码位数(1字节)

{

free_huffman_tree(root);

return NULL;

}

numbits = (unsigned char)c;

numbytes = (unsigned char)numbytes_from_numbits(numbits);

bytes = (unsigned char*)malloc(numbytes);

/* 读取对应码字(numbytes字节)*/

if(fread(bytes, 1, numbytes, in) != numbytes)

{

free(bytes);

free_huffman_tree(root);

return NULL;

}

//顺着码字建树:当前读取位为0时,则建左孩子;为1时,则建右孩子

for(curbit = 0; curbit < numbits; ++curbit)

{

if(get_bit(bytes, curbit)) //当前读取位为1时

{

/*如果p->one为NULL,则可以建立树节点。如果不是NULL,

说明之前有码字(前半段与当前码字相同)建立过此树节点*/

if(p->one == NULL)

{

/* curbit == (unsigned char)(numbits - 1)说明已经到达叶节点处,建立叶节点。否则未到达,建立非叶节点*/

p->one = curbit == (unsigned char)(numbits - 1)

? new_leaf_node(symbol)

: new_nonleaf_node(0, NULL, NULL);

p->one->parent = p; //建立双向指针

}

p = p->one; //p下移至右孩子

}

else //当前读取位为0时。算法同上

{

if(p->zero == NULL)

{

p->zero = curbit == (unsigned char)(numbits - 1)

? new_leaf_node(symbol)

: new_nonleaf_node(0, NULL, NULL);

p->zero->parent = p;

}

p = p->zero;

}

}

free(bytes); //huffman树已经建成,可以释放掉码字

}

return root; //返回huffman树的根节点指针

}

六、memory_encode_file

1.内存编码流程

2.代码分析

2.1 总流程的代码实现:将输入文件读入内存,将内存编码得到输出文件内存,将输出文件内存写入输出文件

static int

memory_encode_file(FILE *in, FILE *out)

{

/*buf指向放置输入文件的内容的内存(以下简称输入内存),

bufout指向放置输出文件的内容的内存(以下简称输出内存)*/

unsigned char *buf = NULL, *bufout = NULL;

unsigned int len = 0, cur = 0, inc = 1024, bufoutlen = 0;

/* assert的作用是计算括号内表达式,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。*/

assert(in && out); //断言:输入文件、输出文件都被成功打开

/*将输入文件读入输入内存*/

while(!feof(in)) //文件读取没有结束

{

unsigned char *tmp; //临时指针,用来判断内存是否够用

len += inc; //内存重新分配大小,递增1kb

tmp = (unsigned char*)realloc(buf, len); /*内存分配扩大1kb,注意realloc扩容后内存地址可能改变,也可能不变*/

if(!tmp) //判断重新分配是否成功,即判断内存是否充足

{

if(buf) //如果内存不够,则释放buf

free(buf);

return 1;

}

buf = tmp; //buf指向重新分配的内存

cur += fread(buf + cur, 1, inc, in); /*以1kb为单位,读取输入文件内容。cur的最终值为输入文件大小*/

}

if(!buf) //如果buf为空,则返回1

return 1;

/* 对输入内存内容进行编码:buf指向输入内存,bufout指向输出内存,cur为输入文件的大小(也是输入内存的大小),bufoutlen为输出文件的大小(也是输出内存的大小)*/

/*传入bufout指针的地址原因:bufout所指内存是通过malloc后多次realloc获得,malloc后内存地址一定会变,realloc后内存地址有时会变有时不变,所以bufout的指向是不断变化的,即指针内容会发生改变,因此要传入指针地址。

传入bufoutlen的地址原因:要改变bufoutlen的值*/

if(huffman_encode_memory(buf, cur, &bufout, &bufoutlen))

{

free(buf);

return 1;

} //内存编码完成

free(buf); /*内存编码完成后,输入内存(由buf指向)使用完毕,可以释放*/

/* 将输出内存(由bufout指向)中的内容写人输出文件*/

if(fwrite(bufout, 1, bufoutlen, out) != bufoutlen)

{

free(bufout);

return 1;

}

free(bufout); /*写入完成,输出内存(由bufout指向)使用完毕,可以释放*/

return 0;

}

2.2 将输入文件内存编码,得到输出文件内存,以下为编码流程

#define CACHE_SIZE 1024 //宏定义缓存大小为1kb

int huffman_encode_memory(const unsigned char *bufin,

unsigned int bufinlen,

unsigned char **pbufout,

unsigned int *pbufoutlen)

{

SymbolFrequencies sf;

SymbolEncoder *se;

huffman_node *root = NULL;

int rc;

unsigned int symbol_count;

buf_cache cache;

/* 确保参数的正确性 */

if(!pbufout || !pbufoutlen)

return 1;

/*cache是输出文件的缓存(指针)+缓存区大小+当前已缓存大小+内存(二级指针)+内存大小(指针)的结构体*/

/*init_cache是将cache结构体内的元素都设为初始值:malloc分配CACHE_SIZE大小的缓存,缓存区大小设为CACHE_ SIZE,当前已缓存大小设为0,内存的二级指针设为输出文件内存的二级指针,内存大小为0*/

if(init_cache(&cache, CACHE_SIZE, pbufout, pbufoutlen))

return 1;

/* 第一次遍历输入内存,得到输入文件大小(也是输入内存大小)(其实就是bufinlen),并建立huffman树叶节点*/

symbol_count = get_symbol_frequencies_from_memory(&sf, bufin, bufinlen);

/*同文件编码:根据频率,构建huffman树,根据huffman树,得到各符号的码字节点*/

se = calculate_huffman_codes(&sf);

root = sf[0]; //huffman树根节点

/*将符号种类数,输入文件大小,各符号,以及对应符号的码位数、码字写入输出内存开始部分*/

rc = write_code_table_to_memory(&cache, se, symbol_count);

/*第二次扫描输入内存,通过查表找到对应字符的码字,按位写入输出内存,确保每一码字直接无二进制位间隔*/

if(rc == 0)

rc = do_memory_encode(&cache, bufin, bufinlen, se);

/* 将缓冲区内容放入输出内存*/

flush_cache(&cache);

free_huffman_tree(root); /*释放huffman树所用内存 */

free_encoder(se); //释放所有码字节点所用内存

free_cache(&cache); //释放输出内存(缓冲区+内存)

return rc;

}

2.3 初始化输出内存缓冲区cache

static int init_cache(buf_cache* pc,

unsigned int cache_size,

unsigned char **pbufout,

unsigned int *pbufoutlen)

{

assert(pc && pbufout && pbufoutlen); /*断言:pc、pbufout、pbufoutlen都不是NULL*/

if(!pbufout || !pbufoutlen)

return 1;

pc->cache = (unsigned char*)malloc(cache_size); /*开辟缓存,大小为cache_size,本程序是1024*/

pc->cache_len = cache_size; //缓存大小设为cache_size

pc->cache_cur = 0; //当前已缓存大小设为0

pc->pbufout = pbufout; //输出内存的二级指针

*pbufout = NULL; //输出内存的指针指向NULL

pc->pbufoutlen = pbufoutlen; //输出内存大小指针

*pbufoutlen = 0; //输出内存大小设为0

return pc->cache ? 0 : 1; //缓存开辟成功时,返回0,否则返回1

}

2.4 第一次遍历输入内存,得到输入文件大小(也是输入内存大小),并建立huffman树叶节点

static unsigned int /*代码与原理跟get_symbol_frequencies()函数十分类似,区别就是扫描输入文件时是fgetc,扫描输入内存时,直接用下标法取元素。*/

get_symbol_frequencies_from_memory(SymbolFrequencies *pSF,

const unsigned char *bufin,

unsigned int bufinlen)

{

unsigned int i;

unsigned int total_count = 0;

init_frequencies (pSF);

for(i = 0; i < bufinlen; ++i)

{

unsigned char uc = bufin[i];

if(!(*pSF)[uc])

(*pSF)[uc] = new_leaf_node(uc);

++(*pSF)[uc]->count;

++total_count; //其实就是bufinlen

}

return total_count;

}

2.5 将符号种类数,输入文件大小,各符号,以及对应符号的码位数、码字写入输出内存开始部分

static int /*代码与原理跟write_code_table()函数十分类似,不同之处在于本函数使用write_cache()向输出内存写入(下面已标注)*/

write_code_table _to_memory(buf_cache *pc,

SymbolEncoder *se,

unsigned int symbol_count)

{

unsigned long i, count = 0;

for(i = 0; i < MAX_SYMBOLS; ++i)

{

if((*se)[i])

++count;

}

i = htonl(count);

//将字符种类数写入输出内存中

if(write_cache(pc, &i, sizeof(i)))

return 1;

symbol_count = htonl(symbol_count);

//将输入文件大小写入输出内存

if(write_cache(pc, &symbol_count, sizeof(symbol_count)))

return 1;

for(i = 0; i < MAX_SYMBOLS; ++i)

{

huffman_code *p = (*se)[i];

if(p)

{

unsigned int numbytes;

unsigned char uc = (unsigned char)i;

/* 将符号写入输出内存(1字节) */

if(write_cache(pc, &uc, sizeof(uc)))

return 1;

uc = (unsigned char)p->numbits;

/* 将码位数写入输出内存(1字节)*/

if(write_cache(pc, &uc, sizeof(uc)))

return 1;

/*将码字写入输出内存(numbytes字节)*/

/*这里区别将所有字符编码时,码字之间无间距*/

numbytes = numbytes_from_numbits(p->numbits);

if(write_cache(pc, p->bits, numbytes))

return 1;

}

}

return 0;

}

2.6 第二次扫描输入内存,通过查表找到对应字符的码字,按位写入输出内存,确保每一码字直接无二进制位间隔

static int /*代码与原理跟do_file_encode十分相似,不同之处在于do_file_encode是写入文件,这里是写入内存*/

do_memory_encode(buf_cache *pc,

const unsigned char* bufin,

unsigned int bufinlen,

SymbolEncoder *se)

{

unsigned char curbyte = 0;

unsigned char curbit = 0;

unsigned int i;

for(i = 0; i < bufinlen; ++i)

{

unsigned char uc = bufin[i];

huffman_code *code = (*se)[uc];

unsigned long i;

for(i = 0; i < code->numbits; ++i)

{

curbyte |= get_bit(code->bits, i) << curbit;

if(++curbit == 8)

{ //将码字写入输出内存

if(write_cache(pc, &curbyte, sizeof(curbyte)))

return 1;

curbyte = 0;

curbit = 0;

}

}

}

return curbit > 0 ? write_cache(pc, &curbyte, sizeof(curbyte)) : 0;

}

2.7将数据写入输出内存的函数

static int write_cache(buf_cache* pc,

const void *to_write,

unsigned int to_write_len)

{

unsigned char* tmp;

//如果pc和to_write至少一个为NULL的话,则终止程序进行

assert(pc && to_write);

//如果已缓存数据量大于缓存区大小的话,则终止程序进行

assert(pc->cache_len >= pc->cache_cur);

/*如果将要写入的数据量与已缓存的数据量之和大于缓存区大小,那么就先flush缓存区,然后再将要写入的数据直接写入输出内存,即不使用缓存区。

否则,使用缓存区缓存将要写入的数据,待以后缓存区将满时,flush到输出内存*/

if(to_write_len > pc->cache_len - pc->cache_cur)

{

unsigned int newlen;

flush_cache(pc); // flush缓存区中的内容到输出内存

//计算输出内存的原有数据量与将要写入的数据量之和,作为newlen

newlen = *pc->pbufoutlen + to_write_len;

//扩大输出内存空间,大小为newlen

tmp = realloc(*pc->pbufout, newlen);

if(!tmp)

return 1;

/*将要写入的数据复制到输出内存中,注意从tmp + *pc->pbufoutlen位置(新空间开始处)开始,长度为将要写入的数据量*/

memcpy(tmp + *pc->pbufoutlen, to_write, to_write_len);

*pc->pbufout = tmp;

*pc->pbufoutlen = newlen; //输出内存空间大小置为newlen

}

else

{

/* 把将要写入的数据拷贝到缓存中,注意从原缓存数据量开始,长度为将要写入的数据量*/

memcpy(pc->cache + pc->cache_cur, to_write, to_write_len);

/*已缓存位置设为原缓存数据量与刚写入数据量之和*/

pc->cache_cur += to_write_len;

}

return 0;

}

2.8 将缓冲区内容放入输出内存

static int flush_cache(buf_cache* pc)

{

assert(pc); //如果pc为NULL,则终止程序运行

if(pc->cache_cur > 0) /*如果当前已缓存数据量为0,则不无需flush,直接return 0;*/

{

//计算已缓存数据量与输出内存中的数据量之和,作为新长度newlen

unsigned int newlen = pc->cache_cur + *pc->pbufoutlen;

//扩大输出内存空间,大小为newlen

unsigned char* tmp = realloc(*pc->pbufout, newlen);

if(!tmp)

return 1;

/*将缓冲区内容拷贝到新增加的内存空间上,注意从tmp+*pc->pbufoutlen位置(新空间开始处)开始拷贝,拷贝长度为原缓存数据量*/

memcpy(tmp + *pc->pbufoutlen, pc->cache, pc->cache_cur);

*pc->pbufout = tmp;

*pc->pbufoutlen = newlen;

pc->cache_cur = 0; //已缓存数据量置零

}

return 0;

}

七、memory_decode_file

1.内存解码流程

2.代码分析

2.1  内存解码总流程的代码实现

static int /*代码与memory_encode_file(FILE *in,FILE *out)基本相同,不同之处在于本函数调用的是huffman_decode_memory()函数*/

memory_decode_file(FILE *in, FILE *out)

{

unsigned char *buf = NULL, *bufout = NULL;

unsigned int len = 0, cur = 0, inc = 1024, bufoutlen = 0;

assert(in && out);

while(!feof(in))

{

unsigned char *tmp;

len += inc;

tmp = (unsigned char*)realloc(buf, len);

if(!tmp)

{

if(buf)

free(buf);

return 1;

}

buf = tmp;

cur += fread(buf + cur, 1, inc, in);

}

if(!buf)

return 1;

if(huffman_decode_memory(buf, cur, &bufout, &bufoutlen))

{

free(buf);

return 1;

}

free(buf);

if(fwrite(bufout, 1, bufoutlen, out) != bufoutlen)

{

free(bufout);

return 1;

}

free(bufout);

return 0;

}

2.2 对输入内存进行解码,解码内容存入输出内存

int huffman_decode_memory (const unsigned char *bufin,

unsigned int bufinlen,

unsigned char **pbufout,

unsigned int *pbufoutlen)

{

huffman_node *root, *p;

unsigned int data_count;

unsigned int i = 0;

unsigned char *buf;

unsigned int bufcur = 0;

/*确保参数的合法性*/

if(!pbufout || !pbufoutlen)

return 1;

/*读输入内存起始处的码表部分,获得输出文件字符总数,并根据码表建立huffman树 */

root = read_code_table_from_memory(bufin, bufinlen, &i, &data_count);

if(!root)

return 1;

buf = (unsigned char*)malloc(data_count);

/* 下面内容与huffman_decode_file()函数中解码部分基本相同,不同之处已标明*/

p = root;

for(; i < bufinlen && data_count > 0; ++i)

{

unsigned char byte = bufin[i];

unsigned char mask = 1;

while(data_count > 0 && mask)

{

p = byte & mask ? p->one : p->zero;

mask <<= 1;

if(p->isLeaf)

{

p = root;

//下标法取出输出内存中的元素,并将字符存入其中

buf[bufcur++] = p->symbol;

--data_count;

}

}

}

free_huffman_tree(root);

*pbufout = buf;

*pbufoutlen = bufcur;

return 0;

}

2.3 读输入内存起始处的码表部分,获得输出文件字符总数,并根据码表建立huffman树

static huffman_node*

read_code_table _from_memory(const unsigned char* bufin,

unsigned int bufinlen,

unsigned int *pindex)

{略。与read_code_table()函数基本相同,只是读取数据的方式不同,本函数使用的是memread函数,具体解析如下。}

2.4  memread函数:

static int

memread(const unsigned char* buf,

unsigned int buflen,

/*用来记录已读取的数据量,标记读取位置。使用指针的原因是,要使不断改变的读取位置,被本次函数调用之后,仍能被函数环境中其他元素使用*/

unsigned int *pindex,

void* bufout,

unsigned int readlen)

{

/*如果buf、pindex、bufout中存在为NULL,则终止程序运行*/

assert(buf && pindex && bufout);

/*如果读取位置大于被读取内存大小,则终止程序运行*/

assert(buflen >= *pindex);

if(buflen < *pindex)

return 1;

/*如果读取位置与将读取的数据量之和大于被读取内存大小,则返回1*/

if(readlen + *pindex >= buflen)

return 1;

/*将被读取内存读取位置之后的内容,按照所要求大小,读入外部内存*/

memcpy(bufout, buf + *pindex, readlen);

/*读取位置右移刚读取数据量大小*/

*pindex += readlen;

return 0;

}

八、发现的问题及实验改进

问题1:

郭远航的实验报告中描述了一个问题:“结构体huffman_code_tag建立时,码字长度的数据类型为unsignedlong,占用4byte。在将码表写入文件时,用的是函数fputc()(解码读出时用fgetc()),即写入(/读出)1byte的码长。这就造成了前后的不匹配,存在错误隐患。”

具体错误隐患为:产生的问题就是getchar时,会取低内存的字节,如果主机采用motorola字节序,则造成码位数取值为0,从而造成错误。

实验报告中指出解决方案:“在结构体huffman_code_tag建立时,将码字长度的数据类型改为unsignedchar,占用1byte。”

但是问题在于,这种解决方案忽略了一个很小的细节。当各字符频率排序后,出现类似于1,1,2,4,8,16,……这种数列中任意一个数字都大于等于前面所有数字之和的情况时,建成huffman树以后,会出现极不平衡的二叉树(如图)。

这种情况下,如果文件中每一种可能的字符都出现,即2^8个符号全部都有相应编码时,huffman树的高度就会是2^8。所以numbits应该为256。按照上述解决方案,将numbits的类型改为unsigned char时,numbits最大为255,无法存储256. 即使赋值numbits=256;(实际numbits会溢出为0),--numbits;后,numbits的值会变成255,仍然具有实际意义,但是不可忽略如下情况:当码位数为256时,numbits溢位为0.

上述情况都会出错。

所以最好的解决办法不是在结构体中将numbits的类型由unsigned long改为unsigned char,而是仍保留unsigned long类型(因为可以存储数字256),并在write_code_table中写码位数时,将fputc()改为fwrite();read_code_table中,fgetc()改为fread()。在已编码文件起始处的code_table部分,码位数始终占据4字节空间。这样即解决了由于cpu字节序引发的问题,又解决了程序中多次使用

造成的256数字无法存储的问题。

问题2:

输入文件(内存)中字符总数symbol_count总是采用unsignedint类型,unsigned int最多表示2^32个数字,也就是说,最多压缩2^32字节(4G)的文件(准确地说应该是4G-1字节的文件)。

如果要压缩4G或者4G以上的文件,那么此程序无法正确运行。

解决办法:

使用unsigned__int64或者unsigned long long类型来定义symbol_count,最大压缩2^64字节(2^34G)的文件。相应的不能用htonl()/ntohl()来转换字节序,因为其中参数必须是32位的数字。所以应该定义函数htonll()/ntohll()如下,可以转换64位的字节序。

(注:vc6.0中无法使用unsigned long long,可以使用unsigned __int64。用vc6.0时,将以下unsigned long long全部改为unsigned __int64)

unsigned long long ntohll(unsigned long long val)

{

return (((unsigned long long )htonl((int)((val << 32) >> 32))) << 32) | (unsigned int)htonl((int)(val >> 32));

}

unsigned long long htonll(unsigned long long val)

{

return (((unsigned long long )htonl((int)((val << 32) >> 32))) << 32) | (unsigned int)htonl((int)(val >> 32));

}

举例分析函数ntohll():

假设64位数字var=0X ff 66 ee 55 dd 44 cc 33(以下均为十六进制)

内存中:33 cc 44 dd 55 ee 66 ff(左:低内存;右:高内存)

或“|”左边内存变化:

1)      val<<32: 00 00 00 00 33 cc 44 dd

2)      val>>32: 33 cc 44 dd 00 00 00 00

3)      int强制类型转换: 33 cc 44 dd

4)      htonl改变字节序: dd 44 cc 33

5)      unsigned long long 强制类型转换:dd 44 cc 33 00 00 00 00

6)      <<32:  00 00 00 00dd 44 cc 33

或“|”右边内存变化:

1)      val>>32:  55 ee 66ff 00 00 00 00

2)      int强制类型转换:55 ee 66 ff

3)      htonl改变字节序:ff 66 ee 55

4)      unsigned int 强制类型转换:ff 66 ee 55

最后结果:ff 66 ee 55 dd 44 cc 33(左:低内存;右:高内存)

九、实验结果分析

文件类型

原文件大小

压缩后文件大小

压缩效率

word文档

31k

14k

54.83%

ppt文档

209k

152k

27.3%

mp3音乐

4567k

4558k

0.20%

exe应用程序

56694k

56694k

0

MP4视频

9964k

9957k

0.07%

wma音乐

3457k

3429k

0.81%

excel文档

342k

158k

53.8%

WinRAR压缩文件

2038k

2039k

-0.05%

HTML文档

157k

114k

27.4%

avi视频

81k

56k

30.9%

(写在后面)

这篇文百分之九十九的内容都是两年前的写的,想起那时可以把整个程序都默写下来,心里还是挺多感触的,好好珍惜剩下不多的一年多的时光,加油共勉。

霍夫曼编码及解码实验c语言,Huffman编码与解码_C语言实现相关推荐

  1. Zlib压缩算法:LZ77、LZ78、霍夫曼编码、滑动窗口、Rabin-Karp算法、哈希链、I/O缓冲区

    Table of Contents 1.简介 1.1 什么是zlib 2.压缩算法 2.1 放气 2.2 LZ77 2.2.1 滑动窗口 2.2.2 长距离对 2.3 霍夫曼编码 3. zlib的实现 ...

  2. c语言实现霍夫曼编码

    // // 霍夫曼编码 //#include <stdio.h> #include <stdlib.h> #include <string.h>/**思路:用一个有 ...

  3. Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树

    Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树 文章目录 Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树 0x00 摘要 0x01 背景概念 1.1 词向量 ...

  4. 数据结构与算法 / 霍夫曼树、霍夫曼编码和解码

    一. 诞生原因 找出存放一串字符所需的最少的二进制编码. 二. 构造方法 首先统计出每种字符出现的频率,即:概率.权值. 例如:频率表 A:60,    B:45,   C:13   D:69   E ...

  5. 数据结构与算法之霍夫曼编码解码实现

    目标:将字符串"can you can a can as a can canner can a can."编码再解码 流程: 将字符串转成bytes (byte[]格式)(eg.[ ...

  6. 视频编解码——霍夫曼编码的实现

    目录 原理 分析 输入文件 字符出现次数排序功能 .C .H 效果 思考 未完成的代码 .c .h 能够成功运行的代码 原理 视频编解码--熵编码-哈夫曼编码的原理及实现 分析 首先制作一个输入文件, ...

  7. labview 霍夫曼树_哈夫曼树编码实验报告_信息论与编码实验2 实验报告_信息论与编码报告...

    huffman编码C语言实验报告 今日推荐 180份文档 2014...4页 1下载券 安卓版100 doors 2攻略1... 3页 1下载券 <逃脱本色>doors....语文教育实习 ...

  8. 信息论 哈夫曼编码 与 菲诺编码的实现(对一幅BMP格式的灰度图像(个人 证件照片)进行二元霍夫曼编码和译码。并进行编码效率的计算,对一幅BMP格式的灰度图像进行二 元Fano编码、译码 )

    信息论 哈夫曼编码 与 菲诺编码的实现(对一幅BMP格式的灰度图像(个人 证件照片)进行二元霍夫曼编码和译码.并进行编码效率的计算,对一幅BMP格式的灰度图像进行二 元Fano编码.译码 ) 原始图片 ...

  9. 信源压缩编码 编程c语言,霍夫曼信源编码实验报告.docx

    霍夫曼信源编码实验报告.docx PAGE PAGE 7 实验1:霍夫曼信源编码综合设计[实验目的]通过本专题设计,掌握霍夫曼编码的原理和实现方法,并熟悉利用C语言进行程序设计,对典型的文本数据和图像 ...

  10. 熵编码(算术+霍夫曼)编解码基础知识总结

    在MPEG的TMC13模型中,对于surface point cloud compression,对block和vertices进行熵编码:对于lidar point cloud compressio ...

最新文章

  1. 在CentOS 6.9 x86_64的nginx 1.12.2上开启ngx_http_geo_module模块实录
  2. Cogs 376. [IOI2002]任务安排(后效性DP)
  3. 前端学习(3013):vue+element今日头条管理--表单验证基本使用
  4. 【HDU - 1031 】Design T-Shirt(水题 排序)
  5. 地牢房间迷宫走廊生成(二),Python实现洪水法、完美迷宫
  6. 互联网晚报 | 3月11日 星期五 |​ ​​商汤科技在深圳成立新公司,;微信支付电子小票上线...
  7. Linux crontab定时执行任务 命令格式与详细例子
  8. Pyrhon矩阵问题的解决方法
  9. Java抽象类/接口
  10. python打开鼠标指定文件夹_学会python文件操作,鼠标好像没用了,学习python第10天...
  11. 线性代数:矩阵运算常用公式
  12. CentOS7自行搭建KMS服务器
  13. matlab 堆图,堆积条形图Matlab
  14. ROS dst-nat端口映射限制访问映射IP
  15. 全面解读数据中台、数据仓库和数据湖
  16. python中如何进行类的派生与继承_python 面向对象之继承与派生
  17. 资源屋分享两款导航网站源码 支持自动收录、自动审核、自动检测友链功能
  18. 孔雀优化算法(POA)——(含MATLAB代码)
  19. FPGA Verilog视频笔记
  20. 传智播客设计学院简介网页代码

热门文章

  1. Makefile中创建一个以当前时间为文件夹名的文件
  2. Linux虚拟文件系统(节点路径搜索)
  3. 欧几里得算法、扩展欧几里得算法、求逆元、中国剩余定理、扩展中国剩余定理
  4. python都有什么包装_Python包装
  5. stm32是以c语言来编程吗,stm32用什么语言编程
  6. matlab 实验七 低层绘图操作,matlab实验内容解答.doc
  7. 负数的补码公式是什么_为什么0xffffffff是-1?(计算机对整型的存储)
  8. html5拼图微信小游戏,微信小程序:拼图游戏
  9. java+se+ee+spring_【JavaEE】Springmvc+Spring整合及example
  10. java项目怎么使用js插件_Intro.js 分步向导插件使用方法 Web程序 - 贪吃蛇学院-专业IT技术平台...