哈夫曼編碼與譯碼
一、哈夫曼編碼定義
1.1、基本術語
路徑: 從一結點到另一結點上的分支構成這兩個結點的路徑。
路徑長度: 路徑上的分支數目。
樹的路徑長度: 從根到所有結點的路徑長度之和。
結點的帶權路徑長度: 從該結點到樹根之間的路徑長度與結點上權值的乘積。
樹的帶權路徑長度: 樹中所有葉子結點的帶權路徑長度之和。
1.2、哈夫曼樹定義: 設有n 個權值 {w1,w2,......wn},試構造具有 n 個葉結點的二叉樹,每個葉結點權值為 wi ,則其中帶權路徑長度WPL最小的二叉樹稱為哈夫曼樹(最優二叉樹)。特點:權值越大的葉子離根越近。若葉結點上的權值均相同,則完全二叉樹一定是最優二叉樹,否則完全二叉樹不一定是最優二叉樹。
WPL=2*(7+5+2+4)=36
WPL=3*(7+5)+2*4+2=46
WPL=3*(2+4)+2*5+7=35
1.3、哈夫曼樹構造:
(1) 根據給定的n個權值 {w1,w2,......wn}, 生成 n 棵二叉樹的集合F= {T1,T2,.......Tm};其中每棵二叉樹Ti只有一個帶權為Wi的根結點,左右子樹為空。
(2) 在 F 中選擇兩棵根結點值最小的樹 Ti ,Tj 作為左右子樹,構成一棵新二叉樹Tk , Tk根結點值為Ti ,Tj根結點權值之和;
(3) 在 F 中刪除Ti ,Tj ,並把 Tk 加到 F中;
(4) 重復 (2) (3),直到 F中只含一棵樹。
例:w={7,5,2,4}
1.4、哈夫曼編碼
數據的壓縮過程稱為編碼,解壓過程稱為解碼。
編碼:將文件中的字符轉換為唯一的一個二進制串。
解碼:將一個二進制串轉換為對應的字符。
定長編碼:設編碼字符個數為n,碼長為k,則k= 上界(log2^(n+1))。
不等長編碼:使出現頻率最多的字符采用盡可能短的編碼。
前綴編碼:對不等長編碼,要求任一字符的編碼都不是另一個字符的編碼的前綴。
采用二叉樹設計前綴編碼:用二叉樹的葉結點表示待編碼的字符,並約定左分支表示字符‘0’,右分支表示字符‘1’,則從根結點到葉子結點的路徑上分支字符組成的字符串作為該葉子結點的編碼。由此得到的編碼必為二進制的前綴編碼。
方法:以n種字符出現的頻率作權,設計一棵哈夫曼樹,由此得到字符的二進制前綴編碼為總長最短的二進制前綴編碼,這種編碼即為哈夫曼編碼。
二、實驗實現
2.1、實驗內容
哈夫曼編碼生成與譯碼。
輸入符號(序號用英文字母A, B, C, …表示)以及各符號出現概率,以字符串形式輸出各符號對應的二進制哈夫曼編碼。
以字符串形式輸入接收到的比特序列,輸出譯碼后的符號序列。建議用菜單形式提供功能。
2.2、輸入與輸出
輸入:編碼的個數,編碼的權重,編碼的符號,接收到的比特序列。
輸出:編碼結果和譯碼結果。
2.3.關鍵數據結構與算法描述
關鍵數據結構:霍夫曼樹的節點構造以及權重表,符號表,編碼表,實際符號數。由於采用數組的形式來存儲所有節點,則要提供左右子樹和雙親節點指針。

1 typedef char ElemType; 2 typedef struct { 3 int parent; //雙親下標
4 int lchild; //左兒子下標
5 int rchild; //右兒子下標
6 double w; //結點權重
7 }HF_BTNode; //碼樹結點類型
8 typedef struct
9 { 10 int n; //實際符號數, n<=N
11 ElemType s[N]; //符號表
12 double weight[N]; //符號權重表
13 char code[N][N+1]; //編碼表
14 HF_BTNode hf[2*N-1]; //碼樹
15 } HFT; //Huffman碼樹及碼表
算法描述:
首先是哈夫曼樹的生成需要根據相應的數據結構采用相應的算法。因為采用的是數組存儲樹的節點,屬於順序存儲結構。首先根據算法應該先找到所有根節點中最小的兩個組成一棵新樹的左右子樹,刪除這兩個節點(此處用parent為-1來說明為根節點,如不是-1,則為刪除),添加新生成的節點即讓新樹的根節點的parent為-1即可。因此根據思路可以遍歷要生成節點前的所有節點來不斷生成新樹,最后形成只有二度節點和零度節點的二叉樹。具體算法如下:

