哈夫曼編碼(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 }