您的位置:首页 > 其它

数据压缩试验三 Huffman 编解码算法实现与压缩效率分析

2017-07-04 09:12 615 查看

一、实验目的

掌握Huffman编解码实现的数据结构和实现框架, 进一步熟练使用C编程语言, 并完成压缩效率的分析。

二、实验原理

1. Huffman编码

(1)Huffman Coding (霍夫曼编码)是一种无失真编码的编码方式, Huffman 编码是可变字长编码(VLC)的一种。

(2)Huffman 编码基于信源的概率统计模型,它的基本思路是,出现概率大的信源符号编长码,出现概率小的信源符号编短码,从而使平均码长最小。

(3)在程序实现中常使用一种叫做树的数据结构实现 Huffman 编码,由它编出的码是即时码。

2. Huffman编码的方法

(1) 将文件以ASCII字符流的形式读入, 统计每个符号的发生频率;

(2)将所有文件中出现过的字符按照频率从小到大的顺序排列;

(3) 每一次选出最小的两个值,作为二叉树的两个叶子节点,将和作为它们的根节点,这两个叶子节点不再参与比较,新的根节点参与比较;

(4)重复3, 直到最后得到和为1的根节点;

(5)将形成的二叉树的左节点标0,右节点标1, 把从最上面的根节点到最下面的叶子节点途中遇到的0、 1序列串起来,得到了各个字符的编码表示。

3. Huffman编码的数据结构设计

在程序实现中使用一种叫做二叉树的数据结构实现Huffman编码。

(1)Huffman节点结构

typedef struct huffman_node_tag
{
unsigned char isLeaf; /* 是否为叶结点 */
unsigned long count; /* 信源中出现频数 */
struct huffman_node_tag *parent; /* 父节点指针 */

union /* 如果不是树叶,则此项为该结点左右孩子的指针;否则为某个信源符号 */
{
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;


4. 静态链接库的使用

  本实验由两个项目组成,第一个项目为 Huffman 编码的具体实现,名为 huff_code,创建项目时选择的是静态库,生成一个 .lib 文件。第二个项目 huff_run 只需要包含这个库即可调用其中的编码函数。项目属性需要配置库目录属性,也就是第一个项目生成文件的路径,和附加依赖性属性,也就是库的名称,如图所示。由于代码中用到了字节序转换的函数 htonl、ntohl,附加依赖项还需包含 ws2_32.lib。







三、实验步骤及代码分析

1.调试

 首先调试Huffman的编码程序, 对照编码算法步骤对关键语句加上注释,并说明进行何操作。

Huffman编码流程

Created with Raphaël 2.1.0读入待编码的源文件第一次扫描:统计文件中各个字符出现频率建立Huffman树将码表及其他必要信息写入输出文件第二次扫描:对源文件进行编码并输出

(1)读取文件

使用库函数中的getopt解析命令行参数,这个函数的前两个参数为main中的argc和argv,第三个参数为单个字符组成的字符串,每个字符表示不同的选项,单个字符后接一个冒号,表示该选项后必须跟一个参数。

//--------huffcode.c--------
...
static void
usage(FILE* out)//命令行参数格式
{
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",
// step1: by yzhang, for huffman statistics
"-t - output huffman statistics\n",
//step1:end by yzhang
out);
}
//————————————————————————————————————————————————————————
int main(int argc, char** argv)
{
char memory = 0; //memory表示是否对内存数据进行操作
char compress = 1; //compress为1表示编码,0表示解码
const char *file_in = NULL, *file_out = NULL;
FILE *in = stdin, *out = stdout;
while((opt = getopt(argc, argv, "i:o:cdhvm")) != -1){ //读取命令行参数的选项
switch(opt){
case 'i': file_in = optarg; break; // i 为输入文件
case 'o': file_out = optarg; break; // o 为输出文件
case 'c': compress = 1; break; // c 为压缩操作
case 'd': compress = 0; break; // d 为解压缩操作
case 'h': usage(stdout); system("pause"); return 0; // h 为输出参数用法说明
case 'v': version(stdout); system("pause"); return 0; // v 为输出版本号信息
case 'm': memory = 1; break; // m 为对内存数据进行编码
default: usage(stderr); system("pause"); return 1;
}
}
if(file_in){ //读取输入输出文件等
in = fopen(file_in, "rb"); if(!in)...
}
...
if(memory)
{//对内存数据进行编码或解码操作
return compress ?
memory_encode_file(in, out) : memory_decode_file(in, out);
}

if(compress)  //change by yzhang
huffman_encode_file(in, out,outTable);//step1:changed by yzhang from huffman_encode_file(in, out) to huffman_encode_file(in, out,outTable)
else
huffman_decode_file(in, out);
}


分析对文件的编码流程

//--------huffman.c--------
...
//最大符号数目,由于一个字节一个字节进行编码,因此为256
#define MAX_SYMBOLS 256
typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS]; //信源符号数组,数据类型为之前定义过的树节点类型
typedef huffman_code* SymbolEncoder[MAX_SYMBOLS]; //编码后的码字数组,数据类型为之前定义过的码字类型
//————————————————————————————————————————————————————————
int huffman_encode_file(FILE *in, FILE *out) //对文件进行Huffman编码的函数
{
SymbolFrequencies sf;
SymbolEncoder *se;
huffman_node *root = NULL;
unsigned int symbol_count = get_symbol_frequencies(&sf, in); //第一遍扫描文件,得到文件中各字节的出现频率
se = calculate_huffman_codes(&sf); //再根据得到的符号频率建立一棵Huffman树,还有Huffman码表
root = sf[0]; //编完码表后,Huffman树的根节点为 sf[0],具体原因在后面的分析
rewind(in); //回到文件开头,准备第二遍扫描文件
int rc = write_code_table(out, se, symbol_count); //先在输出文件中写入码表
if(rc == 0) rc = do_file_encode(in, out, se); //写完码表后对文件字节按照码表进行编码
free_huffman_tree(root); free_encoder(se);
return rc;
}


