从周五开始学习霍夫曼树,一直到今天终于完成,期间遇到了各种各样的棘手的问题,通过一遍遍在纸上分析每一步的具体状态得以解决。现在对学习霍夫曼树的过程加以记录
首先介绍霍夫曼树
霍夫曼树(Huffman Tree),又称最优二叉树,是一类带权路径长度最短的树。假设有n个权值{w1,w2,…,wn},如果构造一棵有n个叶子节点的二叉树,而这n个叶子节点的权值是{w1,w2,…,wn},则所构造出的带权路径长度最小的二叉树就被称为赫夫曼树。
这里补充下树的带权路径长度的概念。树的带权路径长度指树中所有叶子节点到根节点的路径长度与该叶子节点权值的乘积之和,如果在一棵二叉树中共有n个叶子节点,用Wi表示第i个叶子节点的权值,Li表示第i个也叶子节点到根节点的路径长度,则该二叉树的带权路径长度 WPL=W1*L1 + W2*L2 + … Wn*Ln。
根据节点的个数以及权值的不同,赫夫曼树的形状也各不相同,赫夫曼树具有如下特性:
对于同一组权值,所能得到的霍夫曼树不一定是唯一的。
赫夫曼树的左右子树可以互换,因为这并不影响树的带权路径长度。
带权值的节点都是叶子节点,不带权值的节点都是某棵子二叉树的根节点。
权值越大的节点越靠近赫夫曼树的根节点,权值越小的节点越远离赫夫曼树的根节点。
赫夫曼树中只有叶子节点和度为2的节点,没有度为1的节点。
一棵有n个叶子节点的赫夫曼树共有2n-1个节点。
赫夫曼树的构建步骤如下:
1、将给定的n个权值看做n棵只有根节点(无左右孩子)的二叉树,组成一个集合HT,每棵树的权值为该节点的权值。
2、从集合HT中选出2棵权值最小的二叉树,组成一棵新的二叉树,其权值为这2棵二叉树的权值之和。
3、将步骤2中选出的2棵二叉树从集合HT中删去,同时将步骤2中新得到的二叉树加入到集合HT中。
4、重复步骤2和步骤3,直到集合HT中只含一棵树,这棵树便是赫夫曼树。
假如给定如下5个权值:
则按照以上步骤,可以构造出如下面左图所示的赫夫曼树,当然也可能构造出如下面右图所示的赫夫曼树,这并不是唯一的。
Huffman编码
赫夫曼树的应用十分广泛,比如众所周知的在通信电文中的应用。在等传送电文时,我们希望电文的总长尽可能短,因此可以对每个字符设计长度不等的编码,让电文中出现较多的字符采用尽可能短的编码。为了保证在译码时不出现歧义,我们可以采取如下图所示的编码方式:
即左分支编码为字符0,右分支编码为字符1,将从根节点到叶子节点的路径上分支字符组成的字符串作为叶子节点字符的编码,这便是赫夫曼编码。我们根据上面左图可以得到各叶子节点的赫夫曼编码如下:
权值为5的也自己节点的赫夫曼编码为:11
权值为4的也自己节点的赫夫曼编码为:10
权值为3的也自己节点的赫夫曼编码为:00
权值为2的也自己节点的赫夫曼编码为:011
权值为1的也自己节点的赫夫曼编码为:010
而对于上面右图,则可以得到各叶子节点的赫夫曼编码如下:
权值为5的也自己节点的赫夫曼编码为:00
权值为4的也自己节点的赫夫曼编码为:01
权值为3的也自己节点的赫夫曼编码为:10
权值为2的也自己节点的赫夫曼编码为:110
权值为1的也自己节点的赫夫曼编码为:111
下面给出C语言实现
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/*定义霍夫曼树节点*/
typedef struct HTNode{
int parent;/*记录双亲*/
int Lchild;/*左右子树*/
int Rchild;
int Weight;/*记录权重*/
}HTNode;
typedef struct HTNode * HuffmanTree;
typedef char ** HuffmanCode;
/*在前k棵树种找到权重最小的树*/
int Min(HuffmanTree HT,int k){
int i=0,min_weight=0,min=0;
/*找出第一个双亲存在的节点,将其权值赋值给min_weight*/
/*注意此处不能直接将HT[0].weight赋给min_weight,原因是如果HT[0].weight最小,那么在第一次构造二叉树时就会被选走,而后续的每一轮选择最小权值构造二叉树的比较
还是先用HT[0].weight的值来进行判断,这样又会再次将其选走,从而产生逻辑上的错误。*/
while(HT[i].parent!=-1)
i++;
min_weight=HT[i].Weight;
min=i;
for(i;i<k;i++){
if(HT[i].Weight<min_weight&&HT[i].parent==-1){
min_weight=HT[i].Weight;
min=i;
}
}
/*找到最小权重的树,将其双亲置为1*/
/*!!!!!注意这的HT的下标!!!!!一晚上才找出这个小问题,别写成HT[i]!!!!!!*/
HT[min].parent=1;
return min;
}
/*从前k棵树中选出权重最小的两棵树,将其序号赋给min1和min2*/
Status SelectMin(HuffmanTree HT,int k,int &min1,int &min2){
min1=Min(HT,k);
min2=Min(HT,k);
return OK;
}
/*创建一课霍夫曼树,-1表示不存在*/
/*wet为一个记录权重的数组,类型为int*/
HuffmanTree CreateHuffmanTree(HuffmanTree HT,int *wet,int n){
int i=0;
int total=2*n-1;/*有n个数据需要编码,即有n个叶子节点,也就有n-1个度为2的节点,总节点数为n+n-1=2*n-1*/
/*初始状态下,前n个节点的双亲,左右子树应该均为-1,权重为对应的权重*/
/*用HT的前n个分量存储n棵树(由n个待编码的数据组成)的森林*/
/*申请total个int组成的动态数组*/
HT=(HuffmanTree)malloc(total*sizeof(HTNode));
if(!HT)
return ERROR;
for(i=0;i<n;i++){
HT[i].Lchild=-1;
HT[i].parent=-1;
HT[i].Rchild=-1;
HT[i].Weight=*wet;
wet++;
}
/*对n到total的分量进行初始化*/
for(i;i<total;i++){
HT[i].Lchild=-1;
HT[i].Rchild=-1;
HT[i].parent=-1;
HT[i].Weight=0;
}
/*用HT的后n-1个分量存储霍夫曼树*/
/*调用函数SelectMin找出森林中两棵权重最小的树*/
int min1=0,min2=0;
for(i=n;i<total;i++){
SelectMin(HT,i,min1,min2);
HT[min1].parent=i;
HT[min2].parent=i;
HT[i].Lchild=min1;
HT[i].Rchild=min2;
HT[i].Weight=HT[min1].Weight+HT[min2].Weight;
}
return HT;
}
/*从叶子节点开始逆向进行霍夫曼编码*/
/*HC用来储存霍夫曼编码值*/
Status HuffmanCoding(HuffmanTree HT,HuffmanCode &HC,int n){
/*HC本身是一个char类型数组的指针,其指向n个char类型的地址,所以给HC分配内存应该写成下面那样*/
HC=(HuffmanCode)malloc(n*sizeof(char *));
if(!HC)
return ERROR;
/*声明一个动态数组code,用来临时存储霍夫曼编码,数组含有n-1个霍夫曼码,加上一个‘\0‘终止符正好是n个元素,所以分配内存时*/
char *code;
code=(char *)malloc(n*sizeof(char));
if(!code)
return ERROR;
code[n-1]=‘\0‘;/*让最后一个元素为终止符*/
int i=0;
for(i=0;i<n;i++){
int current=i;
int father=HT[i].parent;
int start=n-1;
while(father!=-1){
if(current==HT[father].Lchild)
code[--start]=‘0‘;
else
code[--start]=‘1‘;
current=father;
father=HT[father].parent;
}
/*HC[i]用于最终存储霍夫曼码,是char类型的数组,有n个char类型的数据*/
HC[i]=(char *)malloc((n-start)*sizeof(char));
if(!HC[i])
return ERROR;
/*从临时空间中复制到HC[i]中*/
strcpy(HC[i],code+start);
}
/*释放临时存储空间*/
free(code);
code=NULL;
return OK;
}
int main(void){
int amount=0,i=0;
int *wet=(int *)malloc(amount*sizeof(int));
printf("请输入要编码的字符个数(个数为整型且>1)");
scanf("%d",&amount);
while(amount<=1){
printf("字符个数必须大于1\n");
scanf("%d",&amount);
}
printf("请输入要编码的字符的权值");
for(i=0;i<amount;i++){
scanf("%d",wet+i);
}
HuffmanTree HT;
HT=CreateHuffmanTree(HT,wet,amount);
HuffmanCode HC;
HuffmanCoding(HT,HC,amount);
for(i=0;i<amount;i++){
puts(HC[i]);
}
free(wet);
return OK;
}
参考了http://blog.csdn.net/ns_code/article/details/19174553这篇博客
2016-05-09更新:
今日在复习霍夫曼树的过程中,发现自己忽视了一些细节问题,造成了一些错误,下面加以记录并反思
问题一:出现在函数Min中
int Min(HuffmanTree HT,int k){
int min=0,min_weight=0,i=0;
/*注意此处是先寻找双亲不存在的节点再赋值,给min_weight赋值应该在循环以外*/
while(HT[i].parent!=-1)
i++;
min_weight=HT[i].weight;
min=i;
for(i;i<k;i++){
if(HT[i].weight<min_weight&&HT[i].parent==-1){
min_weight=HT[i].weight;
min=i;
}
}
/*赋值之后切记要将其双亲赋值为1,否则会出现错误*/
HT[min].parent=1;
return min;
}
问题二:出现在CreateHuffmanTree函数中
HuffmanTree CreateHuffmanTree(HuffmanTree HT,int * wet,int amount){
int i=0,min_weight=0,min=0;
int total=2*amount-1;
/*注意此处分配内存时,sizeof()中应该是HTNode!!要对HT的本质进行理解,HT是一棵树!*/
HT=(HuffmanTree)malloc(total*sizeof(HTNode));
if(!HT)
return ERROR;
for(i=0;i<amount;i++){
HT[i].Lchild=-1;
HT[i].parent=-1;
HT[i].Rchild=-1;
HT[i].weight=*wet;
wet++;
}
for(i;i<total;i++){
HT[i].Lchild=-1;
HT[i].Rchild=-1;
HT[i].parent=-1;
HT[i].weight=0;
}
int min1=0,min2=0;
/*注意此处i的初始值,要对这个过程加以理解,霍夫曼树是用amount之后的分量来存储的,故不能写i=0*/
for(i=amount;i<total;i++){
SelectMin(HT,min1,min2,i);
HT[min1].parent=i;
HT[min2].parent=i;
HT[i].Lchild=min1;
HT[i].Rchild=min2;
HT[i].weight=HT[min1].weight+HT[min2].weight;/*注意新生成的节点的权值为选出的两个最小的节点的权值之和*/
}
return HT;
}
这个问题主要是对建立霍夫曼树的过程理解不够透彻,导致在为HT分配内存时写错大小,和循环过程中将i的初始值误写为0,今后应当注意。
问题三:出现在HuffmanCoding函数中
Status HuffmanCoding(HuffmanTree HT,HuffmanCode &HC,int amount){
int i=0;
/*要先给HC分配内存。HC存储了amount个指向霍夫曼码的指针*/
HC=(HuffmanCode)malloc(amount*sizeof(char *));
if(!HC)
return ERROR;
char * code;
code=(char *)malloc(amount*sizeof(char));
if(!code)
return ERROR;
code[amount-1]=‘\0‘;
for(i=0;i<amount;i++){
int current=i;
int start=amount-1;
int father=HT[i].parent;
while(father!=-1){
if(current==HT[father].Lchild)
code[--start]=‘0‘;
else
code[--start]=‘1‘;
current=father;
father=HT[father].parent;
}
/*HC[i]是HC中若干个存储单元之一,存储着一个具体字符的霍夫曼码,所以分配内存空间时sizeof()中为char*/
HC[i]=(char *)malloc((amount-start)*sizeof(char));
if(!HC[i])
return ERROR;
strcpy(HC[i],code+start);
}
free(code);
code=NULL;
}
对HC的用途理解不够到位,再次强调,HC是一个存储了若干个指向霍夫曼码的指针的数组,HC[i]则用于存储具体字符的霍夫曼码
问题四:反复出现在主函数之中,必须加以记录,认真反省
int main(void){
HuffmanTree HT;
int amount=0;
printf("请输入需要编码的字符个数");
scanf("%d",&amount);
int i=0;
int * wet=(int *)malloc(amount*sizeof(int));
printf("请输入每个字符的权重");
for(i=0;i<amount;i++){
scanf("%d",wet+i);
}
/*此处忘记写HT=,导致未能成功建立霍夫曼树,由于这里要改变函数形参的值,一般情况考虑传入指针变量,但这个函数如果写成指针又太复杂,
很容易出错,故这里使用return把建立好的霍夫曼树直接返回,会方便许多,但是切记要把返回值赋给HT!!!*/
HT=CreateHuffmanTree(HT,wet,amount);
HuffmanCode HC;
HuffmanCoding(HT,HC,amount);
for(i=0;i<amount;i++){
puts(HC[i]);
}
free(wet);
}