赫夫曼树及赫夫曼编码相关内容

来源:互联网 发布:数据库求质数的代码 编辑:程序博客网 时间:2024/05/16 15:17

赫夫曼树又叫最优二叉树,指的是对于一组具有确定权值的叶子结点的具有最小带权路径长度的二叉树。

赫夫曼树的基本概念:

1.结点的权:根据实际应用中各结点的数据的重要性,赋予各个结点一个数,数值大的表示其重要性大,这个数值称为结点的权。

2.路径:树从一个结点到另一个结点之间的分支,称为这两个结点件的路径。

3.路径长度:一个路径上的分支数量称为路径长度。

4.树的路径长度:从树的根结点到每个结点的路径长度之和,在结点数目相同的二叉树中,完全二叉树的路径长度最短。

5.结点是带权路径长度:若结点是带权的,还可定义结点的带权路径长度,就是从该结点到根结点的路径长度与该结点的权的乘积

6.树的带权路径长度:树中所有叶结点的带权路径长度之和就是树的带权路径长度(Weighted Path Length)

赫夫曼树是带权路径长度最小的二叉树,如下图,一个二叉树有4个叶节点A,B,C,D,权值分别为5,7,,2,13。通过这4个叶节点可构成多种二叉树,


树的带权路径长度就是各叶节点的带权路径长度之和,上图中各二叉树的带权路径长度分别由A,B,C,D的带权路径长度之和

1:WPL=5*2+7*2+2*2+13*2=54;

2:WPL=5*1+7*2+2*3+13*3=64;

3:WPL=13*1+7*2+2*3+5*3=48;

由上计算可得在第三种结构中二叉树的带权路径长度最小,即第三棵树是赫夫曼树。

知道了什么是赫夫曼树,构造二叉树的步骤如下:

1.根据n个有权值的结点构造n棵二叉树,每个结点为一个二叉树,其权重保存在根结点中,其左右子树均为空,以这n棵二叉树为基础进行查找。

2.在这n棵二叉树中找到两颗根结点权值最小的树,用这两棵树作为左右子树构造一颗新的二叉树,将左右子树根结点的权值相加,作为新的二叉树根结点的权值。

3.将上一步找到的两颗权值最小二叉树排除在下次查找的范围之外,将新创建二叉树添加到查找的范围之内

4.重复步骤2,3直到查找范围只剩下一棵树为止。创建过程如下:



赫夫曼树的作用----赫夫曼编码:

在通信。数据存储,数据传输时,一般都需要对处理的字符进行二进制编码,用不同的0,1表示各种字符编码,例如ASCII码就是一种编码方案,这种方案是等长编码,即每个字符使用相同的二进制位。

但在实际中需要用较少的编码保存或传输相同的信息,这就要使用不等长编码,即按照字符出现的【频率对不同的字符使用不同长度的编码,频率高的使用短编码,频率低的使用长编码,这样可以缩短整个信息的编码位数,从而减少总码长。

例如:在电报中,电文以0,1两个数字构成,假设电文中使用A,B,C,D这4个字符,若按等长编码,需要2位二进制字符,

A:00          B:01        C:10         D :11

假设在这份电文中4个字符出现的次数为:5,7,2,13,采用等长编码总长度为:5*2+7*2+2*2+13*2=54

而使用赫夫曼编码,让出现次数多的字符使用短的编码,出现次数少的字符使用长编码:

A:01          B:1            C:10       D:0

但是这会出现一个译码二义性的问题,比如当接受到的编码字符串为0101时,是将01作为A字符还是将0作为D字符??

解决译码二义性的问题,要求对字符集进行编码时,其中任一字符的编码都不能是其他字符编码的前缀,比如0字符D不能是01字符A的前缀。这和树中任意叶节点都不会是其他叶结点的前驱,从根结点到叶结点的路径上分支字符所组成的字符串可作为叶结点的编码。

例如对A,B,C,D这4个字符,取其出现次数作为权值,得到下面最优二叉树,



设置赫夫曼树中左分支代表编码0,右分支代表编码1,四个字符编码:  A:111     B:10  C:110  D:0


下面通过程序生成赫夫曼编码 “HumanTree.c”

定义赫夫曼树结点的结构

#include <stdlib.h>#include <stdio.h>#include <string.h>typedef struct{    int weight; //权值     int parent; //父结点序号     int left; //左子树序号    int right; //右子树序号 }HuffmanTree;typedef char *HuffmanCode;  //Huffman编码指针,指向赫夫曼编码字符串

接下来创建赫夫曼树的具体代码:

