构造最优二叉树-赫夫曼(Huffman)树算法

http://blog.163.com/zhoumhan_0351/blog/static/3995422720098275836215/

构造最优二叉树-赫夫曼(Huffman)树算法  

一、基本概念

1赫夫曼(Huffman)树又称最优二叉树或最优搜索树,是一种带权路径长度最短的二叉树。在许多应用中,常常赋给树中结点一个有某种意义的实数,称此实数为该结点的权。从树根结点到该结点之间的路径长度与该结点上权的乘积称为结点的带权路径长度(WPL),树中所有叶子结点的带权路径长度之和称为该树的带权路径长度,通常记为:





2两结点间的路径:从一结点到另一结点所经过的结点序列;路径长度:从根结点到相应结点路径上的分支数目;树的路径长度:从根到每一结点的路径长度之和。

3、深度为k,结点数为n的二叉树,当且仅当每个结点的编号都与相同深度的满二叉树中从1n的结点一一对应时,称为完全二叉树。在结点数目相同的二叉树中完全二叉树是路径长度最短的二叉树。

4WPL最小的二叉树是最优二叉树(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的一维数组来存储哈夫曼树中的结点,其存储结构为:

#definen 100              //叶结点数目

#definem 2*n-1            //树中结点总数

typedefstruct

{float weight;            //权值,设权值均大于零

intlchild,rchild,parent;  //左右孩子及双亲指针

}HTNode;

  typedefHTNode HuffmanTree[m]; //哈夫曼树是一维数组

     因为C语言数组的下界为0,用-1表示空指针。树中结点的lchildrchildparent不等于-1时,分别表示该结点的左、右孩子和双亲结点在数组中的下标。

设置parent域有两个作用:一是使查找某结点的双亲变得简单;二是可通过判定parent的值是否为-1来区分根与非根结点。

(2)哈夫曼算法的简要描述

在上述存储结构上实现的哈夫曼算法可大致描述为(T的类型为HuffmanTree)

初始化T[0m-1]2n-1个结点里的三个指针均置为空(即置为-1),权值置为0

输入读入n个叶子的权值存于数组的前n个分量(T[0n-1])中。它们是初始森林中n个孤立的根结点上的权值。

合并对森林中的树共进行n-1次合并,所产生的新结点依次放入数组T的第i个分量中(nim-1)。每次合并分两步:

1)在当前森林T[0i-1]的所有结点中,选取权值最小和次小的两个根结点T[p1]T[p2]作为合并对象,这里0p1p2i-1

       2)将根为T[p1]T[p2]的两棵树作为左右子树合并为一棵新的树,新树的根是新结点T[i]

具体操作:

T[p1]T[p2]parent置为i

T[i]lchildrchild分别置为p1p2

新结点T[i]的权值置为T[p1]T[p2]的权值之和。

合并后T[pl]T[p2]在当前森林中已不再是根,因为它们的双亲指针均已指向了T[i],所以下一次合并时不会被选中为合并对象。

(3)赫夫曼算法的数组法构造

voidCreateHuffmanTree(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]中选择两个权最小的根结点,其序号分别为p1p2

  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"

#definem 100

 

structptree             //定义二叉树结点类型

{

intw;                  //定义结点权值

structptree *lchild;     //定义左子结点指针

structptree *rchild;     //定义右子结点指针

};

 

structpforest             //定义链表结点类型

{

structpforest *link;

structptree *root;

};

 

intWPL=0;                 //初始化WTL0

structptree *hafm();

voidtravel();

structpforest *inforest(struct pforest *f,struct ptree *t);

 

voidtravel(struct ptree *head,int n)                     