int m=2*a.n-1; //所有節點數
for(int i=0; i<m; i++)//對所有節點
{ a.hf[i].parent=a.hf[i].lchild=a.hf[i].rchild=-1;//-1代表着為根節點
} for(i=0; i<a.n; i++) { a.hf[i].w=a.weight[i]; //權重賦值
} /**********************生成霍夫曼樹***********************************/
int j1,j2,j; for(i=a.n; i<m; i++) //從原有節點之后增添數值
{ for(j1=0; j1<i; j1++) //遍歷帶增添節點之前所有節點找到根節點
{ if(a.hf[j1].parent==-1) { break; } } for(j=j1+1; j<i; j++)//找到最小的根節點作為新增節點的左子樹
{ if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j1].w) { j1=j; } }//j1為最小節點
a.hf[j1].parent=i; a.hf[i].lchild=j1; for(j2=0; j2<i; j2++) //同理找到次小節點,作為新增節點右子樹
{ if(a.hf[j2].parent==-1) { break; } } for(j=j2+1; j<i; j++) { if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j2].w) { j2=j; //j2為次小節點
} } a.hf[j2].parent=i; a.hf[i].rchild=j2; a.hf[i].w=a.hf[j1].w+a.hf[j2].w;//節點生成完成,權重賦值
}
然后,就要進行編碼了,對生成的霍夫曼樹約定左邊編1,右邊編0,只需從符號表亦即最原始節點開始追溯到根,即可的反序的編碼,然后將其翻轉即可。具體代碼如下:

/*********************生成code字符串數組*******************/
for(i=0; i<a.n; i++) { int j=i; char *p=a.code[i],*q; //j從第i個葉子出發上溯至根結點,故編碼表一定和字符表對應
while(a.hf[j].parent!=-1)//未到根節點時持續編號
{ int child=j; j=a.hf[j].parent; if(a.hf[j].lchild==child) { *p++='1';//左邊是1
} else { *p++='0';//右邊是0
} } *p='\0'; q=a.code[i]; p--; //因是從葉子到根故要字符串逆序
char ch; while(q<p) { ch=*q; *q=*p; *p=ch; q++; p--; } }
最后,就是譯碼過程了,根據二進制碼的字符串開始遍歷,根據霍夫曼樹從根出發按照1向左走,0向右走的規則,直至不能再走,則得到了想要的節點,該節點對應與符號表中一個數,故此唯一確定。如此循環往復,直至編碼被譯完。具體代碼如下:

while(receive[i]!='\0') { k=2*a.n-2; //k指向根結點,注意是從0開始
while(k>=a.n&&(receive[i]!='\0')) { if(receive[i++]=='1') { k=a.hf[k].lchild; //向左走
} else { k=a.hf[k].rchild; //向右走
} } if(k<a.n) { decoded[j++]=a.s[k]; //根據對應關系輸出一個符號
} } decoded[j]='\0';
2.4、測試與理論
理論:按理說輸入相應的編碼符號表,編碼數,編碼權重就會生成相應的霍夫曼樹,例如輸入編碼符號表為“abcde”,編碼數自然為5,編碼權重分別為0.1,0.2,0.3,0.3,0.1則應該生成相應的形如下面的霍夫曼樹:
則輸入序列”1110110001010001”對應與“baeccdc”,此處要注意權重相同時的編碼處理方法,誰先被發現,誰就是左子樹。
測試結果為:
選擇1之后:
選擇1后,再選擇2,輸入數據得到:
可見結果是正確的。
2.5、附錄(源代碼)

