基礎知識
目前,進行快速的遠距離通信的主要手段是電報,即將需傳送的信息轉換成由二進制字符組成的字符串。例如,需傳送的信息爲“ABACCDA”,它只有四個字符,只需要兩個字符的串便可以進行編碼。假設A、B、C、D的編碼分別爲00、01、10、11,則上述信息所轉換成的電文爲“00010010101100”,總長爲14位,對方接收到時,可按照二位一分進行譯碼。
當然,在傳送電文時,總希望電文的總長度儘可能的短。如果對每個字符設計長度不等的編碼,且讓電文中出現次數較多的字符采用儘可能短的編碼,則傳送電文的總長度便可減少。如果設計A、B、C、D的編碼分別爲0、00、1和01,則上述信息的電文可轉換長度爲9的字符串“000011010”。但是這樣的電文無法翻譯,例如傳送過去的前4個字符串的子串“0000”就有多種翻譯方法,可以譯爲“AAAA”、“BB”、“AAB”等等。因此,若要設計長短不一的編碼,則必須滿足任何一個字符的編碼都不是另一個字符的前綴,這種編碼稱爲前綴編碼。
可以利用二叉樹來設計二進制的前綴編碼。例如有圖(1)所示一棵二叉樹,它的4個葉子節點分別表示A、B、C、D這四個字符,並且約定左分支表示字符‘0’,右分支表示字符‘1’,則可以將從根節點到葉子節點的路徑上分支字符組成字符串作爲該葉子節點字符的編碼。由圖(1)可以得到A、B、C、D的二進制前綴編碼爲0、10、110和111。
圖(1)
那麼,如何得到使電文總長最短的二進制前綴編碼呢?假設每種字符在電文中出現的次數爲ωi,其編碼長度爲ιi,電文中只有n種字符,則電文總長爲Σωi*ιi(其中i=1…n)。對應到二叉樹上,若置ωi爲葉子節點的權值,ιi恰爲從根節點到葉子節點的長度,則Σωi*ιi(其中i=1…n)恰爲二叉樹上帶權路徑長度。由此可知,設計電文總長最短的二進制前綴編碼即轉換爲以n種字符出現的頻率作爲權值設計一棵Huffman樹的問題,由此得到的二進制前綴編碼稱爲Huffman編碼。
Huffman樹的構造算法稱爲Huffman算法,敘述如下:
(1)根據給定的n個權值{ω1,ω2,…,ωn}構成n棵二叉樹的集合F={T1,T2,…,Tn},其中每棵二叉樹Ti中只有一 個帶權爲ωi的根節點,其左右子樹均爲空。
(2)在F中選取兩棵根節點的權值最小的樹作爲左、右子樹構造一棵新的二叉樹,且置新的二叉樹的根節點的權值爲其左、右子樹上根節點的權值之和。
(3)在F中刪除這兩棵樹,同時將新得到的二叉樹加入到F中。
(4)重複(2)和(3),直到F只含一棵樹爲止。這棵樹便是Huffman樹。
Huffman樹的構造過程的一個實例如圖(2)所示,假設有a、b、c、d四個字符,權值分別爲7、5、2和4:
圖(2)
代碼實現
由於Huffman樹中沒有度爲1的節點(這類樹又稱爲嚴格的(strict)(或正則的)二叉樹),則一棵含有N個葉子節點的Huffman樹共有2N-1個節點。可以存儲在一個大小爲2N-1的一維數組中。
接下來以表(1)所示的字符與頻率爲例,演示用C語言實現Huffman樹的構造、編碼及譯碼過程:
表(1)
1、建立Huffman樹
首先建立Huffman樹,存儲在一維數組tree[M](其中M=2N-1,N=8)中,tree[M]中的每一項存儲Huffman樹的一個節點信息:字符(ch)、權值(weight)、左、右孩子在tree數組中的下標(Lchild、Rchild)、父節點在tree數組中的下標(parent)。最終構建的Huffman樹如圖(3)所示:
圖(3)
tree[M]數組保存了這棵Huffman樹的信息,其內容如表(2)所示:(表中的-1表示左右孩子節點爲空(即葉子節點)或是父節點爲空,父節點爲空的節點爲根節點。)
表(2)
2、獲取各個字符的Huffman編碼
接下來根據建立的Huffman樹來獲取每個字符的編碼,保存到code[N](N爲需編碼的字符個數,此例中N=8)數組中,code[N]中的每一項存儲一個字符的編碼信息:字符(ch)、長度爲N的位串(bits[N])、字符編碼在位串中的起始位置(start,位串下標爲start—N的內容爲該字符的編碼)。
依次從每個葉子節點出發,向上回溯直到根節點,從後往前填寫bits[N],獲得每個字符的編碼,得到的Code[N]內容如表(3)所示:
表(3)
3、編碼
例如對字符串A=“bbfehagd”進行編碼,僅需根據code數組將A中的每個字符轉換成其對應的編碼即可。則A轉換成的電文爲:101001110001000100001111。
4、譯碼
譯碼過程需要從建立的Huffman樹的根節點出發(即tree[M]數組的第M-1項),從左往右掃描待翻譯的“01”電文,若遇‘0’,則走向左孩子;若遇‘1’,則走向右孩子,直到走到葉子節點,該葉子節點所表示的字符即爲這一段“01”子串所表示的字符;繼而重新從根節點出發,翻譯下一段“01”串,直到將待翻譯的“01”串掃描完。若字符串讀完,還未到葉子節點,則輸入電文有錯。
例如對上文中編碼生成的電文B="101001110001000100001111"的譯碼過程如圖(4)所示:
(a) (b)
(c) (d)
(e) (f)
(g) (h)
圖(4)
代碼清單
#include<stdio.h>
#include<stdlib.h>
#define N 8 //葉子總數,即需編碼字符的個數
#define M 2*N-1 //節點總數
#define maxval 10000.0 //最大權值
#define maxsize 1000 //數組大小的最大值
typedef struct HuffmanTree//用於存儲Huffman樹中的節點信息
{
char ch; //字符
float weight; //權值
int Lchild; //左孩子
int Rchild; //右孩子
int parent; //父節點
}HuffmanTree;
typedef struct CodeType//用於存儲單個字符的編碼結果
{
char ch; //字符
char bits[N]; //位串
int start; //編碼在位串中的起始位置
}CodeType;
void CreateHuffmanTree(HuffmanTree tree[]); //創建Huffman樹
void HuffmanCode(CodeType code[],HuffmanTree tree[]);//根據Huffman樹求出Huffman編碼存儲在code數組中
void incode(CodeType code[],char *A,char *B); //將字符串A編碼,變成“01”串保存在數組B中
void decode(HuffmanTree tree[],char *str); //將“01”字符串str進行譯碼,直接輸出
int main()
{
HuffmanTree tree[M]; //tree存儲Huffman樹
CodeType code[N]; //code存儲單個字符的編碼結果
char A[maxsize]; //待編碼的字符串
char B[maxsize]; //字符串編碼後生成的"01"串
CreateHuffmanTree(tree); //創建Huffman樹
HuffmanCode(code,tree); //獲得單個字符的編碼結果
printf("請輸入要編碼的字符串:");
gets(A);
incode(code,A,B);//根據單個字符的編碼將字符串A編碼成字符串B
printf("編碼結果爲:");
puts(B);
decode(tree,B);//根據Huffman樹對編碼結果進行翻譯,直接輸出
return 0;
}
void CreateHuffmanTree(HuffmanTree tree[])
{
int i,j;
int p1,p2; //p1,p2記錄最小權值及次小權值節點在數組中的下標
float min1,min2; //min1記錄最小權值,min2記錄次小權值
for(i=0;i<M;i++) //初始化Huffman樹的M個節點
{
tree[i].weight=0.0;
tree[i].Lchild=-1;
tree[i].Rchild=-1;
tree[i].parent=-1;
}
//輸入Huffman樹前N個節點的信息,即待編碼的字符及其權值
printf("請依次輸入%d個字符及權值\n",N);
for(i=0;i<N;i++)
{
char c;
float w;
printf("請輸入第%d個字符及其對應的權值:",i+1);
scanf("%c %f",&c,&w);
getchar(); //吃掉回車符
tree[i].ch=c;
tree[i].weight=w;
}
//進行N-1次合併,生成N-1個新節點
//每次找到權值最小的兩個單個節點(即無父節點的節點)+,合併形成新節點,更改這兩個節點的父節點信息、新節點的權值及左右孩子節點信息
for(i=N;i<M;i++)
{
p1=p2=0; //最小權值節點及次小權值節點對應下標初始化爲0
min1=min2=maxval; //最小權值及次小權值初始化爲權值最大值
for(j=0;j<i;j++) //依次檢測Huffman樹的前i個節點
{
if(tree[j].parent==-1)//若該節點無父節點
{
if(tree[j].weight<min1)//若該節點的權值小於最小權值,
{ //將最小權值賦給次小權值,該節點的權值賦給最小權值作爲最小權值
min2=min1; //並更改對應p1,p2的值,使之指向對應節點的下標
min1=tree[j].weight;
p2=p1;
p1=j;
}
else
{
if(tree[j].weight<min2)//若該節點的權值大於最小權值,小於次小權值,
{ //將該節點的權值賦給次小權值,該節點的下標賦給p2
min2=tree[j].weight;
p2=j;
}
}
}
}
tree[p1].parent=i; //更改權值最小兩個節點的父節點信息
tree[p2].parent=i;
tree[i].Lchild=p1;//更改父節點左右孩子信息及權值
tree[i].Rchild=p2;
tree[i].weight=tree[p1].weight+tree[p2].weight;
}
}
void HuffmanCode(CodeType code[],HuffmanTree tree[])//根據Huffman樹求出Huffman編碼存儲在code數組中
{
int i,c,p;
CodeType cd;//緩衝變量
for(i=0;i<N;i++)//依次檢測前N個節點,前N個節點爲葉子節點,即從Huffman從下往上獲得單個字符的編碼
{
cd.start=N;
cd.ch=tree[i].ch;
c=i; //c爲當前節點
p=tree[i].parent; //p爲當前
while(p!=-1)
{
cd.start--;
if(tree[p].Lchild==c)
cd.bits[cd.start]='0';//tree[i]是左子樹,生成代碼'0'
else
cd.bits[cd.start]='1';//tree[i]是右子樹,生成代碼'1'
c=p;
p=tree[p].parent;
}
code[i]=cd;//第i+1個字符的編碼存入code[i]
}
}
void incode(CodeType code[],char *A,char *B)//編碼
{
int i,k=0;
for(i=0;A[i]!='\0';i++)
{
int j=0,p;
while(code[j].ch!=A[i])
j++;
for(p=code[j].start;p<N;p++)
B[k++]=code[j].bits[p];
}
B[k]='\0';//注意!
}
void decode(HuffmanTree tree[],char *str)//譯碼
{
int j=0,i=M-1;//tree[M-1]爲根節點,從根節點開始譯碼
printf("譯碼結果爲:");
while(str[j]!='\0')
{
if(str[j]=='0')
i=tree[i].Lchild;//走向左孩子
else
i=tree[i].Rchild;//走向右孩子
if(tree[i].Lchild==-1)//tree[i]是葉子節點
{
printf("%c",tree[i].ch);
i=M-1;//回到根節點
}
j++;
}
printf("\n");
if(tree[i].Lchild!=-1&&str[j]!='\0')//字符串讀完,但未到葉子節點,則輸入電文有錯
printf("ERROR!");
}
運行結果:
謝謝觀賞~~