void CreateTree(HuffmanTree *ht,int n,int *w) //n表示创建赫夫曼树叶结点数量  w表示一个指针,用来传入n个叶结点的权值{    int i,m=2*n-1;//总的节点数    int bt1,bt2; //二叉树结点序与     if(n<=1) return ; //只有一个结点,无法创建     for(i=1;i<=n;++i) //初始化叶结点 将n个叶结点权值保存到对应的赫夫曼树结点中,    {        ht[i].weight=w[i-1];        ht[i].parent=0;        ht[i].left=0;        ht[i].right=0;    }    for(;i<=m;++i)//将赫夫曼树非叶结点清空     {        ht[i].weight=0;        ht[i].parent=0;        ht[i].left=0;        ht[i].right=0;    }    for(i=n+1;i<=m;++i) //逐个计算非叶结点,创建Huffman树     {        SelectNode(ht,i-1,&bt1,&bt2); //从1~i-1个结点选择parent结点为0,权重最小的两个结点         ht[bt1].parent=i;  //将选出的两个结点作为当前结点的左右子树,        ht[bt2].parent=i;        ht[i].left=bt1;            ht[i].right=bt2;        ht[i].weight=ht[bt1].weight+ht[bt2].weight;  //将这两个结点的权值相加保存到当前结点中    }}

然后是上面调用的SelectNode函数,从无父结点的结点中选出两个权值最小的。

void SelectNode(HuffmanTree *ht,int n,int *bt1,int *bt2)//从1~x个结点选择parent结点为0,权重最小的两个结点 {     int i;     HuffmanTree *ht1,*ht2,*t;     ht1=ht2=NULL; //初始化两个结点为空      for(i=1;i<=n;++i) //循环处理1~n个结点(包括叶结点和非叶结点)      {         if(!ht[i].parent) //父结点为空(结点的parent=0) 若不为空,不再参与查找         {             if(ht1==NULL) //结点指针1为空              {                 ht1=ht+i; //指向第i个结点  给临时变量h1赋值,使其保存有效(父结点为空)结点的序号                 continue; //继续循环              }             if(ht2==NULL) //结点指针2为空              {                 ht2=ht+i; //指向第i个结点                  if(ht1->weight>ht2->weight) //比较两个结点的权重,                 {                     t=ht2;                     ht2=ht1;      //使ht2指向的结点权值为次小结点                     ht1=t;        //使ht1指向的结点权值为最小值                 }                 continue; //继续循环              }             if(ht1 && ht2) //若ht1、ht2两个指针都有效 将数组各有效结点权值分别与ht1 ht2指向的结点的权值进行比较,最后ht1 ht2指向的结点权值最小             {                 if(ht[i].weight<=ht1->weight) //第i个结点权重小于ht1指向的结点                  {                     ht2=ht1; //ht2保存ht1,因为这时ht1指向的结点成为第2小的                      ht1=ht+i; //ht1指向第i个结点                  }else if(ht[i].weight<ht2->weight){ //若第i个结点权重小于ht2指向的结点                      ht2=ht+i; //ht2指向第i个结点                  }             }         }     }     if(ht1>ht2){ //增加比较,使权值最小的两个结点按照数组最初的排序出现,叶结点在数组的前面,其序号相对较小         *bt2=ht1-ht;         *bt1=ht2-ht;     }else{         *bt1=ht1-ht;         *bt2=ht2-ht;     }}

生成赫夫曼树后,根据赫夫曼树生成每个字符的赫夫曼编码

void HuffmanCoding(HuffmanTree *ht,int n,HuffmanCode *hc)  //n表示需要生成的赫夫曼编码的字符数量,hc是一个用来返回生成赫夫曼编码字符串首地址的指针{     char *cd;     int start,i;     int current,parent;         cd=(char*)malloc(sizeof(char)*n);//申请能保存n个字符的内存空间,用来保存一个字符的编码字符串     cd[n-1]='\0'; //设置内存区域最后一个字符为字符串的结束字符      for(i=1;i<=n;i++)     {         start=n-1;         current=i;         parent=ht[current].parent;//获取当前结点的父结点          while(parent) //父结点不为空          {             if(current==ht[parent].left)//若该结点是父结点的左子树                 cd[--start]='0'; //编码为0              else //若结点是父结点的右子树                cd[--start]='1'; //编码为1              current=parent; //设置当前结点指向父结点              parent=ht[parent].parent; //获取当前结点的父结点序号             }         hc[i-1]=(char*)malloc(sizeof(char)*(n-start));//分配保存编码的内存          strcpy(hc[i-1],&cd[start]);               }     free(cd); //释放编码占用的内存 }
从叶结点(i)向根结点(n)查找,当前结点是父结点的左子树,生成编码0,是父结点右子树,生成编码1。