1 #include "stdio.h"
2 #include "stdlib.h"
3 #include "iostream"
4 using namespace std; 5 #define N 300 //最大允許符號數
6 typedef char ElemType; 7 typedef struct { 8 int parent; //雙親下標
9 int lchild; //左兒子下標
10 int rchild; //右兒子下標
11 double w; //結點權重
12 }HF_BTNode; //碼樹結點類型
13 typedef struct
14 { 15 int n; //實際符號數, n<=N
16 ElemType s[N]; //符號表
17 double weight[N]; //符號權重表
18 char code[N][N+1]; //編碼表
19 HF_BTNode hf[2*N-1]; //碼樹
20 } HFT; //Huffman碼樹及碼表
21
22 void createHF(HFT &a) 23 { 24 int m=2*a.n-1; //所有節點數
25 for(int i=0; i<m; i++)//對所有節點
26 { 27 a.hf[i].parent=a.hf[i].lchild=a.hf[i].rchild=-1;//-1代表着為根節點
28 } 29 for(i=0; i<a.n; i++) 30 { 31 a.hf[i].w=a.weight[i]; //權重賦值
32 } 33 /**********************生成霍夫曼樹***********************************/
34 int j1,j2,j; 35 for(i=a.n; i<m; i++) //從原有節點之后增添數值
36 { 37 for(j1=0; j1<i; j1++) //遍歷帶增添節點之前所有節點找到根節點
38 { 39 if(a.hf[j1].parent==-1) 40 { 41 break; 42 } 43 } 44 for(j=j1+1; j<i; j++)//找到最小的根節點作為新增節點的左子樹
45 { 46 if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j1].w) 47 { 48 j1=j; 49 } 50 }//j1為最小節點
51 a.hf[j1].parent=i; 52 a.hf[i].lchild=j1; 53 for(j2=0; j2<i; j2++) //同理找到次小節點,作為新增節點右子樹
54 { 55 if(a.hf[j2].parent==-1) 56 { 57 break; 58 } 59 } 60 for(j=j2+1; j<i; j++) 61 { 62 if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j2].w) 63 { 64 j2=j; //j2為次小節點
65 } 66 } 67 a.hf[j2].parent=i; 68 a.hf[i].rchild=j2; 69 a.hf[i].w=a.hf[j1].w+a.hf[j2].w;//節點生成完成,權重賦值
70 } 71 /***********************樹生成完畢*********************/
72 /*********************生成code字符串數組*******************/
73 for(i=0; i<a.n; i++) 74 { 75 int j=i; 76 char *p=a.code[i],*q; //j從第i個葉子出發上溯至根結點,故編碼表一定和字符表對應
77 while(a.hf[j].parent!=-1)//未到根節點時持續編號
78 { 79 int child=j; 80 j=a.hf[j].parent; 81 if(a.hf[j].lchild==child) 82 { 83 *p++='1';//左邊是1
84 } 85 else
86 { 87 *p++='0';//右邊是0
88 } 89 } 90 *p='\0'; 91 q=a.code[i]; 92 p--; //因是從葉子到根故要字符串逆序
93 char ch; 94 while(q<p) 95 { 96 ch=*q; 97 *q=*p; 98 *p=ch; 99 q++; 100 p--; 101 } 102
103 } 104 //根據對應關系輸出編碼對應表
105 cout<<"the code corresponding's string:"<<endl; 106 for(i=0;i<a.n;i++) 107 { 108 cout<<a.s[i]<<":"<<a.code[i]<<endl; 109 } 110 } 111 /**************譯碼算法********************/
112 char * dec(HFT &a, char receive[]) 113 { 114 int i,j,k; 115 i=j=0; //一定要初始化為0
116 char decoded[N]; 117
118 while(receive[i]!='\0') 119 { 120 k=2*a.n-2; //k指向根結點,注意是從0開始
121 while(k>=a.n&&(receive[i]!='\0')) 122 { 123 if(receive[i++]=='1') 124 { 125 k=a.hf[k].lchild; //向左走
126 } 127 else
128 { 129 k=a.hf[k].rchild; //向右走
130 } 131 } 132 if(k<a.n) 133 { 134 decoded[j++]=a.s[k]; //根據對應關系輸出一個符號
135 } 136 } 137 decoded[j]='\0'; 138 return decoded; 139 } 140 void MainMenu( ) 141 { 142 HFT tree; 143 int i; 144 char choice,ch; 145 int s=0; 146 char InputCode[N],decode[N]; 147 start: 148 system("cls"); 149 cout<<"-------------------welcome----------------------"<<endl; 150 cout<<" 1.編碼 "<<endl; 151 cout<<" 2.譯碼 "<<endl; 152 cout<<" 3.退出 "<<endl; 153 cout<<"-------------------end--------------------------"<<endl; 154 cin>>choice; 155 switch(choice) 156 { 157 case '1': 158 system("cls"); 159 cout<<"plese input the code num:"<<endl; 160 cin>>tree.n; 161 cout<<"please input the code weight:"<<endl; 162 for(i=0;i<tree.n;i++) 163 { 164 cin>>tree.weight[i]; 165
166 } 167 for(i=0;i<tree.n;i++) 168 { 169 s+=tree.weight[i]; 170 } 171 b: cout<<"please input the needed string:"<<endl; 172 cin>>tree.s; 173 if(strlen(tree.s)!=tree.n) 174 { 175 goto b; 176 } 177 createHF(tree); 178 cout<<"|--------------------1.返回--------------------|"<<endl; 179 cout<<"|--------------------2.退出--------------------|"<<endl; 180 cin>>ch; 181 if(ch=='1') 182 goto start; 183 else
184 exit(0); 185
186 case '2': 187 system("cls"); 188 cout<<"input the code:"<<endl; 189 cin>>InputCode; 190 strcpy(decode,dec(tree,InputCode)); 191 cout<<decode<<endl; 192 cout<<"|--------------------1.返回--------------------|"<<endl; 193 cout<<"|--------------------2.退出--------------------|"<<endl; 194 cin>>ch; 195 if(ch=='1') 196 goto start; 197 else
198 exit(0); 199 case '3': exit(0); 200 default: 201 cout<<"error"; 202 goto start; 203 } 204
205 } 206 int main() 207 { 208 MainMenu( ); 209 return 0; 210 }
關於本系列已經是第六個了,看了一下有回饋的卻很少,可能是剛剛開始寫博客吧,很多地方還是有點生疏的,再加上臨近畢業答辯,抽出一點時間學點東西也是忙里偷閑的,寫的有不好之處請各位多多擔待,畢竟營造良好的網絡學習環境和氛圍是我們新一代人應該做的事情,這個時代講究的是信息的交流和共享,在這一點我一直在努力着,希望大家能錯我的文章中受到啟發,這樣也是非常好的一件事了,獨樂樂不如眾樂樂,就是這個道理了!