(2)统计文件中各字节出现的频率

static unsigned int get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in) //统计中各字节出现频率的函数
{
int c; unsigned int total_count = 0;
memset(*pSF, 0, sizeof(SymbolFrequencies)); //首先把所有符号的频率设为0
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) //建立一个叶节点的函数
{
huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node)); //分配一个叶节点的存储空间
p->isLeaf = 1; //表明当前节点为叶节点
p->symbol = symbol; //节点存储的信源符号
p->count = 0; //信源符号数目设为0
p->parent = 0; //父节点为空
return p;
}


(3)建立Huffman树

static SymbolEncoder* calculate_huffman_codes(SymbolFrequencies * pSF) //创建一棵Huffman树的函数
{
unsigned int i = 0, n = 0;
huffman_node *m1 = NULL, *m2 = NULL;
SymbolEncoder *pSE = NULL;
qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp); //先使用自定义的顺序对出现次数进行排序,使得下标为0的元素的count最小
for(n = 0; n < MAX_SYMBOLS && (*pSF)
; ++n); //统计下信源符号的真实种类数,因为一个文件中不一定256种字节都会出现
for(i = 0; i < n - 1; ++i)
{
//把出现次数最少的两个信源符号节点设为 m1,m2
m1 = (*pSF)[0];
m2 = (*pSF)[1];
//然后合并这两个符号,把合并后的新节点设为这两个节点的父节点
(*pSF)[0] = m1->parent = m2->parent = new_nonleaf_node(m1->count + m2->count, m1, m2);
(*pSF)[1] = NULL; //合并之后,第二个节点为空
qsort((*pSF), n, sizeof((*pSF)[0]), SFComp); //然后再排一遍序
}
//树构造完成后,为码字数组分配内存空间并初始化
pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));
memset(pSE, 0, sizeof(SymbolEncoder));
build_symbol_encoder((*pSF)[0], pSE); //从树根开始,为每个符号构建码字
return pSE;
}


qsort为标准库中自带的快速排序函数,参数为 <待排序数组> <数组元素个数> <元素的大小> <自定义比较数组元素的函数>。