在向指针cd所指向的内存地址填入编码时,因为是从叶结点向根结点查找,是逆序,因此字符串编码时也是逆序,即从字符串的右侧向左侧填写
根据生成的编码长度(n-start)重新申请内存,用来保存该字符的编码,当指针保存在hc数组中,在函数结束后,该数组返回给调用程序

使用字符串复制函数strcpy将cd指向内存的字符复制到hc数组中对应的序号

上面函数可以生成字符集中各字符的赫夫曼编码,然后根据该赫夫曼编码将字符串进行编码,得到编码后的字符串:

void Encode(HuffmanCode *hc,char *alphabet,char *str,char *code)//将一个字符串转换为Huffman编码//hc为Huffman编码表 ,alphabet为对应的字母表,str为需要转换的字符串,code返回转换的结果 {          int len=0,i=0,j;     code[0]='\0';     while(str[i])     //对每个字符进行循环处理     {         j=0;         while(alphabet[j]!=str[i])     //在字符集字符串alphabet中查找,找到需要转换的那个字符             j++;                  strcpy(code+len,hc[j]); //将赫夫曼编码表中对应字母的Huffman编码复制到code指定位置          len=len+strlen(hc[j]); //累加字符串长度          i++;     }     code[len]='\0';}

上面通过查表将字符串编码为赫夫曼码,这样就可以作为电文发送出去,然后电文接收方还需把这些不等长的二进制编码还原为明文字符,需要从赫夫曼树根部开始逐个编码字符进行查找,当从根结点开始查找找到某个叶节点,则表示对这部分二进制编码进行了解压,还原为一个明文字符,这样从编码字符串中逐个获取二进制编码再在赫夫曼树中进行查找,即完成解码工作。

void Decode(HuffmanTree *ht,int m,char *code,char *alphabet,char *decode)//将一个Huffman编码组成的字符串转换为明文字符串 //ht为Huffman二叉树,m为字符数量,alphabet为对应的字母表,str为需要转换的字符串,decode返回转换的结果 {     int position=0,i,j=0;     m=2*m-1;     while(code[position]) //逐个处理赫夫曼曼生成的字符串     {          for(i=m;ht[i].left && ht[i].right; position++) //循环完成一个字符的解码过程          {              if(code[position]=='0') //编码位为0                   i=ht[i].left; //处理左子树               else //编码为 1                   i=ht[i].right; //处理右子树           }          decode[j]=alphabet[i-1]; //得到一个字母          j++;//处理下一字符      }       decode[j]='\0'; //字符串结尾 }

本例创建的赫夫曼树是按照数组形式存放的,数组中最后一个元素就是赫夫曼树的根结点,从最后一个元素(根结点)开始在树中查找,判断编码字符为0,则表示下一个要查找的是根结点的左子树,否则下一个是要查找的是根结点的右子树,这样在赫夫曼树中逐编码字符的查找,一直找到叶节点为止,然后根据找到的叶结点序号,在字符集中找到对应的字母,将其保存到解码字符串,接着处理下一个字符,重复直到编码表code中编码字符全部处理完,在decode字符串保存解码后的字符。


下面测试以上赫夫曼树创建,赫夫曼编码生成,岁字符串编码解码操作函数:

int main(){    int i,n=4,m;     char test[]="DBDBDABDCDADBDADBDADACDBDBD"; //测试字符串    char code[100],code1[100];                   /保存编码和解码的字符串    char alphabet[]={'A','B','C','D'}; //4个字符    int w[]={5,7,2,13} ;//4个字符的权重     HuffmanTree *ht;    HuffmanCode *hc;        m=2*n-1;        ht=(HuffmanTree *)malloc((m+1)*sizeof(HuffmanTree)); //申请内存,保存赫夫曼树     if(!ht)    {        printf("内存分配失败!\n");        exit(0);        }    hc=(HuffmanCode *)malloc(n*sizeof(char*));    if(!hc)    {        printf("内存分配失败!\n");        exit(0);        }        CreateTree(ht,n,w); //创建赫夫曼树     HuffmanCoding(ht,n,hc); //根据赫夫曼树生成赫夫曼编码     for(i=1;i<=n;i++) //循环输出赫夫曼编码         printf("字母:%c,权重:%d,编码为 %s\n",alphabet[i-1],ht[i].weight,hc[i-1]);        Encode(hc,alphabet,test,code); //根据赫夫曼编码生成编码字符串     printf("\n字符串:\n%s\n转换后为:\n%s\n",test,code);         Decode(ht,n,code,alphabet,code1); //根据编码字符串生成解码后的字符串     printf("\n编码:\n%s\n转换后为:\n%s\n",code,code1);     getch();    return 0;}

下面是测试结果:






0 0
原创粉丝点击