一、实验原理
Huffman Coding (霍夫曼编码)是一种无失真编码的编码方式,Huffman 编码是可 变字长编码(VLC)的一种。
Huffman 编码基于信源的概率统计模型,它的基本思路是,出现概率大的信源符 号编长码,出现概率小的信源符号编短码,从而使平均码长最小。在程序实现中常使用一种叫做树的数据结构实现 Huffman 编码,由它编出的码是 即时码。
Huffman 编码的方法:
1 统计符号的发生概率;
2 把频率按从小到大的顺序排列
3 每一次选出最小的两个值,作为二叉树的两个叶子节点,将和作为它们的根节点,这两个叶子节点不再参与比较,新的根节点参与比较;
4 重复 3,直到最后得到和为 1 的根节点;
5 将形成的二叉树的左节点标 0,右节点标 1,把从最上面的根节点到最下面的叶子节点途中遇到的 0,1 序列串起来,就得到了各个符号的编码。
二、实验流程及代码
1、数据结构
1.1Huffman结点
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;
1.2Huffman 码字结点
typedef struct huffman_code_tag
{
/* The length of this code in bits. */
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编码
2.1编码流程
2.2第一次扫描,统计信源字符发生频率(8 比特,共 256 个信源符号)
2.2.1创建一个 256 个元素的指针数组,用以保存 256 个信源符号的频率。其下 标对应相应字符的 ASCII 码。
2.2.2数组中的非空元素为当前待编码文件中实际出现的信源符号。
2.2.3程序代码如下:
typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS];//信源符号数组,数据类型是huffman_node
static unsigned int get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in)//统计各字节的出现频率的函数
{
int c;
unsigned int total_count = 0;//总字节数为0
/* Set all frequencies to 0. */
init_frequencies(pSF);//初始化所有符号频率为0
/* Count the frequency of each symbol in the input file. */
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;
}
2.3 建立Huffman树并计算符号对应的Huffman码字
2.3.1 按频率从小到大顺序排序并建立 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 1
printf("BEFORE SORT\n");//出错了
print_freqs(pSF); //演示堆栈的使用
#endif
/* Sort the symbol frequency array by ascending frequency. */
qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);//按信源符号出现频率大小排序,小概率符号在前(pSF数组中)
#if 1
printf("AFTER SORT\n");
print_freqs(pSF);
#endif
/* Get the number of symbols. */
for(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n)//得到当前待编码文件中所出现的信源符号的种类总数
;
/*
* Construct a Huffman tree. This code is based
* on the algorithm given in Managing Gigabytes
* by Ian Witten et al, 2nd edition, page 34.
* Note that this implementation uses a simple
* count instead of probability.
*/
for(i = 0; i < n - 1; ++i)//建立huffman树需要合并n-1次,所以循环n-1次
{
/* Set m1 and m2 to the two subsets of least probability. */
m1 = (*pSF)[0];
m2 = (*pSF)[1];
//将m1,m2置为当前频数最小的两个信源符号
/* Replace m1 and m2 with a set {m1, m2} whose probability
* is the sum of that of m1 and m2. */
(*pSF)[0] = m1->parent = m2->parent =
new_nonleaf_node(m1->count + m2->count, m1, m2);
(*pSF)[1] = NULL;//将m1,m2合并为一个huffman节点加入到数组中,新节点为这两个节点的父节点,合并之后,第二个节点为空
/* Put newSet into the correct count position in pSF. */
qsort((*pSF), n, sizeof((*pSF)[0]), SFComp);//重新排序
}
/* Build the SymbolEncoder array from the tree. *///由建立的huffman树计算每个符号的码字
pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));
memset(pSE, 0, sizeof(SymbolEncoder));
build_symbol_encoder((*pSF)[0], pSE);//从树根开始为每个符号构建码字
return pSE;
}
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;
//两个排序元素为树节点类型
/* Sort all NULLs to the end. */
if(hn1 == NULL && hn2 == NULL)//两个为空返回0
return 0;
if(hn1 == NULL)//1为空则2大
return 1;
if(hn2 == NULL)//2为空则1大
return -1;
if(hn1->count > hn2->count)//1比2大返1
return 1;
else if(hn1->count < hn2->count)//2比1大返-1
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;
}
2.3.2 递归遍历 Huffman 树,对存在的每个字符计算码字
static void build_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSF)//递归遍历huffman树
{
if(subtree == NULL)//是否到了root,说明编码结束
return;
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)
{
/* Build the huffman code by walking up to
* the root node and then reversing the bits,
* since the Huffman code is calculated by
* walking down the tree. */
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);//所编位在当前byte中的位置
unsigned long cur_byte = numbits / 8;//当前是第几个byte
/* If we need another byte to hold the code,
then allocate it. *///realloc与malloc不同,在保持原有的数据不变的情况下重新分配新的空间,原有数据存在新空间的前面部分,空间的地址可能有变化需注意
if(cur_bit == 0)
{
size_t newSize = cur_byte + 1;
bits = (char*)realloc(bits, newSize);
bits[newSize - 1] = 0; /* Initialize the new byte. *///初始化新分配的8bit为0
}
/* If a one must be added then or it in. If a zero
* must be added then do nothing, since the byte
* was initialized to zero. */
if(leaf == parent->one)//左移1至当前byte'的当前位
bits[cur_byte] |= 1 << cur_bit;
++numbits;
leaf = parent;
}
if(bits)
reverse_bits(bits, numbits);//整个码字逆序
p = (huffman_code*)malloc(sizeof(huffman_code));
p->numbits = numbits;
p->bits = bits;//整数个字节,与numbits配合才可得到真正码字
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);
}
2.4将Huffman码表写入文件
/* Write the entries. *///写入码表
for(i = 0; i < MAX_SYMBOLS; ++i)
{
huffman_code *p = (*se)[i];
if(p)
{
unsigned int numbytes;//码表中有三种数据,先写入字节符号
/* Write the 1 byte symbol. */
fputc((unsigned char)i, out);//写入码长
/* Write the 1 byte code bit length. */
fputc(p->numbits, out);
/* Write the code bytes. */ //得到字节数,写入码字
numbytes = numbytes_from_numbits(p->numbits);
if(fwrite(p->bits, 1, numbytes, out) != numbytes)
return 1;
}
}
2.5第二次扫描文件,对文件查表进行Huffman编码,并写入文件
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];//查表[逐字节读取待编码的文件,要找到当前符号(字节)uc对应的码字code,只需要把uc作为码字数组se的下标]
unsigned long i;
for(i = 0; i < code->numbits; ++i)//将码字写入文件
{
/* Add the current bit to curbyte. *///把码字中的一个bit位放到编码字节位置
curbyte |= get_bit(code->bits, i) << curbit;
/* If this byte is filled up then write it
* out and reset the curbit and curbyte. */
if(++curbit == 8)//每次写入一个字节
{
fputc(curbyte, out);
curbyte = 0;
curbit = 0;
}
}
}
/*
* If there is data in curbyte that has not been
* output yet, which means that the last encoded
* character did not fall on a byte boundary,
* then output it.
*/
if(curbit > 0)
fputc(curbyte, out); //处理一下最后一个字节的编码不足一字节的情况
return 0;
}
3 Huffman 解码
3.1解码流程:
3.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;
/* Read the number of entries.
(it is stored in network byte order). */
if(fread(&count, sizeof(count), 1, in) != 1)//如果没有数据就返回NULL
{
free_huffman_tree(root);
return NULL;
}
count = ntohl(count);//将一个无符号长整形数从网络字节顺序转换为主机字节顺序
/* 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);
/* 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)//一次读一个字节,
{
free_huffman_tree(root);
return NULL;
}
symbol = (unsigned char)c;//第一个字节是信源符号symbol
if((c = fgetc(in)) == EOF)
{
free_huffman_tree(root);
return NULL;
}
numbits = (unsigned char)c;//第二个是码长数据numbits
numbytes = (unsigned char)numbytes_from_numbits(numbits);
bytes = (unsigned char*)malloc(numbytes);
//计算出需要的字节数并开辟空间
if(fread(bytes, 1, numbytes, in) != numbytes)//得到byte数
{
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;//1的父节点指向当前节点
}
p = p->one;//沿'1'方向下移一级
}
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;//p为父节点
}
}
free(bytes);
}
return root;//遍历整棵树
}
3.3读取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)//data_count>0,逻辑上仍有数据,(c=fgetc(in))!=EOF),文件中仍有数据
{
unsigned char byte = (unsigned char)c;
unsigned char mask = 1;//mask用于逐位读出码字
while(data_count > 0 && mask)//loop9:mask=0x00000000,跳出循环
{
p = byte & mask ? p->one : p->zero;//沿Huffman树前进
mask <<= 1;//左移,准备下一个字节
if(p->isLeaf)//如果走到了叶节点
{
fputc(p->symbol, out);//输出叶节点中存储的符号
p = root;//转到根节点从头读下一个码字
--data_count;//没解码的符号数-1
}
}
}
free_huffman_tree(root);//所有Huffman码字均已解码输出,文件解码完毕
return 0;
}
4.实验中需要添加代码将编码结果列表输出,输出列表主要包含四项:信源符号,符号频率(或出现次数),符号的码字长度,码字。我们添加代码使输出一串数据来更好观察编码过程。
4.1建立一个表的数据结构
//step2:add by yzhang for huffman statistics
typedef struct huffman_statistics_result//信源符号的统计数据类型
{
float freq[256];//码字出现频率
unsigned long numbits[256];//每个码字的长度
unsigned char bits[256][100];//码字是怎么样的
}huffman_stat;
4.2设定这个表输出是用什么参数,如何查找的
// step1: by yzhang, for huffman statistics
"-t - output huffman statistics\n",//t则是输出我们需要的表
//step1:end by yzhang
4.3在huffcode.c的main函数中加入初始化及操作命令
//step1:add by yzhang for huffman statistics
const char *file_out_table = NULL;//创建并初始化一个file为我们的表
//end by yzhang
FILE * outTable = NULL;//准备输出表格
//end by yzhang
// by yzhang for huffman statistics
case 't'://当命令参数中出现t时则输出成我们的表
file_out_table = optarg;
break;
//end by yzhang
//by yzhang for huffman statistics
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;
}
}
//end by yzhang
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)//将原本输出两个的函数输出改成三个,最后一个就是输出我们自己需要的表
4.4在huffman.c中我们是如何定义可以获取huffman编码文件功能呢?
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");
}
}
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;
}
}
if(count==total_count)
return 1;
else
return 0;
}
int huffST_getcodeword(SymbolEncoder *se, huffman_stat *st)
{
unsigned long i,j;
for(i = 0; i < MAX_SYMBOLS; ++i)
{
huffman_code *p = (*se)[i];//
if(p)
{
unsigned int numbytes;
st->numbits[i] = p->numbits;//得到码字长度
numbytes = numbytes_from_numbits(p->numbits);
for (j=0;j<numbytes;j++)
st->bits[i][j] = p->bits[j];//得到具体码字
}
else
st->numbits[i] =0;
}
return 0;
}
三、实验数据分析
这个实验中,我选择了十种不同的文件格式进行huffman编码,得到编码文件,然后通过频率计算出信源熵,并且计算原文件/编码后文件的压缩比。
下图是我们的实验文件。
我们随意查看一个文件输出的xls,freq与codelength之间的列是我计算信源熵用的中间数据。
我们十种文件格式的统计数据表格如下,我尽量选择了大一点的文件使各种信源符号能够出现。
十种文件格式的字节频率分布图分别如下
由以上实验结果表示,Huffman编码对字节概率相差大的文件编码效果好。
四、实验感想
实验代码较长,虽然Huffman编码原理比较简单,但是化为代码出现的时候还是容易弄混,比如根叶节点经常记错。而且这次实验出现了新的输入出文件的书写,“-i -o -t”的形式我觉得更清楚了。