临时变量 m1,m2 不断地设为信源符号数组中出现次数最少的两个元素,数组第一个元素一直是出现次数最小的两个符号的合并,这样循环结束后,pSF 数组中所有元素除第一个 pSF[0] 以外都空,而这些新建立的节点分布在内存中各个角落,通过节点属性中的左右两个子节点指针指向各自的子节点,构建出一棵二叉树结构,把这些节点连在一起,pSF[0] 就是这棵树的根节点。因此如果要遍历这棵树,只要 pSF[0] 就够了。

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;
if(hn1 == NULL && hn2 == NULL) return 0; //如果两个节点都空,返回相等
if(hn1 == NULL) return 1; //如果第一个节点为空,则第二个节点大
if(hn2 == NULL) return -1; //反之第二个节点小
//如果都不空,则比较两个节点中的计数属性值,然后同上返回比较结果
if(hn1->count > hn2->count) return 1;
else if(hn1->count < hn2->count) return -1;
return 0;
}


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


(4)生成码字

  实验中码字数组中的一个元素为 unsigned char 类型,一个元素保存了 8 位的码字,一个码字中的一位(0或1)在存储时确实只占用了 1 bit。 假如有一个叶节点在树中的位置按照编码规则得到从根到叶的编码为 111100011。这个码字一共有 9 位,那么需要占用 unsigned char 类型数组中的两个元素的位置。

  生成码字时,首先遍历二叉树找到叶节点,然后逐层向上回到根部,先从叶到根编码。设这个数组为 bits,则 bits[0] 保存了码字的 1 到 8 位,bits[1] 保存了码字的第 9 位,而且一个字节的低位保存码字的低位。即bits[0]:11100011,bits[1]:00000001

  这是从叶到根的编码(110001111),真正的码字要从根到叶读(111100011),因此在 new_code 函数最后使用了一个对码字进行倒序的函数 reverse_bits,执行倒序之后 bits 数组变为bits[0]:10001111,bits[1]:00000001

  读码字的时候,先从 bits[0] 的低位向高位读,读完 bits[0] 读 bits[1],也是低位往高位读,这样就读出了正确的码字(111100011)。

具体代码说明

static void build_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSE) //遍历码树的函数
{
if(subtree == NULL) return; //如果是空树,返回
if(subtree->isLeaf) //如果是叶节点,则对叶节点进行编码
(*pSE)[subtree->symbol] = new_code(subtree);
else //如果都不是,那么先访问左节点,到了叶节点之后再访问右节点
{
build_symbol_encoder(subtree->zero, pSE);
build_symbol_encoder(subtree->one, pSE);
}
}


遍历码树时,先一直向下访问左子节点到叶节点,再回到根,再访问右子节点,pSE 的下标就是待编码的信源符号。

static huffman_code* new_code(const huffman_node* leaf) //生成码字的函数
{
//码字的位数 numbits,也就是树从下到上的第几层,还有保存码字的数组 bits
unsigned long numbits = 0;
unsigned char* bits = NULL;
while(leaf && leaf->parent) //如果还没到根节点
{
//那么得到当前节点的父节点,由码字位数得到码字在字节中的位置和码字的字节数
huffman_node *parent = leaf->parent;
unsigned char cur_bit = (unsigned char)(numbits % 8);
unsigned long cur_byte = numbits / 8;
if(cur_bit == 0) //如果比特位数为0,说明到了下一个字节,新建一个字节保存后面的码字
{
size_t newSize = cur_byte + 1; //新的字节数为当前字节数+1,size_t 即为 unsigned int 类型
bits = (char*)realloc(bits, newSize); //数组按照新的字节数重新分配空间
bits[newSize - 1] = 0; //并把新增加的字节设为0
}
if(leaf == parent->one) //如果是右子节点,按照Huffman树左0右1的原则,应当把当前字节中当前位置1
//先把1右移到当前位(cur_bit)位置,再把当前字节(bits[cur_byte])与移位后的1做或操作
bits[cur_byte] |= 1 << cur_bit;
++numbits; //然后码字的位数加1
leaf = parent; //下一位码字在父节点所在的那一层
}
//回到根之后编码完毕,对码字进行倒序
if(bits)
reverse_bits(bits, numbits);
//倒序后,输出码字数组
huffman_code *p = (huffman_code*)malloc(sizeof(huffman_code));
p->numbits = numbits; p->bits = bits;
return p;
}


