【笔记】哈夫曼树

来源:互联网 发布:winform中c 编程 编辑:程序博客网 时间:2024/06/18 08:32

  • 哈夫曼树的概念
  • 哈夫曼树的构造算法
  • 哈夫曼编码
  • 哈夫曼编码算法的实现

  哈夫曼树又称最优二叉树。它是一种带权路径长度最短的树,应用非常广泛。

1.哈夫曼树的概念

  • 扩充二叉树

  对每棵二叉树进行扩充:每当在原来的二叉树中出现空子树时,就加上一个特殊的结点。显然,每个内节点都有两个儿子,而每个方结点都没有儿子,如果二叉树有n个内结点和S个外结点,则S=n+1,即外结点的个数比内结点的个数多1.

  设已按此法将第一颗二叉树加以扩充,树的外路长(用E表示)定义为从根结点到外结点的路长之和,而內路长(用I表示)定义为从根结点到每个内结点的路长之和。他们总是满足E=I+2n

  • 路径和路径长度

  路径是指在树中一个结点到另一个结点所走过的路程。路径长度是一个结点到另一个结点之间的分支数目。树的路径长度是指从树根到每个结点的路径长度的和。

  • 树的带权路径长度

  结点的带权路径长度为从该结点到树根之间的路径长度与即诶但上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和,通常记作WPL=ni=1wi×li,其中,n是树中叶子结点的个数,wi是第i个叶子结点的权值,li第i个叶子结点路径长度。加权路长的应用之一是把二叉树看成一个判断过程:从根开始做某种测试,根据测试的结果选择两个分支之一,而在分支中可以做进一步的测试等。


这里写图片描述

  注意:加权路长最小者并非一定是完全平衡的二叉树。


2.哈夫曼树的构造算法

  哈夫曼树就是带权路径长度最小的树,权值最小的结点原理根结点,权值越大的结点越靠近根结点。
  哈夫曼树的构造算法:

  1. 由给定的n个权值{w1,w2,,wn}构成n棵只有根结点的二叉树集合F=T1,T2,,Tn,其中每棵二叉树Ti中只有一个带权为wi的根结点,其左右子树均为空。
  2. 在二叉树集合F中选区两棵根结点的权值最小的和次小的的树作为左、右子树构造一棵新的二叉树,新二叉树的根结点的权重为这两棵子树根结点的权重之和。
  3. 在二叉树集合F中删除这两棵二叉树,并将新得到的二叉树加入到集合F中。
  4. 重复步骤2和3,指导结合F中只剩下一棵二叉树为止。这棵树就是最优二叉树——哈夫曼树。


这里写图片描述


3.哈夫曼编码

  在电报的传输过程中,需将传送的文字转换成二进制的字符组成的字符串。在传送电文时,希望电文的长度尽可能短。如果按照每个字符进行长度不等的编码,将出现频率高的字符采用尽可能短的编码,则电文的代码长度就会减少。用一个二进制数字串对每个字符进行编码,使任意一个字符的编码不会是任何其他字符编码的前缀。通常把编码的这种特性叫做前缀性,前缀性使两个字符编码之间不需要加分隔符。可以按下述方法对二进制数字进行译码:反复删去该串的前缀,这些前缀就是一些字符的编码。因此所设计的长度不等的编码必须满足任意一个字符的编码都不是另一个字符的前缀的要求,这样的编码称为前缀编码

  可以将前缀编码看成二叉树中的路径。每个结点的左分支附以一个0,而结点的右分支附以一个1,将字符作为叶结点的标号。从根结点到叶结点的路径上遇到0或1构成的序列就是相应的字符的编码。因此任意一种前缀编码都可以用一棵二叉树来表示。


这里写图片描述

  哈夫曼编码算法的基本思想:从给定的字符集中选择出现概率最小的两个字符a、b;用一个字符(如x)代替a和b,而x的概率对应于a和b的概率之和。然后,对新的、字符个数较少的字符集(去掉a、b而加上x)递归地求最佳前缀编码。原来的字符集中字符的编码可以这样得到:a的编码是在x编码后附以0,而b的编码是在x的编码后附以1。
  每棵树中叶结点的标号是要编码的字符,其跟记载该树所有叶结点字符所对应的概率之和,此和数称为该树的权。起初,每个字符本身是一棵树;当算法那结束时,形成唯一的一棵树,所有的字符都在它的叶结点上。从根结点到叶结点的路径上的0、1序列就表示该叶结点标号的编码。对于给定的字符集和出现的概率,哈夫曼树所表示的字符的编码的平均长度最小。

4.哈夫曼编码算法的实现

  假设一个字符序列为{a,b,c,d},对应的权重为{2,3,6,8}。构造一棵哈夫曼树,然后输出相应的哈夫曼编码。

  • 哈夫曼树的类型定义
typedef struct{    unsigned int weight;    unsigned int parent,lchild,rchild;}HTNode,*HuffmanTree; typedef char **HuffmanCode; /*存放哈夫曼编码*/

  HuffmanCode为一个二级指针,相当于二维数组,用来存放每一个叶子结点的哈夫曼编码。起初时,将每一个叶子结点的双亲结点域、左孩子域和右孩子域都初始化为0。若有n个叶子结点,则非叶子结点有n-1个,所以总共结点数目是2n-1个。同时也要将剩下的n-1个双亲结点域初始化为0,这主要是为了查找权值最小的结点方便。

  • 创建哈夫曼树并构造哈夫曼编码

  一次选择两个权值最小的结点s1和s2分别作为左子树结点和右子树结点,并为其双亲结点赋予一个地址,双亲结点的权值为s1和s2的权值之和。修改它们的parent域,使它们指向同一个双亲结点,双亲结点的左子树为权值最小的结点,右子树为权值次小的结点。重复执行这种操作n-1次,即求出n-1个非叶子结点的权值。这样就构造出了一棵哈夫曼树。

  求哈夫曼编码的方式有两种,即从根结点开始到叶子结点正向求哈夫曼编码和从叶子结点到根结点你想求哈夫曼编码,这里给出从根结点到叶子结点求哈夫曼编码的算法:
  从编号为2n-1的结点开始,即根结点开始,依次通过判断左孩子和右孩子是否存在进行编码,若左孩子存在则编码为0,若右孩子存在则编码为1;同时,利用weight域作为结点是否已经访问的标志位,若左孩子结点已经访问则将相应的weight域置为1,若右孩子结点也已经访问过则将相应的weight域置为2,若左孩子和右孩子都已经访问过则回退至双亲结点。按照这个思路,指导所有结点都已经访问过,并回退至根结点,则算法结束。

