HuffmanTree哈夫曼樹(赫夫曼樹)及哈夫曼編碼
今天帶領大家學一下哈夫曼
一. 概念:
赫夫曼樹又叫做最優二叉樹,它的特點是帶權路徑最短。
1)路徑:路徑是指從樹中一個結點到另一個結點的分支所構成的路線,
2)路徑長度:路徑長度是指路徑上的分支數目。
3)樹的路徑長度:樹的路徑長度是指從根到每個結點的路徑長度之和。
4)帶權路徑長度:結點具有權值,從該節點到根之間的路徑長度乘以結點的權值,就是該結點的帶權路徑長度。
5)樹的帶權路徑長度(WPL):樹的帶權路徑長度是指樹中所有葉子結點的帶權路徑長度之和。
舉個例子 a b c d 四個結點,權值分別為 7,5,2,4
如圖所示二叉樹的4個葉子結點。a根結點的分支數目為2,a的路徑長度為2,a 的帶權路徑長度為 7*2 = 14
同樣 b 的帶權路徑長度為 5 * 2 = 10。c , d 的分別是 3 * 2 = 6, 4 * 2 = 8。
這棵樹的帶權路徑長度為 WPL = 8 + 6 + 10 + 14.
二. 赫夫曼樹的構造方法
還是上面的例子把
a b c d 四個結點,權值分別為 7,5,2,4
(1)我們選出權值最小的兩個根c和d,作為左子樹和右子數 構成一棵二叉樹,新二叉樹的根結點權值為c和d權值之和。刪出c和d。同時將新構造的二叉樹加入集合點。如下圖
繼續從 選擇權值最小的兩個根,即權值為 5和6 的兩個結點,然後以它們作為左右子樹構造一個新的二叉樹。新的二叉樹的權值為 5 + 6 = 11,刪除 權值為 5和6 的兩棵樹,將新構造權值為11的樹加如集合。
繼續從選擇權值最小的 直到所有集合中只剩下一棵二叉樹。
此時集合中只剩下這一刻二叉樹,這棵樹就是赫夫曼樹,至此赫夫曼樹的構造結束,計算值為WPL = 7*1 + 5*2 + 2*3 + 4*3 = 35
在以a b c d這4個結點為葉子結點的所有二叉樹中,赫夫曼樹的WPL最小
赫夫曼樹的特點
1)權值越大的結點,距離根節點越近。
2)樹種沒有度為 1 的結點,這類樹有叫做 正則(嚴格)二叉樹。
3)樹的帶權路徑長度最短
4)赫夫曼樹不唯一
三. 赫夫曼編碼
前面關於赫夫曼樹的講解中 多次提到最,最優二叉 最短帶權路徑長度。所以說我們的赫夫曼樹一定是有很大的作用的。比如在儲存檔案的時候,對於包含同一內容的檔案有多種儲存方式,是不是可以找出一種最節省空間的儲存方式呢?答案是肯定的,不然我就不會問。這就是赫夫曼編碼的用途,常見的zip壓縮檔案和.jpeg圖片檔案的底層技術都用到了赫夫曼編碼。
現在給大家舉一個簡單的例子來說明赫夫曼編碼如何實現對檔案進行壓縮儲存的,如這樣一串字元“S = AAABBACCCDEEA” 選用3位長度的二進位制為各個字元編碼(二進位制數位隨意,只要夠編碼所有不同的字元即可)
這是我們使用常規編碼後,每個字元的編碼號
根據該表我們可以把S串編碼為:
T(S)= 000000000001001000010010010011100100000 該串長度為 39,那有沒有辦法使這個編碼串的長度變小呢?並且能準確解碼得到原來的字串?下面我們用赫夫曼樹試一試。
First step: 我們先統計各個字元出現的次數。
A 5次 ,B 2次 , C 3次,D 1次,E 2次
Second: 我們以字元為結點,以出現的次數為權值,構造赫夫曼樹。
對應赫夫曼樹的每個結點的左右分支進行編號,從根結點開始,向左為 0 向右為 1 。之後從根到每個結點路徑上的數字序列即為每個字元的編碼,
A~E的赫夫曼編碼為 | ||||
A | B | C | D | E |
0 | 110 | 10 | 1110 | 1111 |
怎S串的編碼為H(S) = 00011011001010101110111111110
上述由赫夫曼樹匯出每個字元的編碼,進而得到對整個字串的編碼過程稱為赫夫曼編碼。H(S)的長度為29,必T(S)短了很多,前面每三位代表一個字元,解碼時每三位解一次就可以得出原始碼,而現在赫夫曼解碼確是不定長的,不同的字元可能出現不同的編碼串,這樣如果出現一個字元的編碼串是另一個字串編碼的字首,豈不是會出現解碼的歧義?例如 A的編碼是0 B的是00 那對於 00這個編碼是 AA呢還是B呢?這個問題可以用字首碼來解決,即任何一個字元的編碼都不是另一個字元編碼的字首。而赫夫曼編碼產生的就是字首碼 ,因為被編碼的字元都處與葉子結點上,而根通往任何一葉子結點的路徑都不可能是通往其餘葉子結點路徑的子路徑。因此任何一編碼串的子串不可能是其他編碼串的子串
其實 大家隨便畫一棵二叉數也能構造出字首碼,為什麼非要用赫夫曼樹呢?
由赫夫曼樹的特性可知,其樹的帶權路徑的長度最短。赫夫曼編碼過程中,每個字元的權值是在字串中出現的次數,出現的次數越多,距離根節點就越近,每個字元的編碼長度就是這個字元的路徑長度。因此得到的整個字串的長度一定是最短的,結論赫夫曼編碼產生的就是最短字首碼
四. 赫夫曼n叉樹
赫夫曼可不止二叉樹哦,
赫夫曼二叉樹只是赫夫曼樹的一種。
我們怎麼去構造一個赫夫曼n叉樹呢? 來舉個栗子,
序列A B C D 權值分別為 1 3 4 6 怎麼構造一個三叉樹呢? 這時我們發現無法直接構造,我們需要補上權值為0的結點讓整個序列可以湊成構造赫夫曼3插樹的序列,E 的權值為 0, 然後類似二叉樹的構建方法。每次挑權值最小的三個結點構造一棵三叉樹,以此類推,直至集合中所有結點都加入樹中。
重點:當發現無法構造時,(1),補權值為0的結點讓整個序列可以湊成構造赫夫曼n插樹的序列。(2)類似構造赫夫曼二叉樹的方法構造n叉樹,直至所有結點都加入樹中
赫夫曼樹的編碼 程式碼 解碼 建樹過程
現在讓我們一步一步構思
從最簡單的開始,先給你 A B C D 4個節點 權值分別為 5 4 2 7
把他們一起存到一個數組內,然後 遍歷 找到2個權值最小的 節點相加然後加入陣列,標記一下,這兩個及節點就被刪除了,兩個節點相加得到新的節點,加入到陣列中,之後再繼續 遍歷 找到權值最小的兩個點,繼續重複上述操作,直到所有前面的 最初的 4 個節點都用過。(下面陣列我們對第0列棄用,方便填下標)
下標 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
weight | 0 | 5 | 4 | 2 | 7 | 0 | 0 | 0 |
parent | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
lchild | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
rchild | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
我們進行第一次遍歷 找到最小的兩個數 4 2 然後讓他們作為左右子樹構造成一個 二叉樹 將這個 新的權值 2+4 = 6填入到下標第一空的地方,將 2 和 4 的parent 改為 新建權值的下標 將 新建節點的左右子樹 改為兩個節點的 下標
下標 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
weight | 0 | 5 | 4 | 2 | 7 | 6 | 0 | 0 |
parent | 0 | 0 | 5 | 5 | 0 | 0 | 0 | 0 |
lchild | 0 | 0 | 0 | 0 | 0 | 2 | 0 | 0 |
rchild | 0 | 0 | 0 | 0 | 0 | 3 | 0 | 0 |
之後繼續遍歷 weight 從第1個 到 第5個 找到 權值最小的兩個(2 和 4已經被用過了)找到 5 和 6,繼續建樹 。新建樹的權值為 11 ,然後改 5 和 6 的parent 為 新建權值的下標 6.改新建權值的 lchild為 1 。rchild 為 5。
下標 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
weight | 0 | 5 | 4 | 2 | 7 | 6 | 11 | 0 |
parent | 0 | 6 | 5 | 5 | 0 | 6 | 0 | 0 |
lchild | 0 | 0 | 0 | 0 | 0 | 2 | 1 | 0 |
rchild | 0 | 0 | 0 | 0 | 0 | 3 | 5 | 0 |
然後我們繼續遍歷找到 7 和 11 繼續建樹,加到陣列的最後一個位置 改 7 和 11 的parent 改為 7, 新建節點18 的lchild 為 權值為7的下標4 .新建節點18 的 rchild 為權值為11 的下標6.這時所有的節點都用完了,我們構建赫夫曼樹完成。
下標 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
weight | 0 | 5 | 4 | 2 | 7 | 6 | 11 | 18 |
parent | 0 | 6 | 5 | 5 | 7 | 6 | 7 | 0 |
lchild | 0 | 0 | 0 | 0 | 0 | 2 | 1 | 4 |
rchild | 0 | 0 | 0 | 0 | 0 | 3 | 5 | 6 |
然後求 每個字元的Huffman編碼。
我們從舉個例子,先找第一個 字元的huffman編碼,也就是權值為 5 的赫夫曼編碼,
1)先構建一個棧。
2)然後找第一個字元的 parent,判斷是lchild 還是 rchild。(parent 是 6)找到下標為6的列
3)判斷是lchild 還是 rchild。如果是lchild 則向棧中 壓入0,如果是rchild 則向棧中壓入1.(左孩子,把0壓入棧中)
4)然後繼續找parent。(找到6(權值11)的parent,下標為 7),重複操作 (3)操作(4)直到 Parent 變為0結束(也就是根節點)。然後彈棧,用一個數組接收。
然後我們求出第一個字元的 赫夫曼編碼是 10
下面給個題:
給定報文中26個字母a-z及空格的出現頻率{64, 13, 22, 32, 103, 21, 15, 47, 57, 1, 5, 32, 20, 57, 63, 15, 1, 48, 51, 80, 23, 8, 18, 1, 16, 1, 168},構建哈夫曼樹併為這27個字元編制哈夫曼編碼,並輸出。模擬傳送端,從鍵盤輸入字串,以%為結束標記,在螢幕上輸出輸入串的編碼;模擬接收端,從鍵盤上輸入0-1哈夫曼編碼串,翻譯出對應的原文
#include<iostream>
#include<stdlib.h>
#include<stack>
#include<cstring>
#include<string.h>
using namespace std;
typedef struct
{
int weight;
int parent, lchild, rchild;
}HTNode, *HuffmanTree;
typedef char **HuffmanCode;
void Select(HuffmanTree &HT, int end, int &s1, int &s2) //找出最小的兩個值。兩個最小的值得下標記錄到 s1 s2中。
{
int min1 = 0x3f3f3f, min2 = 0x3f3f3f;
for(int i = 1; i <= end; i++)
{
if(HT[i].parent == 0 && HT[i].weight < min1)
{
min1 = HT[i].weight;
s1 = i;
}
}
for(int i = 1; i <= end; i++)
{
if(HT[i].parent == 0 && HT[i].weight < min2 && s1 != i)
{
min2 = HT[i].weight;
s2 = i;
}
}
}
void HuffmanCoding(HuffmanTree &HT, HuffmanCode &HC, int *w, int n) //建立赫夫曼樹和赫夫曼編碼
{
int i, s1, s2;
HuffmanTree p;
if(n <= 1)
return ;
int m = 2 * n - 1;
HT = (HuffmanTree) malloc((m + 1) * sizeof(HTNode));
for(p = HT + 1, i = 1; i <= n; i++, p++, w++)
{
*p = {*w, 0, 0, 0};
}
for(; i <= m; i++, p++)
*p = {0, 0, 0, 0};
for(i = n + 1; i <= m; i++)
{
Select(HT, i-1, s1, s2);
HT[i].weight = HT[s1].weight + HT[s2].weight;
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
}
//從葉子到根逆向求每個字元的赫夫曼樹編碼
stack<char> s;
for(i = 1; i <= n; i++)
{
int temp = i, p, k = 0;
p = HT[temp].parent;
while(p)
{
if(HT[p].lchild == temp)
s.push('0');
if(HT[p].rchild == temp)
s.push('1');
temp = p;
p = HT[temp].parent;
k++;
}
int j = 0;
while(!s.empty())
{
HC[i][++j] = s.top();
s.pop();
}
HC[i][0] = j;
}
}
void showHuffmanCode(HuffmanCode HC) //顯示每個字元的赫夫曼編碼
{
char c ;
for(int i = 1; i <= 27; i++){
if(i != 27)
{
c = i + 'A' - 1;
cout << c << "的赫夫曼編碼是:";
}
else
{
cout << "空格的赫夫曼編碼是:";
}
for(int j = 1; j <= HC[i][0]; j++)
{
cout << HC[i][j];
}
cout << endl;
}
}
void TanserString(HuffmanCode HC,string s) //將字元轉化為赫夫曼編碼
{
string ss;
for(int i = 0; i < s.length(); i++)
{
if(s[i] >= 'A' && s[i] <= 'Z')
s[i] += 32;
if(s[i] == ' ')
s[i] = 'z' + 1;
}
for(int i = 0; i < s.length(); i++)
{
for(int j = 1; j <= HC[s[i] - 'a' + 1][0] ;j++)
ss += HC[s[i] - 'a' + 1][j];
}
cout << ss << endl;
}
void TanserHuffmanCode(HuffmanCode HC,string s) //將赫夫曼碼變為字元
{
string ss = "", s1 = "";
string t[27];
for(int i = 0 ; i < 27 ;i++)
{
t[i] = "";
for(int k = 1; k <= HC[i + 1][0] ;k++)
{
t[i] += HC[i + 1][k];
}
}
for(int i = 0; i < s.size(); i++)
{
ss += s[i];
for(int j = 0; j < 27; j++)
{
if(ss == t[j])
{
ss = "";
if(j != 26)
{
s1 += j + 'a' ;
}
else if(j == 26)
{
s1 += ' ';
}
}
}
}
cout << s1 << endl;
}
void help(){
cout << "************************************************************" << endl;
cout << "******** 1.輸入HuffmanTree的引數 ****" << endl;
cout << "******** 2.初始化HuffmanTree引數.《含有26字母及空格》 ****" << endl;
cout << "******** 3.建立HuffmanTree和編碼表。 ****" << endl;
cout << "******** 4.輸出編碼表。 ****" << endl;
cout << "******** 5.輸入編碼,並翻譯為字元。 ****" << endl;
cout << "******** 6.輸入字元,並實現轉碼 ****" << endl;
cout << "******** 7.退出 ****" << endl;
cout << "************************************************************" << endl;
}
int main ()
{
HuffmanTree HT;
HuffmanCode HC;
string s;
HC = (HuffmanCode) malloc ((27+1) * sizeof(char *));
for(int i = 1; i <= 28 ;i++)
HC[i] = (char *)malloc((27+1) * sizeof(char));
help();
int a[27] = {64, 13, 22, 32, 103, 21, 15, 47, 57, 1, 5, 32, 20, 57, 63, 15, 1, 48, 51, 80, 23, 8, 18, 1, 16, 1, 168};
int operator_code;
while (1)
{
cout << "請輸入操作 :" << endl;
cin >> operator_code;
if(operator_code == 1)
{
HuffmanCoding(HT, HC, a, 27);
cout << "建立成功,1,2,3已完成,無需輸入2,3" << endl;
}
else if(operator_code == 4)
{
showHuffmanCode(HC);
}
else if(operator_code == 5)
{
getchar();
cout << "請輸入HuffmanCode:";
getline(cin,s);
TanserHuffmanCode(HC,s);
}
else if(operator_code == 6)
{
getchar();
cout << "請輸入字元:";
getline(cin,s);
TanserString(HC,s);
}
else if( operator_code == 7)
{
break;
}
else
{
cout << "輸入違法請重新輸入" << endl;
}
}
return 0;
}