static void reverse_bits(unsigned char* bits, unsigned long numbits) //对码字进行倒序的函数
{
//先判断码字最多需要多少个字节存储
unsigned long numbytes = numbytes_from_numbits(numbits);
//分配字节数所需的存储空间,还有当前字节数和比特位数
unsigned char *tmp = (unsigned char*)alloca(numbytes);
unsigned long curbit;
long curbyte = 0;
memset(tmp, 0, numbytes);
for(curbit = 0; curbit < numbits; ++curbit)
{
//判断当前位是字节里的哪一位,到了下一个字节,字节数+1
unsigned int bitpos = curbit % 8;
if(curbit > 0 && curbit % 8 == 0) ++curbyte;
//从后往前取码字中的每一位,再移位到所在字节的正确位置
tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);
}
memcpy(bits, tmp, numbytes);
}
//由比特位长度得到字节数。除以8取整,如果还有余数说明要再加一个字节
static unsigned long numbytes_from_numbits(unsigned long numbits)
{ return numbits / 8 + (numbits % 8 ? 1 : 0); }
/* 取出码字 bits 中的第 i 位
第 i 位在第 i/8 字节的第 i%8 位,把这一位移到字节最低位处,和 0000 0001 做与操作,从而只留下这一位,返回*/
static unsigned char get_bit(unsigned char* bits, unsigned long i)
{ return (bits[i / 8] >> i % 8) & 1; }


码字由数组 bits 存储,数组的一个元素有 8 位(一个字节),因此定义了 cur_bit 和 cur_byte 两个变量,用于标识当前的一位码字在 bits 中的字节位置和字节里的位位置。默认情况下码字数组 bits 全为 0,需要置 1 的情况就和 1 进行或操作把某些比特位置 1。

(5)写入码表,对文件进行编码

static int write_code_table(FILE* out, SymbolEncoder *se, unsigned int symbol_count) //写入码表的函数
{
unsigned long i, count = 0;
//还是要先统计下真实的码字种类,不一定256种都有
for(i = 0; i < MAX_SYMBOLS; ++i)
if((*se)[i]) ++count;
//把字节种类数和字节总数变成大端保存的形式,写入文件中
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)
{
fputc((unsigned char)i, out); //码表中有三种数据,先写入字节符号
fputc(p->numbits, out); //再写入码长
//最后得到字节数,写入码字
unsigned int numbytes = numbytes_from_numbits(p->numbits);
if(fwrite(p->bits, 1, numbytes, out) != numbytes) return 1;
}
}
return 0;
}


在文件中写入字节种类数和字节数时,系统按照小端方式写入,比如 256(100H) 写入后变为 00 01 00 00。为了在文件中能从左到右直接读出真实数据,这里先把它们变成了大端方式保存再写入文件,在解码时还要做一次转换。

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)
{
//逐字节读取待编码的文件,要找到当前符号(字节)uc对应的码字code,只需要把uc作为码字数组se的下标即可
unsigned char uc = (unsigned char)c;
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){
fputc(curbyte, out);
curbyte = 0; curbit = 0;
}
}
}
//处理一下最后一个字节的编码不足一字节的情况
if(curbit > 0) fputc(curbyte, out);
return 0;
}


对文件进行编码时,一个字节一个字节地读文件,把字节作为信源符号,查找码字数组得到码字。写文件也是一个字节一个字节写,有一些码字可能不足一个字节或超过一个字节(8位码字),那么就等到下一个符号的编码,直到凑足一个字节的长度再写入文件。因此编码后的数据中一个字节可能包含有原来文件的多个符号(字节),从而达到了数据压缩的目的。

Huffman解码流程

Created with Raphaël 2.1.0读入待解码的源文件读码表建立Huffman码树扫描文件,逐字符进行解码

读取压缩后文件首部Huffman码表:

/*
* read_code_table builds a Huffman tree from the code
* in the in file. This function returns NULL on error.
* The returned value should be freed with free_huffman_tree.
*/
static huffman_node*//读码表
read_code_table(FILE* in, unsigned int *pDataBytes)
{
huffman_node *root = new_nonleaf_node(0, NULL, NULL);
unsigned int count;

/* Read the number of entries.
(it is stored in network byte order). */
//读文件和写文件一样,按照小端方式读
if (fread(&count, sizeof(count), 1, in) != 1)
{
free_huffman_tree(root);
return NULL;
}
//所以按照大端方式存放的数据count,再转换一次就得到了正确结果
count = ntohl(count);
//ntohl()是将一个无符号长整形数从网络字节顺序转换为主机字节顺序
//从big-endian转化为little-endian

/* Read the number of data bytes this encoding represents. */
if (fread(pDataBytes, sizeof(*pDataBytes), 1, in) != 1)
{
free_huffman_tree(root);
return NULL;
}
*pDataBytes = ntohl(*pDataBytes);
// 原文件的总字节数pDataBytes也由littleendian转化为bigendian

/* Read the entries. */
//读完这些后,文件指针指向了码表开头,依次读取码表中的每一项,每一项由符号,码长,码字三种数据组成
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)//一次读一个字节,第一个字节是信源符号symbol
{
free_huffman_tree(root);
return NULL;
}
symbol = (unsigned char)c;

