一、題目描述
哈夫曼編碼是廣泛地用於數據文件壓縮的十分有效的編碼方法。其壓縮率通常在20%~90%之間。哈夫曼編碼算法用字符在文件中出現的頻率表來建立一個用0,1串表示各字符的最優表示方式。一個包含100,000個字符的文件,各字符出現頻率不同,如下表所示:
有多種方式表示文件中的信息,若用0,1碼錶示字符的方法,即每個字符用唯一的一個0,1串表示。若採用定長編碼表示,則需要3位表示一個字符,整個文件編碼需要300,000位;若採用變長編碼表示,給頻率高的字符較短的編碼;頻率低的字符較長的編碼,達到整體編碼減少的目的,則整個文件編碼需要(45×1+13×3+12×3+16×3+9×4+5×4)×1000=224,000位,由此可見,變長碼比定長碼方案好,總碼長減小約25%。
二、舉例
我們用一個簡單的例子,來簡單描述下哈夫曼編碼是什麼?有什麼好處?
場景:X地區需要向Y地區發送一些文本,兩地之間通過電纜(或者通過電報)連接,要求用最少的二進制流傳遞信息:ABACDAAB
可以看到該信息中一共出現4個A,2個B,C、D各1個
- 如果用常見的二進制形式編碼,那麼A:00,B:01,C:10,D:11;信息轉二進制流爲:0001001011000001,一共是16位。
- 如果用哈夫曼編碼,則其的一種爲A:1,B:01,C:000,D:001;信息轉二進制流爲:10110000011101,一共是14位,比普通的少2位。
這時,我們可能就明白了哈夫曼編碼不就是頻率出現越高,其編碼越短嗎?
我們嘗試這樣來賦值,A:0,B:1,C:01,D:10;二進制流爲:0100110001,這個只有10位,比哈夫曼還少呢!
上面的說法似乎有道理,但是我們忽略了哈夫曼的用途可能是信息傳輸和壓縮。當我們把編碼規則和二進制流告訴接收方時,他們需要把這些二進制流還原爲看得懂的信息。普通編碼和哈夫曼編碼都可以順利還原,而第三種則不可能還原,0100…到底是ABAA…還是CAA…呢?
所以哈夫曼編碼必須是前綴碼,而且還是最優前綴碼(前綴碼定義:在一個字符集中,任何一個字符的編碼都不是另一個字符編碼的前綴)
因此,必須達到以上兩點構造出的編碼纔是哈夫曼碼。
三、算法設計與分析
1、前綴碼:
對每一個字符規定一個0,1串作爲其代碼,並要求任一字符的代碼都不是其他字符代碼的前綴。這種編碼稱爲前綴碼。編碼的前綴性質可以使譯碼方法非常簡單;例如001011101可以唯一的分解爲0,0,101,1101,因而其譯碼爲aabe。
譯碼過程需要方便的取出編碼的前綴,因此需要表示前綴碼的合適的數據結構。爲此,可以用二叉樹作爲前綴碼的數據結構:樹葉表示給定字符;從樹根到樹葉的路徑當作該字符的前綴碼;代碼中每一位的0或1分別作爲指示某節點到左兒子或右兒子的“路標”。
圖-1a 與固定長度編碼對應的樹; 圖-1b 對應於最優前綴編碼的樹
從上圖可以看出,表示最優前綴碼的二叉樹總是一棵完全二叉樹,即樹中任意節點都有2個兒子。圖a表示定長編碼方案不是最優的,其編碼的二叉樹不是一棵完全二叉樹。在一般情況下,若C是編碼字符集,表示其最優前綴碼的二叉樹中恰有|C|個葉子。每個葉子對應於字符集中的一個字符,該二叉樹有|C|-1個內部節點。
給定編碼字符集C及頻率分佈f,即C中任一字符c以頻率f©在數據文件中出現。C的一個前綴碼編碼方案對應於一棵二叉樹T。字符c在樹T中的深度記爲dT©。dT©也是字符c的前綴碼長。則平均碼長定義爲:
使平均碼長達到最小的前綴碼編碼方案稱爲C的最優前綴碼。
2、構造哈夫曼編碼:
哈夫曼提出構造最優前綴碼的貪心算法,由此產生的編碼方案稱爲哈夫曼編碼。其構造步驟如下:
- (1)哈夫曼算法以自底向上的方式構造表示最優前綴碼的二叉樹T。
- (2)算法以|C|個葉結點開始,執行|C|-1次的“合併”運算後產生最終所要求的樹T
- (3)假設編碼字符集中每一字符c的頻率是f©。以f爲鍵值的優先隊列Q用在貪心選擇時有效地確定算法當前要合併的2棵具有最小頻率的樹。一旦2棵具有最小頻率的樹合併後,產生一棵新的樹,其頻率爲合併的2棵樹的頻率之和,並將新樹插入優先隊列Q。經過n-1次的合併後,優先隊列中只剩下一棵樹,即所要求的樹T。
構造過程如圖-2所示:
圖-2 哈夫曼樹構造過程
三、結果與分析
本實驗以算法導論書中的例題爲測試用例,來驗證算法的正確性。即
實驗結果截圖如下圖-7,結果與題目描述中給出的變長代碼字一樣:
圖-7 實驗結果截圖
五、實驗總結
1、實驗結果與給出的變長代碼字一樣,算法正確,且哈夫曼編碼問題是一個貪心算法問題。採用哈夫曼編碼技術可以最小化總的編碼長度,從而實現數據文件的壓縮存儲。
2、構造好哈夫曼樹後,可用排列樹回溯法來打印哈夫曼編碼,即遇到左子樹向左走,vector添加記錄0;遇到右子樹向右走,vector添加記錄1;走到葉子節點並打印出葉節點的編碼後回溯,同時往上退一層,則vector彈出一個值。如此不斷回溯下去,即可打印所有字符編碼。
六、源代碼(C++)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//Huffman樹的節點類
typedef struct Node
{
char value; //結點的字符值
int weight; //結點字符出現的頻度
Node *lchild,*rchild; //結點的左右孩子
}Node;
//自定義排序規則,即以vector中node結點weight值升序排序
bool ComNode(Node *p,Node *q)
{
return p->weight<q->weight;
}
//構造Huffman樹,返回根結點指針
Node* BuildHuffmanTree(vector<Node*> vctNode)
{
while(vctNode.size()>1) //vctNode森林中樹個數大於1時循環進行合併
{
sort(vctNode.begin(),vctNode.end(),ComNode); //依頻度高低對森林中的樹進行升序排序
Node *first=vctNode[0]; //取排完序後vctNode森林中頻度最小的樹根
Node *second=vctNode[1]; //取排完序後vctNode森林中頻度第二小的樹根
Node *merge=new Node; //合併上面兩個樹
merge->weight=first->weight+second->weight;
merge->lchild=first;
merge->rchild=second;
vector<Node*>::iterator iter;
iter=vctNode.erase(vctNode.begin(),vctNode.begin()+2); //從vctNode森林中刪除上訴頻度最小的兩個節點first和second
vctNode.push_back(merge); //向vctNode森林中添加合併後的merge樹
}
return vctNode[0]; //返回構造好的根節點
}
//用回溯法來打印編碼
void PrintHuffman(Node *node,vector<int> vctchar)
{
if(node->lchild==NULL && node->rchild==NULL)
{//若走到葉子節點,則迭代打印vctchar中存的編碼
cout<<node->value<<": ";
for(vector<int>::iterator iter=vctchar.begin();iter!=vctchar.end();iter++)
cout<<*iter;
cout<<endl;
return;
}
else
{
vctchar.push_back(1); //遇到左子樹時給vctchar中加一個1
PrintHuffman(node->lchild,vctchar);
vctchar.pop_back(); //回溯,刪除剛剛加進去的1
vctchar.push_back(0); //遇到左子樹時給vctchar中加一個0
PrintHuffman(node->rchild,vctchar);
vctchar.pop_back(); //回溯,刪除剛剛加進去的0
}
}
int main()
{
cout<<"************ Huffman編碼問題 ***************"<<endl;
cout<<"請輸入要編碼的字符,並以空格隔開(個數任意):"<<endl;
vector<Node*> vctNode; //存放Node結點的vector容器vctNode
char ch; //臨時存放控制檯輸入的字符
while((ch=getchar())!='\n')
{
if(ch==' ')continue; //遇到空格時跳過,即沒輸入一個字符空一格空格
Node *temp=new Node;
temp->value=ch;
temp->lchild=temp->rchild = NULL;
vctNode.push_back(temp); //將新的節點插入到容器vctNode中
}
cout<<endl<<"請輸入每個字符對應的頻度,並以空格隔開:"<<endl;
for(int i=0;i<vctNode.size();i++)
cin>>vctNode[i]->weight;
Node *root = BuildHuffmanTree(vctNode); //構造Huffman樹,將返回的樹根賦給root
vector<int> vctchar;
cout<<endl<<"對應的Huffman編碼如下:"<<endl;
PrintHuffman(root,vctchar);
system("pause");
}
參考自:
allinallinallin