一、基本概念
1、赫夫曼(Huffman)树又称最优二叉树或最优搜索树,是一种带权路径长度最短的二叉树。在许多应用中,常常赋给树中结点一个有某种意义的实数,称此实数为该结点的权。从树根结点到该结点之间的路径长度与该结点上权的乘积称为结点的带权路径长度(WPL),树中所有叶子结点的带权路径长度之和称为该树的带权路径长度.
2、两结点间的路径:从一结点到另一结点所经过的结点 序列;路径长度:从根结点到相应结点路径上的分支数目;树的路径长度:从根到每一结点的路径长度之和。
3、深度为k,结点数为n的二叉树,当且仅当每个结点的编号都与相同深度的满二叉树中从1到n的结点一一对应时,称为完全二叉树。在结点数目相同的二叉树中完全二叉树是路径长度最短的二叉树。
4、WPL最小的二叉树是最优二叉树(Huffman 树)。
5、赫夫曼(Huffman)树的特征
① 当叶子上的权值均相同时,完全二叉树一定是最优二叉树。否则完全二叉树不一定是最优二叉树。
② 在最优二叉树中,权值越大的叶子离根越近。
③ 最优二叉树的形态不唯一,但WPL最小。
如上图中,只有(d)才是赫夫曼树。其中,圆围中的数值代表权值。
二、算法思想
(1) 以权值分别为W1,W2...Wn的n各结点,构成n棵二叉树T1,T2,...Tn并组成森林F={T1,T2,...Tn},其中每棵二叉树 Ti仅有一个权值为 Wi的根结点;
(2) 在F中选取两棵根结点权值最小的树作为左右子树构造一棵新二叉树,并且置新二叉树根结点权值为左右子树上根结点的权值之和(根结点的权值=左右孩子权值之和,叶结点的权值= Wi)
(3) 从F中删除这两棵二叉树,同时将新二叉树加入到F中;
(4) 重复(2)、(3)直到F中只含一棵二叉树为止,这棵二叉树就是Huffman树。
三、C语言描述
(1)我们用如下结构来存储赫夫曼树:
用大小为2n-1的一维数组来存储哈夫曼树中的结点,其存储结构为:
#define n 100 //叶结点数目 #define m 2*n-1 //树中结点总数 typedef struct { float weight; //权值,设权值均大于零 int lchild,rchild,parent; //左右孩子及双亲指针 } HTNode; typedef HTNode HuffmanTree[m]; //哈夫曼树是一维数组
因为C语言数组的下界为0,用-1表示空指针。树中结点的lchild、rchild和parent不等于-1时,分别表示该结点的左、右孩子和双亲结点在数组中的下标。
设置parent域有两个作用:一是使查找某结点的双亲变得简单;二是可通过判定parent的值是否为-1来区分根与非根结点。
(2)哈夫曼算法的简要描述
在上述存储结构上实现的哈夫曼算法可大致描述为(设T的类型为HuffmanTree):
①初始化 将T[0…m-1]中2n-1个结点里的三个指针均置为空(即置为-1),权值置为0。
②输入 读入n个叶子的权值存于数组的前n个分量(即T[0…n-1])中。它们是初始森林中n个孤立的根结点上的权值。
③合并 对森林中的树共进行n-1次合并,所产生的新结点依次放入数组T的第i个分量中(n≤i≤m-1)。每次合并分两步:
1) 在当前森林T[0…i-1]的所有结点中,选取权值最小和次小的两个根结点T [p1]和T[p2]作为合并对象,这里0≤p1,p2≤i-1。
2) 将根为T[p1]和T[p2]的两棵树作为左右子树合并为一棵新的树,新树的根是新结点T[i]。
具体操作:
将T[p1]和T[p2]的parent置为i;
将T[i]的lchild和rchild分别置为p1和p2;
新结点T[i]的权值置为T[p1]和T[p2]的权值之和。
合并后T[pl]和T[p2]在当前森林中已不再是根,因为它们的双亲指针均已指向了T[i],所以下一次合并时不会被选中为合并对象。
(3)赫夫曼算法的数组法构造
void CreateHuffmanTree(HuffmanTree T) { int i,p1,p2; //构造哈夫曼树,T[m-1]为其根结点 InitHuffmanTree(T); //T初始化 InputWeight(T); //输入叶子权值至T[0..n-1]的weight域 for(i=n;i<m;i++) { SelectMin(T,i-1,&p1,&p2);//共进行n-1次合并,新结点依次存于T[i]中 //在T[0…i-1]中选择两个权最小的根结点,其序号分别为p1和p2 T[p1].parent=T[p2].parent=i; T[i].1child=p1; //最小权的根结点是新结点的左孩子 T[i].rchild=p2; //次小权的根结点是新结点的右孩子 T[i].weight=T[p1].weight+T[p2].weight; }//for }//CreateHuffman
四、C语言实现
#include "stdio.h" #include "stdlib.h" #define m 100 struct ptree //定义二叉树结点类型 { int w; //定义结点权值 struct ptree *lchild; //定义左子结点指针 struct ptree *rchild; //定义右子结点指针 }; struct pforest //定义链表结点类型 { struct pforest *link; struct ptree *root; }; int WPL=0; //初始化WTL为0 struct ptree *hafm(); void travel(); struct pforest *inforest(struct pforest *f,struct ptree *t); void travel(struct ptree *head,int n) { //为验证harfm算法的正确性进行的遍历 struct ptree *p; p=head; if(p!=NULL) { if((p->lchild)==NULL && (p->rchild)==NULL) //如果是叶子结点 { printf("%d ",p->w); printf("the hops of the node is: %d/n",n); WPL=WPL+n*(p->w); //计算权值 }//if travel(p->lchild,n+1); travel(p->rchild,n+1); }//if }//travel struct ptree *hafm(int n, int w[m]) { struct pforest *p1,*p2,*f; struct ptree *ti,*t,*t1,*t2; int i; f=(pforest *)malloc(sizeof(pforest)); f->link=NULL; for(i=1;i<=n;i++) //产生n棵只有根结点的二叉树 { ti=(ptree*)malloc(sizeof(ptree));//开辟新的结点空间 ti->w=w[i]; //给结点赋权值 ti->lchild=NULL; ti->rchild=NULL; f=inforest(f, ti); //按权值从小到大的顺序将结点从上到下地挂在一颗树上 }//for while(((f->link)->link)!=NULL)//至少有二棵二叉树 { p1=f->link; p2=p1->link; f->link=p2->link; //取出前两棵树 t1=p1->root; t2=p2->root; free(p1); //释放p1 free(p2); //释放p2 t=(ptree *)malloc(sizeof(ptree));//开辟新的结点空间 t->w = (t1->w)+(t2->w); //权相加 t->lchild=t1; t->rchild=t2; //产生新二叉树 f=inforest(f,t); }//while p1=f->link; t=p1->root; free(f); return(t); //返回t } pforest *inforest(struct pforest *f,struct ptree *t) { //按权值从小到大的顺序将结点从上到下地挂在一颗树上 struct pforest *p, *q, *r; struct ptree *ti; r=(pforest *)malloc(sizeof(pforest)); //开辟新的结点空间 r->root=t; q=f; p=f->link; while (p!=NULL) //寻找插入位置 { ti=p->root; if(t->w > ti->w) //如果t的权值大于ti的权值 { q=p; p=p->link; //p向后寻找 }//if else p=NULL; //强迫退出循环 }//while r->link=q->link; q->link=r; //r接在q的后面 return(f); //返回f } void InPut(int &n,int w[m]) { printf("please input the sum of node/n"); //提示输入结点数 scanf("%d",&n); //输入结点数 printf ("please input weight of every node/n"); //提示输入每个结点的权值 for(int i=1;i<=n;i++) scanf("%d",&w[i]); //输入每个结点权值 } int main( ) { struct ptree *head; int n,w[m]; InPut(n,w); head=hafm(n,w); travel(head,0); printf("The length of the best path is WPL=%d", WPL);//输出最佳路径权值之和 return 1; }
五、最优二叉树的应用
哈夫曼树的应用很广,哈夫曼编码就是哈夫曼树在电讯通信中的应用之一。它采用不等长编码,让出现次数多的字符用短码,且任一编码不能是另一编码的前缀(我们称之为前缀编码,或非前缀码)。设有n种字符,每种字符出现的次数为Wi,其编码长度为Li (i=1,2,...n),则整个电文总长度为∑ Wi Li ,要得到最短的电文,即使得∑ Wi Li最小。也就是以字符出现的次数为权值,构造一棵Huffman树,并规定左分支编码位0,右分支编码为1,则字符的编码就是从根到该字符所在的叶结点的路径上的分支编号序列。用构造Huffman树编出来的码,称为Huffman编码。
为了获得传送电文的最短长度,可将字符出现的次数(频率)作为权值赋予该结点,构造一棵WPL最小的哈夫曼树,由此得到的二进制前缀编码就是最优前缀编码,也称哈夫曼编码。可以验证,用这样的编码传送电文可使总长最短。
我们在修改程序时,只要将原来的travel改成如下即可实现赫夫曼编码,当然,译码原理也相同:
#define count 15 char ch[count][count]; int counter=0; int tag,counter2=0; void Initch() { for(int i=0;i<count;i++) ch[i][count-1]='/0'; } void travel(struct ptree *head,int n,int tag) { //为验证harfm算法的正确性进行的遍历 struct ptree *p; p=head; if(p!=NULL) { if(tag==-1) { ch[counter][counter2++]='0'; //for(int j=counter;j<count-1;j++) strcpy(ch[j+1],ch[counter]); } if(tag==+1) { ch[counter][counter2++]='1'; for(int j=counter;j<count-1;j++) strcpy(ch[j+1],ch[counter]); } if((p->lchild)==NULL && (p->rchild)==NULL) //如果是叶子结点 { printf("%d ",p->w); printf("the hops of the node is: %d ",n); printf("Code is %s/n",ch[counter]); counter++; counter2--; WPL=WPL+n*(p->w); //计算权值 }//if travel(p->lchild,n+1,-1); travel(p->rchild,n+1,+1); }//if }//travel