if ((c = fgetc(in)) == EOF)//第二个字节是码长数据numbits
{
free_huffman_tree(root);
return NULL;
}

numbits = (unsigned char)c;
//计算出这样一个码长需要多少个字节(numbytes个)保存,开辟与字节数对应的空间

numbytes = (unsigned char)numbytes_from_numbits(numbits);
bytes = (unsigned char*)malloc(numbytes);
if (fread(bytes, 1, numbytes, in) != numbytes)//然后读取numbytes个字节得到码字bytes
{
free(bytes);
free_huffman_tree(root);
return NULL;
}

/*
* Add the entry to the Huffman tree. The value
* of the current bit is used switch between
* zero and one child nodes in the tree. New nodes
* are added as needed in the tree.
*/
for (curbit = 0; curbit < numbits; ++curbit)//读完码表码符号三种数据后,开始由码字建立Huffman树
{
if (get_bit(bytes, curbit))//如果码字中的当前位为1
{
if (p->one == NULL)//那么没有右子节点则新建一个右子节点
{//如果是最后一位,就建立树叶节点,否则就当做一个父节点,后续建立他的子节点
p->one = curbit == (unsigned char)(numbits - 1)
? new_leaf_node (symbol)
: new_nonleaf_node (0, NULL, NULL);
p->one->parent = p;//设置好新建节点的父节点
}
p = p->one;//不管右子节点是不是新建的,都要把这个节点当成父节点,以便建立它后续的子节点
}
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);//和编码一样,只要有最上面的根节点就能遍历整棵树
}

return root;
}


对文件逐字符进行Huffman解码:

int huffman_decode_file(FILE *in, FILE *out)//Huffman解码
{
huffman_node *root, *p;
int c;
unsigned int data_count;

/* Read the Huffman code table. */
root = read_code_table(in, &data_count);//打开文件后首先读入码表,建立Huffman树,并且获取原文件的字节数
if (!root)
return 1;

/* Decode the file. */
p = root;
while (data_count > 0 && (c = fgetc(in)) != EOF)//准备好码树之后,一次读一个字节进行解码
{
unsigned char byte = (unsigned char)c;
unsigned char mask = 1;
//mask负责提取字节中的每一位,提取完之后向左移动一位来提取下一位。因此移动8位之后变成0,循环退出,读下一个字节
while (data_count > 0 && mask)
{
//如果当前字节为0,就转到左子树,否则转到右子树
p = byte & mask ? p->one : p->zero;
mask <<= 1;//准备读下一个字节

if (p->isLeaf)//如果走到了叶节点
{
fputc(p->symbol, out);//就输出叶节点中存储的符号
p = root;//然后转到根节点,再从头读下一个码字
--data_count;//而且剩下没解码的符号数-1
}
}
}

free_huffman_tree(root);
return 0;
}


2.输出

  在程序中添加代码,输出编码结果文件(以列表方式显示字符、字符发生概率、字符对应编码码字长度、字符对应编码码字)。在命令行参数中需要添多加一个参数‘t’来指定输出的文本文件,添加命令行参数方法在读入编码文件部分分析过。

定义信源符号的统计数据类型:

typedef struct huffman_stat_tag //信源符号的统计数据类型
{
unsigned long numbits; //码字长度
unsigned char *bits; //码字
double freq; //信源符号出现的频率
}huffman_stat;
typedef huffman_stat* SymbolStatices[MAX_SYMBOLS];