{

//为验证harfm算法的正确性进行的遍历

structptree *p;

p=head;

if(p!=NULL)

 {

      if((p->lchild)==NULL&& (p->rchild)==NULL) //如果是叶子结点

     {

             printf("%d",p->w);

             printf("thehops 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

 

structptree *hafm(int n, int w[m])

{

structpforest *p1,*p2,*f;

structptree *ti,*t,*t1,*t2;

inti;

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)

{

//按权值从小到大的顺序将结点从上到下地挂在一颗树上      

structpforest *p, *q, *r;

structptree *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

}

 

voidInPut(int &n,int w[m])

{

printf("pleaseinput the sum of node\n"); //提示输入结点数

scanf("%d",&n);     //输入结点数

printf("please input weight of every node\n"); //提示输入每个结点的权值

for(inti=1;i<=n;i++)

scanf("%d",&w[i]); //输入每个结点权值

}

 

intmain( )

{

structptree *head;

intn,w[m];

InPut(n,w);

head=hafm(n,w);

travel(head,0);

printf("Thelength of the best path is WPL=%d", WPL);//输出最佳路径权值之和

return1;

}

五、最优二叉树的应用

哈夫曼树的应用很广,哈夫曼编码就是哈夫曼树在电讯通信中的应用之一。它采用不等长编码,让出现次数多的字符用短码,且任一编码不能是另一编码的前缀(我们称之为前缀编码,或非前缀码)。设有n种字符,每种字符出现的次数为Wi,其编码长度为Li(i=1,2,...n),则整个电文总长度为∑WiLi ,要得到最短的电文,即使得∑WiLi最小。也就是以字符出现的次数为权值,构造一棵Huffman树,并规定左分支编码位0,右分支编码为1,则字符的编码就是从根到该字符所在的叶结点的路径上的分支编号序列。用构造Huffman树编出来的码,称为Huffman编码。

为了获得传送电文的最短长度,可将字符出现的次数(频率)作为权值赋予该结点,构造一棵WPL最小的哈夫曼树,由此得到的二进制前缀编码就是最优前缀编码,也称哈夫曼编码。可以验证,用这样的编码传送电文可使总长最短。如图所示:



     




我们在修改程序时,只要将原来的travel改成如下即可实现赫夫曼编码,当然,译码原理也相同:
#definecount 15
char ch[count][count];
int counter=0;
inttag,counter2=0;
void Initch()
{
for(int i=0;i<count;i++)
   ch[i][count-1]='\0';
}

voidtravel(struct ptree *head,int n,int tag)                     
{
//
为验证harfm算法的正确性进行的遍历
structptree *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("thehops 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



  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
夫曼是一种特殊的二叉,它的每个叶子节点都表示一个字符,并且每个字符对应的编码都是唯一的。夫曼常用于数据压缩。 夫曼的编码方式是按照前缀编码进行编码,即一个字符的编码不能是另一个字符编码的前缀。例如,如果字符 A 的编码是 10,那么字符 B 的编码不能是 1,因为这个编码是字符 A 的前缀。 夫曼的解码方式是从根节点开始遍历,根据编码的 0 或 1 分别选择左子或右子,直到到达叶子节点,找到对应的字符。 下面是一个夫曼的例子: ``` /\ / \ / \ /\ /\ / \ / \ a b c d ``` 假设字符 a 的编码为 0,字符 b 的编码为 10,字符 c 的编码为 110,字符 d 的编码为 111。那么,编码串 010110111 可以被解码成字符串 "abcbd"。 具体的解码过程如下: 1. 从根节点开始遍历,根据第一个字符 0,选择左子。 2. 继续遍历左子,根据第二个字符 1,选择右子。 3. 继续遍历右子,根据第三个字符 0,选择左子。 4. 到达叶子节点,找到字符 a。 5. 回到根节点,根据第四个字符 1,选择右子。 6. 继续遍历右子,根据第五个字符 1,选择右子。 7. 继续遍历右子,根据第六个字符 0,选择左子。 8. 到达叶子节点,找到字符 b。 9. 回到根节点,根据第七个字符 1,选择右子。 10. 继续遍历右子,根据第八个字符 1,选择右子。 11. 到达叶子节点,找到字符 d。 12. 回到根节点,根据第九个字符 1,选择右子。 13. 继续遍历右子,根据第十个字符 1,选择右子14. 到达叶子节点,找到字符 c。 15. 读取完所有的字符,解码完成。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值