Huffman编解码

来源:互联网 发布:学北京话软件 编辑:程序博客网 时间:2024/05/29 17:29

Huffman编解码算法实现与压缩效率分析

一.背景知识及相关公式

1.信源熵

信源熵是信息的度量单位,一般用H表示,单位是比特,对于任意一个随机变量,它的熵定义为,变量的不确定性越大,熵也就越大。


2.Huffman编码

(1)Huffman编码是一种无失真编码的编码方式,可变长编码的一种;
(2)Huffman编码基于信源的概率统计模型,它的基本思路是,出现概率小的信源符号编长码,出现概率大的信源符号编短码,从而使平均码长最小。
(3)在程序实现中常使用一种叫做树的数据结构实现Huffman编码,由它编出的码是即时码。

3.Huffman编码算法

(1)将文件以ASCII字符流的形式读入,统计每个符号的发生频率;
(2)将所有文件中出现过的字符按照频率从小到大的顺序排列 ;
(3)每一次选出最小的两个值,作为二叉树的两个叶子节点,将和作为它们根节点,这两个叶子节点不再参与比较,新的根节点参与比较;
(4)重复 3,直到最后得和为1的根节点;
(5)将形成的二叉树左节点标0,右节点标1,把从最上面的根节点到最下面叶子点途中遇到的0、1序列串起来,得到了各个字符的编码表示。

二.实验过程

1.数据结构

(1)Huffman节点

typedef struct huffman_node_tag{unsigned char isLeaf;  /*是否为叶节点,1表示是叶节点,0表示不是叶节点*/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;

2.Huffman编码

(1)编码流程
 
(2)读入源文件

int main(int argc, char** argv){char memory = 0;char compress = 1;int opt;const char *file_in = NULL, *file_out = NULL;const char *file_out_table = NULL;FILE *in = stdin;FILE *out = stdout;FILE * outTable = NULL;/*获取命令行参数*/while((opt = getopt(argc, argv, "i:o:cdhvmt:")) != -1) {switch(opt){case 'i'://输入文件file_in = optarg;break;case 'o'://输出文件file_out = optarg;break;case 'c'://编码compress = 1;break;case 'd'://解码compress = 0;break;case 'h'://参数用法输出到屏幕usage(stdout);return 0;case 'v'://版本信息输出到屏幕version(stdout);return 0;case 'm':memory = 1;//对内存数据进行编码break;case 't'://编码结果输出file_out_table = optarg;break;default:usage(stderr);return 1;}}/* If an input file is given then open it. */if(file_in){in = fopen(file_in, "rb");if(!in){fprintf(stderr,"Can't open input file '%s': %s\n",file_in, strerror(errno));return 1;}}/* If an output file is given then create it. */if(file_out){out = fopen(file_out, "wb");if(!out){fprintf(stderr,"Can't open output file '%s': %s\n",file_out, strerror(errno));//strerror函数返回指向错误原因字符串的指针,errno是错误代码,errno.h中包含很多错误定义return 1;}}if(file_out_table){outTable = fopen(file_out_table, "w");if(!outTable){fprintf(stderr,"Can't open output file '%s': %s\n",file_out_table, strerror(errno));return 1;}}if(memory)//memeory=1对内存数据进行编码,反之解码{return compress ?memory_encode_file(in, out) : memory_decode_file(in, out);}if(compress)//compress=1对文件数据进行编码,反之解码huffman_encode_file(in, out,outTable);elsehuffman_decode_file(in, out);if(in)fclose(in);if(out)fclose(out);if(outTable)fclose(outTable);return 0;}

       在解析命令行参数时用到getopt()函数。函数原型:int getopt(int nargc, char * const *nargv, const char* ostr)。参数nargc和nargv是由main()传递的参数个数和内容。第三个参数代表欲处理的选项字符串,i、o、t后加一个冒号是带值的参数,c、d、h、v、m后不带冒号是不带值的参数,变量optarg 即会指向这些额外参数,如果参数本身后面加两个冒号,则为可选值的参数。

      strerror()函数, 头文件:#include <string.h>,原型:char * strerror(int errnum),strerror()用来依参数errnum 的错误代码来查询其错误原因的描述字符串, 然后将该字符串指针返回。在程序示例中,如果输入文件不存在,则输出:No such file or directory. 

(3)文件编码函数,包含编码流程的具体操作
inthuffman_encode_file(FILE *in, FILE *out, FILE *out_Table){SymbolFrequencies sf;SymbolEncoder *se;huffman_node *root = NULL;int rc;unsigned int symbol_count;huffman_stat hs;//第一次扫描文件,获取每个信源符号在文件中的出现次数symbol_count = get_symbol_frequencies(&sf, in);         huffST_getSymFrequencies(&sf,&hs,symbol_count);//计算每个信源符号出现的概率se = calculate_huffman_codes(&sf);//建立huffman树,编码表root = sf[0];//huffman树的根节点为sf[0];    huffST_getcodeword(se, &hs);//获取码字output_huffman_statistics(&hs,out_Table);//以表格形式输出编码后各个信源符号的频率,码长及码字rewind(in);//回到文件开头,准备第二次扫描rc = write_code_table(out, se, symbol_count);//在输出文件中写入码表if(rc == 0)//写码表成功rc = do_file_encode(in, out, se);//对文件查表进行huffman编码,并写入输出文件/* Free the Huffman tree. */free_huffman_tree(root);free_encoder(se);return rc;}

(4)编码过程所用到的函数
a.统计信源符号频数
#define MAX_SYMBOLS 256  //按字节编码,最多出现256个信源符号typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS];//数组中的每个元素都是指向一个Huffman节点的指针typedef huffman_code* SymbolEncoder[MAX_SYMBOLS];//码字数组
static unsigned intget_symbol_frequencies(SymbolFrequencies *pSF, FILE *in){int c;unsigned int total_count = 0;//扫描过的信源符号总数初始化为0;/* Set all frequencies to 0. */init_frequencies(pSF);//将所有信源符号的频数初始化为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;//对应信源符号的出现频数初始化为0p->parent = 0;//该叶节点的父节点设为空return p;//返回指向该叶节点的指针}

static voidinit_frequencies(SymbolFrequencies *pSF){memset(*pSF, 0, sizeof(SymbolFrequencies));//把psF数组的每个元素都初始化为0#if 0unsigned int i;for(i = 0; i < MAX_SYMBOLS; ++i){unsigned char uc = (unsigned char)i;(*pSF)[i] = new_leaf_node(uc);}#endif}

b.统计各个信源符号出现概率

int huffST_getSymFrequencies(SymbolFrequencies *SF, huffman_stat *st,int total_count){int i,count =0;for(i = 0; i < MAX_SYMBOLS; ++i){if((*SF)[i]){st->freq[i]=(float)(*SF)[i]->count/total_count;//计算每个信源符号的出现概率count+=(*SF)[i]->count;//已经统计过的字节总数}else {st->freq[i]= 0;//该信源符号没有在文件中出现的过,频率为0}}if(count==total_count)return 1;//文件所有的信源符号都统计完毕elsereturn 0;}
c.建立huffman树,编码表

static SymbolEncoder*calculate_huffman_codes(SymbolFrequencies * pSF){unsigned int i = 0;unsigned int n = 0;huffman_node *m1 = NULL, *m2 = NULL;SymbolEncoder *pSE = NULL;#if 1printf("BEFORE SORT\n");print_freqs(pSF);   //演示堆栈的使用#endif//按照信源符号出现的概率大小排序,小概率符号在前,排序的依据是SFCompqsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);   #if 1printf("AFTER SORT\n");print_freqs(pSF);#endiffor(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n);//获取当前文件中实际出现信源符号的种类总数,有可能没有到256种//需要合并n-1次for(i = 0; i < n - 1; ++i){ m1 = (*pSF)[0];m2 = (*pSF)[1];//m1,m2置为当前频率最小的两个信源符号(*pSF)[0] = m1->parent = m2->parent =new_nonleaf_node(m1->count + m2->count, m1, m2);//将m1,m2合并成新的节点加入到数组中,频数为m1,m2的频数之和(*pSF)[1] = NULL;//第二个节点设为空qsort((*pSF), n, sizeof((*pSF)[0]), SFComp);//建立新节点之后重新排序}    //Huffman树建立完成/* Build the SymbolEncoder array from the tree. */pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));//为码字数组分配空间memset(pSE, 0, sizeof(SymbolEncoder));//初始化build_symbol_encoder((*pSF)[0], pSE);//计算每个信源符号的码字return pSE;}

 函数原型:void qsort( void *base, size_t num, size_t width, int(__cdecl*compare)(const void*,const void*)),参数依次为,数组的起始地址,数组的元素数,每个元素的大小,比较函数的指针。

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;}static intSFComp(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;//两者均不为空的情况下,比较两节点的计数countif(hn1->count > hn2->count)return 1;//节点1大,排在后面else if(hn1->count < hn2->count)return -1;//节点2大,排在后面return 0;}//递归遍历Huffman树static voidbuild_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSF){if(subtree == NULL)return;//如果已经到达root,则编码结束if(subtree->isLeaf)(*pSF)[subtree->symbol] = new_code(subtree);//如果是叶节点则进行编码,生成码字else{build_symbol_encoder(subtree->zero, pSF);build_symbol_encoder(subtree->one, pSF);//如果不是叶节点,则开始左访问子节点和右子节点}}//走到叶节点后,开始从叶节点爬到根节点生成码字的函数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!=0表示当前字符存在,应该编码,leaf->parent!=0表示未到达根节点,编码未完成{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;//新的字节数等于当前字节数加1bits = (char*)realloc(bits, newSize);//保持原有数据不变重新分配空间bits[newSize - 1] = 0; //新分配的一个字节初始化为0}if(leaf == parent->one)//如果叶节点为右子节点,把当前位置1bits[cur_byte] |= 1 << cur_bit;                  ++numbits;//码长加1leaf = parent;//转移到父节点这一层,向上爬一级}//回到根节点,编码完成if(bits)reverse_bits(bits, numbits);//码字逆序p = (huffman_code*)malloc(sizeof(huffman_code));//分配节点空间p->numbits = numbits;//码长p->bits = bits;//码字数组,需要与码长numbits配合才可得到真正的码字。return p;}
static voidreverse_bits(unsigned char* bits, unsigned long numbits){unsigned long numbytes = numbytes_from_numbits(numbits);//获取码字所需的字节数unsigned char *tmp =    (unsigned char*)alloca(numbytes);//分配空间,alloca是在栈(stack)上申请空间,用完马上就释放.unsigned long curbit;//当前位码字在字节中的位置,即该码字存在字节的哪一位long curbyte = 0;//当前位码字在bit数组中字节位置,即该码字存在哪个字节memset(tmp, 0, numbytes);for(curbit = 0; curbit < numbits; ++curbit){unsigned int bitpos = curbit % 8;if(curbit > 0 && curbit % 8 == 0)++curbyte;//判断当前位到字节的哪一位,满8位后要增加一个字节继续存储码字//从高位到低位获取码字的每一位,并移动到正确的位置,暂存在tmp数组中 tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);}memcpy(bits, tmp, numbytes);}//获取码字的第i位的函数,把i/8 字节的第 i%8 位移到字节的最低位,再和1相与static unsigned charget_bit(unsigned char* bits, unsigned long i){return (bits[i / 8] >> i % 8) & 1;}
   在这里增加reverse_bits()函数的原因?
   举例说明从根节点到叶节点的序列为1100 0111 1,生成码字时从叶爬到根,从低位向高位逐位存储存在bit数组中,bit[0]=1000  1111 ,bit[1]=0000 0001;
正确的码字顺序为1111 0001 1,从低位到高位存在bit数组中,bit[0]= 1110 0011,bit[1]=0000 0001,reverse_bits()函数的作用就是逆转码字的存储顺序,方便读出正确的码字。

d.以表格形式输出编码的结果,包括信源符号、概率、码长、码字

void output_huffman_statistics(huffman_stat *st,FILE *out_Table){int i,j;unsigned char c;fprintf(out_Table,"symbol\t   freq\t   codelength\t   code\n");for(i = 0; i < MAX_SYMBOLS; ++i){fprintf(out_Table,"%d\t   ",i);fprintf(out_Table,"%f\t   ",st->freq[i]);fprintf(out_Table,"%d\t    ",st->numbits[i]);if(st->numbits[i]){for(j = 0; j < st->numbits[i]; ++j){c =get_bit(st->bits[i], j);fprintf(out_Table,"%d",c);}}fprintf(out_Table,"\n");}}
e.写码表
static intwrite_code_table(FILE* out, SymbolEncoder *se, unsigned int symbol_count){unsigned long i, count = 0;//获取se中存储的实际码字种类for(i = 0; i < MAX_SYMBOLS; ++i){if((*se)[i])++count;}i = htonl(count);   //htonl就是把本机字节顺序转化为网络字节顺序,即大尾序,这里作一个转换,方便文件可以从左到右读数据//在网络传输中,采用big-endian序,对于0x0A0B0C0D ,传输顺序就是0A 0B 0C 0D ,//因此big-endian作为network byte order,little-endian作为host byte order。//little-endian的优势在于unsigned char/short/int/long类型转换时,存储位置无需改变if(fwrite(&i, sizeof(i), 1, out) != 1)return 1;/* Write the number of bytes that will be encoded. */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;fputc((unsigned char)i, out);//写符号fputc(p->numbits, out);//写码长numbytes = numbytes_from_numbits(p->numbits);//获取该符号的码字所需要的字节数if(fwrite(p->bits, 1, numbytes, out) != numbytes)//写码字return 1;}}return 0;}
(5)第二次扫描,对源文件进行编码输出
static intdo_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 |= get_bit(code->bits, i) << curbit;//获取码字的每一位,并放到编码字节的位置//可能存在某些信源符号的码字的长度不满一个字节,这个时候继续编码,直到满一个字节为止再写入文件if(++curbit == 8){fputc(curbyte, out);curbyte = 0;curbit = 0;}}}//如果最后一个信源符号编码完成后不满足一个字节,则直接写入文件if(curbit > 0)fputc(curbyte, out);return 0;}

3.Huffman解码

(1)解码流程

(2)读码表
static huffman_node*read_code_table(FILE* in, unsigned int *pDataBytes){huffman_node *root = new_nonleaf_node(0, NULL, NULL);unsigned int count;//读取countif(fread(&count, sizeof(count), 1, in) != 1){free_huffman_tree(root);return NULL;}//因为count是以大端模式存储,所以这里用ntohl函数把它转换为小端模式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)//读取信源符号{free_huffman_tree(root);return NULL;}symbol = (unsigned char)c;if((c = fgetc(in)) == EOF)//读取码长{free_huffman_tree(root);return NULL;}numbits = (unsigned char)c;//获取该码字所需要的字节数并分配空间numbytes = (unsigned char)numbytes_from_numbits(numbits);bytes = (unsigned char*)malloc(numbytes);if(fread(bytes, 1, numbytes, in) != numbytes)//读码字{free(bytes);free_huffman_tree(root);return NULL;}//读取当前码字的每一位,建立由根节点到叶节点的huffman树for(curbit = 0; curbit < numbits; ++curbit){if(get_bit(bytes, curbit))//判断当前码字的读取位是否为1{//读取位为1if(p->one == NULL)//如果没有右子节点,则新建一个右子节点{p->one = curbit == (unsigned char)(numbits - 1)? new_leaf_node(symbol): new_nonleaf_node(0, NULL, NULL);//如果当前位是码字的最后一位则建立叶节点,否则建立内部节点p->one->parent = p;//1的一支的父节点指向当前节点}p = p->one;//把右子节点当做新的父节点,沿着1的方向向下移一级}else{//读取位为0if(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;//把左子节点当做新的父节点,沿着0的方向向下移一级}}free(bytes);}return root;//返回根节点}
(3)解码
inthuffman_decode_file(FILE *in, FILE *out){huffman_node *root, *p;int c;unsigned int data_count;root = read_code_table(in, &data_count);//读码表,建huffman树if(!root)return 1;//树建立失败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;//用于逐位读取码字while(data_count > 0 && mask){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;}

三.实验结果分析

以doc类型文件为例,依次输出符号,频率,码长和码字。概率较大的符号明显码字长度要比概率小的符号短很多。

以下有10种类型文件

结果分析


各种类型文件的符号概率分布图








由图表可见,Huffman编码对于符号概率分布越均匀的文件,压缩效率越低。比如rm,AVI文件概率分布较均匀,压缩比接近1。对于符号概率分布不均匀的文件,压缩效果越好。比如bmp文件,压缩比达到1.4.。表格中信源熵总是小于等于平均码长,实际上,信源熵是平均码长的下限。

0 0