void getStatFreq(SymbolStatices* stat, SymbolFrequencies* sf, unsigned int symbol_count) //由信源符号数组得到出现频率的函数
{
unsigned int i;
for (i = 0; i < MAX_SYMBOLS; i++)
(*stat)[i] = (huffman_stat*)malloc(sizeof(huffman_stat)); //把统计数组信源符号的每个位置分配一块空间
for (i = 0; i < MAX_SYMBOLS; i++)
{
if ((*sf)[i]) //如果符号数组当前元素不为空
{
unsigned int j = (*sf)[i]->symbol; //那么得到当前元素保存的信源符号
(*stat)[j]->freq = (double)(*sf)[i]->count / symbol_count; //把符号作为下标,对 freq 赋值
}
}
for (i = 0; i < MAX_SYMBOLS; i++)
{
if (!(*sf)[i]) //找到那些信源符号为空的数组
(*stat)[i]->freq = 0; //信源符号频率为0
}
}


void getStatCode(SymbolStatices* stat, SymbolEncoder *se) //由码字数组得到统计数组中其它两项信息的函数
{

unsigned int i;
for (i = 0; i < MAX_SYMBOLS; i++)
{
//之前已经分配过存储空间了,如果当前符号存在,得到符号的码长和码字
if ((*se)[i])
{
(*stat)[i]->numbits = (*se)[i]->numbits;
(*stat)[i]->bits = (*se)[i]->bits;
}
}
}


void output_statistics(FILE* table, SymbolStatices stat) //将码字列表写入文件的函数
{
unsigned long i,j, count = 0;
for (i = 0; i < MAX_SYMBOLS; ++i)
if (stat[i]) ++count;
fprintf(table, "Symbol\t Frequency\t Length\t Code\n"); //表头每一项的说明
for (i = 0; i < count; i++)
{
fprintf(table, "%d\t", i); //第一列为信源符号
fprintf(table, "%f\t", stat[i]->freq); //第二列为符号的出现频率
//如果信源符号频率为零,码字为空指针,因此输出一个 0 作为码字长度然后输出下一个符号
if (stat[i]->freq == 0)
{
fprintf(table,"0\n");
continue;
}
fprintf(table, "%d\t", stat[i]->numbits); //第三列为码字长度
for (j = 0; j < numbytes_from_numbits(stat[i]->numbits); j++)
{
fprintf(table, "%x", stat[i]->bits[j]); //第四列为用十六进制表示的码字,可以与编码后文件中的码表对应
}
fprintf(table, "\t");
for (j = 0; j < stat[i]->numbits; j++)
{
//还有用二进制方式表示的码字,每次取出码字的一位,输出到文件中
unsigned char c = get_bit(stat[i]->bits, j);
fprintf(table, "%d", c);
}
fprintf(table, "\n");
}
}


三、实验结果比较分析

  选择不同格式类型的文件,使用Huffman编码器进行压缩得到输出的压缩比特流文件。根据第2步得到的编码结果文件,对各种不同格式的文件进行压缩效率的分析。



编码结果示例

与上述代码对应,编码结果分为符号、符号出现频率、码长、码字四部分。可以看出,频率高的符号为短码,低的符号编长码。

总共选择了九种不同格式的文件对它们进行压缩,如图所示



实验用文件

文件类型平均码长信息熵(bit/sym)源文件大小(KB)压缩后文件大小(KB)压缩比
docx7.4957.4681892.000
avi4.9974.956219413721.599
jpg7.9647.94031320.969
mp46.6666.6243863221.199
png8.0007.9964614611.000
pdf7.9217.9074734691.009
ppt5.5815.5481851311.412
rar7.9997.99239391.000
m3.8693.81124122.000
表1实验结果










表2各文件的概率分布图

  结合表 1 和表 2 可以看出Huffman编码的平均码长基本接近于信源熵,符号有 256 种,信源熵最大约为 8 bit/sym。

  Huffman编码对于字节分布不均匀的文件压缩效果好,而对于每个字节出现概率接近等概的文件就不能按照大概率短码,小概率长码的原则进行编码,最终使得文件基本没有压缩,特别是第 3,5,8 测试的 jpg,png,rar 文件,编码结果显示每个符号的码长都是 8 位,再加上存储的码表使得文件反而比压缩前更大了。

四、注意事项

1.程序运行前的命令参数设置



2.多项目工程下设置启动项目

有可能会遇到这样的问题,在于没把含有.exe的项目设为启动项



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: