哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。

发展历史

1951年,哈夫曼和他在MIT信息论的同学需要选择是完成学期报告还是期末考试。导师Robert M. Fano给他们的学期报告的题目是,寻找最有效的二进制编码。由于无法证明哪个已有编码是最有效的,哈夫曼放弃对已有编码的研究,转向新的探索,最终发现了基于有序频率二叉树编码的想法,并很快证明了这个方法是最有效的。由于这个算法,学生终于青出于蓝,超过了他那曾经和信息论创立者香农共同研究过类似编码的导师。哈夫曼使用自底向上的方法构建二叉树,避免了次优算法Shannon-Fano编码的最大弊端──自顶向下构建树。
1952年,David A. Huffman在麻省理工攻读博士时发表了《一种构建极小多余编码的方法》(A Method for the Construction of Minimum-Redundancy Codes)一文,它一般就叫做Huffman编码。
Huffman在1952年根据香农(Shannon)在1948年和范若(Fano)在1949年阐述的这种编码思想提出了一种不定长编码的方法,也称霍夫曼(Huffman)编码。霍夫曼编码的基本方法是先对图像数据扫描一遍,计算出各种像素出现的概率,按概率的大小指定不同长度的唯一码字,由此得到一张该图像的霍夫曼码表。编码后的图像数据记录的是每个像素的码字,而码字与实际像素值的对应关系记录在码表中。
赫夫曼编码是可变字长编码(VLC)的一种。 Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长 度最短的码字,有时称之为最佳编码,一般就称Huffman编码。下面引证一个定理,该定理保证了按字符出现概率分配码长,可使平均码长最短。
原理
设某信源产生有五种符号u1、u2、u3、u4和u5,对应概率P1=0.4,P2=0.1,P3=P4=0.2,P5=0.1。首先,将符号按照概率由大到小排队,如图所示。编码时,从最小概率的两个符号开始,可选其中一个支路为0,另一支路为1。这里,我们选上支路为0,下支路为1。再将已编码的两支路的概率合并,并重新排队。多次重复使用上述方法直至合并概率归一时为止。从图(a)和(b)可以看出,两者虽平均码长相等,但同一符号可以有不同的码长,即编码方法并不唯一,其原因是两支路概率合并后重新排队时,可能出现几个支路概率相等,造成排队方法不唯一。一般,若将新合并后的支路排到等概率的最上支路,将有利于缩短码长方差,且编出的码更接近于等长码。这里图(a)的编码比(b)好。
赫夫曼码的码字(各符号的代码)是异前置码字,即任一码字不会是另一码字的前面部分,这使各码字可以连在一起传送,中间不需另加隔离符号,只要传送时不出错,收端仍可分离各个码字,不致混淆。
实际应用中,除采用定时清洗以消除误差扩散和采用缓冲存储以解决速率匹配以外,主要问题是解决小符号集合的统计匹配,例如黑(1)、白(0)传真信源的统计匹配,采用0和1不同长度游程组成扩大的符号集合信源。游程,指相同码元的长度(如二进码中连续的一串0或一串1的长度或个数)。按照CCITT标准,需要统计2×1728种游程(长度),这样,实现时的存储量太大。事实上长游程的概率很小,故CCITT还规定:若l表示游程长度,则l=64q+r。其中q称主码,r为基码。编码时,不小于64的游程长度由主码和基码组成。而当l为64的整数倍时,只用主码的代码,已不存在基码的代码。
长游程的主码和基码均用赫夫曼规则进行编码,这称为修正赫夫曼码,其结果有表可查。该方法已广泛应用于文件传真机中。
定理
在变字长编码中,如果码字长度严格按照对应符号出现的概率大小逆序排列,则其平 均码字长度为最小。
现在通过一个实例来说明上述定理的实现过程。设将信源符号按出现的概率大小顺序排列为 :
U: ( a1 a2 a3 a4 a5 a6 a7 )
0.20 0.19 0.18 0.17 0.15 0.10 0.01
给概率最小的两个符号a6与a7分别指定为“1”与“0”,然后将它们的概率相加再与原来的 a1~a5组合并重新排序成新的原为:
U′: ( a1 a2 a3 a4 a5 a6′ )
0.20 0.19 0.18 0.17 0.15 0.11
对a5与a′6分别指定“1”与“0”后,再作概率相加并重新按概率排序得
U″:(0.26 0.20 0.19 0.18 0.17)…
直到最后得 U″″:(0.61 0.39)
赫夫曼编码的具体方法:先按出现的概率大小排队,把两个最小的概率相加,作为新的概率 和剩余的概率重新排队,再把最小的两个概率相加,再重新排队,直到最后变成1。每次相 加时都将“0”和“1”赋与相加的两个概率,读出时由该符号开始一直走到最后的“1”, 将路线上所遇到的“0”和“1”按最低位到最高位的顺序排好,就是该符号的赫夫曼编码。
例如a7从左至右,由U至U″″,其码字为1000;
a6按路线将所遇到的“0”和“1”按最低位到最高位的顺序排好,其码字为1001…
用赫夫曼编码所得的平均比特率为:Σ码长×出现概率
上例为:0.2×2+0.19×2+0.18×3+0.17×3+0.15×3+0.1×4+0.01×4=2.72 bit
可以算出本例的信源熵为2.61bit,二者已经是很接近了。
应用例举
哈夫曼树─即最优二叉树,带权路径长度最小的二叉树,经常应用于数据压缩。 在计算机信息处理中,“哈夫曼编码”是一种一致性编码法(又称“熵编码法”),用于数据的无损耗压缩。这一术语是指使用一张特殊的编码表将源字符(例如某文件中的一个符号)进行编码。这张编码表的特殊之处在于,它是根据每一个源字符出现的估算概率而建立起来的(出现概率高的字符使用较短的编码,反之出现概率低的则使用较长的编码,这便使编码之后的字符串的平均期望长度降低,从而达到无损压缩数据的目的)。这种方法是由David.A.Huffman发展起来的。 例如,在英文中,e的出现概率很高,而z的出现概率则最低。当利用哈夫曼编码对一篇英文进行压缩时,e极有可能用一个位(bit)来表示,而z则可能花去25个位(不是26)。用普通的表示方法时,每个英文字母均占用一个字节(byte),即8个位。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。若能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。
编码实现

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <math.h>
  5 #define M 100
  6 typedef struct Fano_Node
  7 {
  8 char ch;
  9 float weight;
 10 }FanoNode[M];
 11 typedef struct node
 12 {
 13 int start;
 14 int end;
 15 struct node *next;
 16 }LinkQueueNode;
 17 typedef struct
 18 {
 19 LinkQueueNode *front;
 20 LinkQueueNode *rear;
 21 }LinkQueue;
 22 //建立队列
 23 void EnterQueue(LinkQueue *q,int s,int e)
 24 {
 25 LinkQueueNode *NewNode;
 26 //生成新节点
 27 NewNode=(LinkQueueNode*)malloc(sizeof( LinkQueueNode ));
 28 if(NewNode!=NULL)
 29 {
 30 NewNode->start=s;
 31 NewNode->end=e;
 32 NewNode->next=NULL;
 33 q->rear->next=NewNode;
 34 q->rear=NewNode;
 35 }
 36 else
 37 {
 38 printf("Error!");
 39 exit(-1);
 40 }
 41 }
 42 //按权分组
 43 void Divide(FanoNode f,int s,int *m,int e)
 44 {
 45 int i;
 46 float sum,sum1;
 47 sum=0;
 48 for(i=s;i<=e;i++)
 49 sum+=f[i].weight;//
 50 *m=s;
 51 sum1=0;
 52 for(i=s;i<e;i++)
 53 {
 54 sum1+=f[i].weight;
 55 *m=fabs(sum-2*sum1)>fabs(sum-2*sum1-2*f[i+1].weight)?(i+1):*m;
 56 if(*m==i) break;
 57 }
 58 }
 59 void main()
 60 {
 61 int i,j,n,max,m,h[M];
 62 int sta,end;
 63 float w;
 64 char c,fc[M][M];
 65 FanoNode FN;
 66 LinkQueueNode *p;
 67 LinkQueue *Q;
 68 //初始化队Q
 69 Q=(LinkQueue *)malloc(sizeof(LinkQueue));
 70 Q->front=(LinkQueueNode*)malloc(sizeof(LinkQueueNode));
 71 Q->rear=Q->front;
 72 Q->front->next=NULL;
 73 printf("\t***FanoCoding***\n");
 74 printf("Please input the number of node:");
 75 //输入信息
 76 scanf("%d",&n);
 77 //超过定义M,退出
 78 if(n>=M)
 79 {
 80 printf(">=%d",M);
 81 exit(-1);
 82 }
 83 i=1; //从第二个元素开始录入
 84 while(i<=n)
 85 {
 86 printf("%d weight and node:",i);
 87 scanf("%f %c",&FN[i].weight,&FN[i].ch);
 88 for(j=1;j<i;j++)
 89 {
 90 if(FN[i].ch==FN[j].ch)//查找重复
 91 {
 92 printf("Same node!!!\n"); break;
 93 }
 94 }
 95 if(i==j)
 96 i++;
 97 }
 98 //排序(降序)
 99 for(i=1;i<=n;i++)
100 {
101 max=i+1;
102 for(j=max;j<=n;j++)
103 max=FN[max].weight<FN[j].weight?j:max;
104 if(FN[i].weight<FN[max].weight)
105 {
106 w=FN[i].weight;
107 FN[i].weight=FN[max].weight;
108 FN[max].weight=w;
109 c=FN[i].ch;
110 FN[i].ch=FN[max].ch;
111 FN[max].ch=c;
112 }
113 }
114 for(i=1;i<=n;i++) //初始化h
115 h[i]=0;
116 EnterQueue(Q,1,n); //1和n进队
117 while(Q->front->next!=NULL)
118 {
119 p=Q->front->next; //出队
120 Q->front->next=p->next;
121 if(p==Q->rear)
122 Q->rear=Q->front;
123 sta=p->start;
124 end=p->end;
125 free(p);
126 Divide(FN,sta,&m,end); /*按权分组*/
127 for(i=sta;i<=m;i++)
128 {
129 fc[i][h[i]]='0';
130 ++h[i];
131 }
132 if(sta!=m)
133 EnterQueue(Q,sta,m);
134 else
135 fc[sta][h[sta]]='\0';
136 for(i=m+1;i<=end;i++)
137 {
138 fc[i][h[i]]='1';
139 ++h[i];
140 }
141 if(m==sta&&(m+1)==end)
142 //如果分组后首元素的下标与中间元素的相等,
143 //并且和最后元素的下标相差为1,则编码码字字符串结束
144 {
145 fc[m][h[m]]='\0';
146 fc[end][h[end]]='\0';
147 }
148 else
149 EnterQueue(Q,m+1,end);
150 }
151 for(i=1;i<=n;i++) /*打印编码信息*/
152 {
153 printf("%c:",FN[i].ch);
154 printf("%s\n",fc[i]);
155 }
156 system("pause");
157 }

版权声明:本文为pxy071128fzm原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/pxy071128fzm/p/13796198.html