void HuffmanCoding(HuffmanTree *HT,HuffmanCode *HC,int *w,int n) /*构造哈夫曼树HT,并从根结点到叶子结点求赫夫曼编码并保存在HC中*/{     int s1,s2,i,m;     unsigned int r,cdlen;     char *cd;    HuffmanTree p;    if(n<=1)        return;    m=2*n-1;    *HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));     for(p=*HT+1,i=1;i<=n;i++,p++,w++)    {        (*p).weight=*w;        (*p).parent=0;        (*p).lchild=0;        (*p).rchild=0;    }    for(;i<=m;++i,++p)        (*p).parent=0;    /*构造哈夫曼树HT*/    for(i=n+1;i<=m;i++)     {         Select(HT,i-1,&s1,&s2);        (*HT)[s1].parent=(*HT)[s2].parent=i;        (*HT)[i].lchild=s1;        (*HT)[i].rchild=s2;        (*HT)[i].weight=(*HT)[s1].weight+(*HT)[s2].weight;    }    /*从根结点到叶子结点求赫夫曼编码并保存在HC中*/    *HC=(HuffmanCode)malloc((n+1)*sizeof(char*));    cd=(char*)malloc(n*sizeof(char));     r=m;                        /*从根结点开始*/    cdlen=0;                    /*编码长度初始化为0*/    for(i=1;i<=m;i++)        (*HT)[i].weight=0;      /*将weight域作为状态标志*/    while(r)    {        if((*HT)[r].weight==0)/*如果weight域等于零,说明左孩子结点没有遍历*/        {             (*HT)[r].weight=1;  /*修改标志*/            if((*HT)[r].lchild!=0)  /*如果存在左孩子结点,则将编码置为0*/            {                r=(*HT)[r].lchild;                cd[cdlen++]='0';            }            else if((*HT)[r].rchild==0) /*如果是叶子结点,则将当前求出的编码保存到HC中*/            {                 (*HC)[r]=(char *)malloc((cdlen+1)*sizeof(char));                cd[cdlen]='\0';                strcpy((*HC)[r],cd);            }        }        else if((*HT)[r].weight==1)     /*如果已经访问过左孩子结点,则访问右孩子结点*/        {             (*HT)[r].weight=2;      /*修改标志*/            if((*HT)[r].rchild!=0)            {                r=(*HT)[r].rchild;                cd[cdlen++]='1';            }        }        else                        /*如果左孩子结点和右孩子结点都已经访问过,则退回到双亲结点*/        {             r=(*HT)[r].parent;            --cdlen;                /*编码长度减1*/        }    }    free(cd);}
  • 查找权值最小和次小的两个结点
int Min(HuffmanTree t,int n)/*返回树中n个结点中权值最小的结点序号*/{     int i,flag;    int f=infinity;                 /*f为一个无限大的值*/    for(i=1;i<=n;i++)        if(t[i].weight<f&&t[i].parent==0)             f=t[i].weight,flag=i;    t[flag].parent=1;           /*给选中的结点的双亲结点赋值1,避免再次查找该结点*/    return flag;}void Select(HuffmanTree *t,int n,int *s1,int *s2)/*在n个结点中选择两个权值最小的结点序号,其中s1最小,s2次小*/{     int x;    *s1=Min(*t,n);    *s2=Min(*t,n);    if((*t)[*s1].weight>(*t)[*s2].weight)/*若序号s1的权值大于s2的权值,将两者交换,使s1最小,s2次小*/    {        x=*s1;        *s1=*s2;        *s2=x;    }}
  • 主函数文件
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<malloc.h>#define infinity 65535          /*定义一个无限大的值*//*哈夫曼树类型定义*/typedef struct{    unsigned int weight;    unsigned int parent,lchild,rchild;}HTNode,*HuffmanTree; typedef char **HuffmanCode; /*存放哈夫曼编码*/int Min(HuffmanTree t,int n);void Select(HuffmanTree *t,int n,int *s1,int *s2);void HuffmanCoding(HuffmanTree *HT,HuffmanCode *HC,int *w,int n);void main(){    HuffmanTree HT;    HuffmanCode HC;    int *w,n,i;    printf("请输入叶子结点的个数: ");    scanf("%d",&n);    w=(int*)malloc(n*sizeof(int));      /*为n个结点的权值分配内存空间*/    for(i=0;i<n;i++)    {        printf("请输入第%d个结点的权值:",i+1);        scanf("%d",w+i);    }    HuffmanCoding(&HT,&HC,w,n);    for(i=1;i<=n;i++)    {        printf("哈夫曼编码:");        puts(HC[i]);    }    /*释放内存空间*/    for(i=1;i<=n;i++)        free(HC[i]);    free(HC);    free(HT);}
  • 测试结果


这里写图片描述

  在算法的实现过程中,数组HT在初始时和哈夫曼树生成后的状态如下图所示。


这里写图片描述

原创粉丝点击