1、問題描述
哈夫曼編碼是廣泛地用於數據文件壓縮的十分有效的編碼方法。其壓縮率通常在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%。
前綴碼:對每一個字符規定一個0,1串作為其代碼,並要求任一字符的代碼都不是其他字符代碼的前綴。這種編碼稱為前綴碼。編碼的前綴性質可以使譯碼方法非常簡單;例如001011101可以唯一的分解為0,0,101,1101,因而其譯碼為aabe。
譯碼過程需要方便的取出編碼的前綴,因此需要表示前綴碼的合適的數據結構。為此,可以用二叉樹作為前綴碼的數據結構:樹葉表示給定字符;從樹根到樹葉的路徑當作該字符的前綴碼;代碼中每一位的0或1分別作為指示某節點到左兒子或右兒子的“路標”。
從上圖可以看出,表示最優前綴碼的二叉樹總是一棵完全二叉樹,即樹中任意節點都有2個兒子。圖a表示定長編碼方案不是最優的,其編碼的二叉樹不是一棵完全二叉樹。在一般情況下,若C是編碼字符集,表示其最優前綴碼的二叉樹中恰有|C|個葉子。每個葉子對應於字符集中的一個字符,該二叉樹有|C|-1個內部節點。
給定編碼字符集C及頻率分布f,即C中任一字符c以頻率f(c)在數據文件中出現。C的一個前綴碼編碼方案對應於一棵二叉樹T。字符c在樹T中的深度記為dT(c)。dT(c)也是字符c的前綴碼長。則平均碼長定義為:使平均碼長達到最小的前綴碼編碼方案稱為C的最優前綴碼。
2、構造哈弗曼編碼
哈夫曼提出構造最優前綴碼的貪心算法,由此產生的編碼方案稱為哈夫曼編碼。其構造步驟如下:
(1)哈夫曼算法以自底向上的方式構造表示最優前綴碼的二叉樹T。
(2)算法以|C|個葉結點開始,執行|C|-1次的“合並”運算后產生最終所要求的樹T。
(3)假設編碼字符集中每一字符c的頻率是f(c)。以f為鍵值的優先隊列Q用在貪心選擇時有效地確定算法當前要合並的2棵具有最小頻率的樹。一旦2棵具有最小頻率的樹合並后,產生一棵新的樹,其頻率為合並的2棵樹的頻率之和,並將新樹插入優先隊列Q。經過n-1次的合並后,優先隊列中只剩下一棵樹,即所要求的樹T。
構造過程如圖所示:
具體代碼實現如下:
(1)4d4.cpp,程序主文件

//4d4 貪心算法 哈夫曼算法 #include "stdafx.h" #include "BinaryTree.h" #include "MinHeap.h" #include <iostream> using namespace std; const int N = 6; template<class Type> class Huffman; template<class Type> BinaryTree<int> HuffmanTree(Type f[],int n); template<class Type> class Huffman { friend BinaryTree<int> HuffmanTree(Type[],int); public: operator Type() const { return weight; } //private: BinaryTree<int> tree; Type weight; }; int main() { char c[] = {'0','a','b','c','d','e','f'}; int f[] = {0,45,13,12,16,9,5};//下標從1開始 BinaryTree<int> t = HuffmanTree(f,N); cout<<"各字符出現的對應頻率分別為:"<<endl; for(int i=1; i<=N; i++) { cout<<c[i]<<":"<<f[i]<<" "; } cout<<endl; cout<<"生成二叉樹的前序遍歷結果為:"<<endl; t.Pre_Order(); cout<<endl; cout<<"生成二叉樹的中序遍歷結果為:"<<endl; t.In_Order(); cout<<endl; t.DestroyTree(); return 0; } template<class Type> BinaryTree<int> HuffmanTree(Type f[],int n) { //生成單節點樹 Huffman<Type> *w = new Huffman<Type>[n+1]; BinaryTree<int> z,zero; for(int i=1; i<=n; i++) { z.MakeTree(i,zero,zero); w[i].weight = f[i]; w[i].tree = z; } //建優先隊列 MinHeap<Huffman<Type>> Q(n); for(int i=1; i<=n; i++) Q.Insert(w[i]); //反復合並最小頻率樹 Huffman<Type> x,y; for(int i=1; i<n; i++) { x = Q.RemoveMin(); y = Q.RemoveMin(); z.MakeTree(0,x.tree,y.tree); x.weight += y.weight; x.tree = z; Q.Insert(x); } x = Q.RemoveMin(); delete[] w; return x.tree; }
(2)BinaryTree.h 二叉樹實現

#include<iostream> using namespace std; template<class T> struct BTNode { T data; BTNode<T> *lChild,*rChild; BTNode() { lChild=rChild=NULL; } BTNode(const T &val,BTNode<T> *Childl=NULL,BTNode<T> *Childr=NULL) { data=val; lChild=Childl; rChild=Childr; } BTNode<T>* CopyTree() { BTNode<T> *nl,*nr,*nn; if(&data==NULL) return NULL; nl=lChild->CopyTree(); nr=rChild->CopyTree(); nn=new BTNode<T>(data,nl,nr); return nn; } }; template<class T> class BinaryTree { public: BTNode<T> *root; BinaryTree(); ~BinaryTree(); void Pre_Order(); void In_Order(); void Post_Order(); int TreeHeight()const; int TreeNodeCount()const; void DestroyTree(); void MakeTree(T pData,BinaryTree<T> leftTree,BinaryTree<T> rightTree); void Change(BTNode<T> *r); private: void Destroy(BTNode<T> *&r); void PreOrder(BTNode<T> *r); void InOrder(BTNode<T> *r); void PostOrder(BTNode<T> *r); int Height(const BTNode<T> *r)const; int NodeCount(const BTNode<T> *r)const; }; template<class T> BinaryTree<T>::BinaryTree() { root=NULL; } template<class T> BinaryTree<T>::~BinaryTree() { } template<class T> void BinaryTree<T>::Pre_Order() { PreOrder(root); } template<class T> void BinaryTree<T>::In_Order() { InOrder(root); } template<class T> void BinaryTree<T>::Post_Order() { PostOrder(root); } template<class T> int BinaryTree<T>::TreeHeight()const { return Height(root); } template<class T> int BinaryTree<T>::TreeNodeCount()const { return NodeCount(root); } template<class T> void BinaryTree<T>::DestroyTree() { Destroy(root); } template<class T> void BinaryTree<T>::PreOrder(BTNode<T> *r) { if(r!=NULL) { cout<<r->data<<' '; PreOrder(r->lChild); PreOrder(r->rChild); } } template<class T> void BinaryTree<T>::InOrder(BTNode<T> *r) { if(r!=NULL) { InOrder(r->lChild); cout<<r->data<<' '; InOrder(r->rChild); } } template<class T> void BinaryTree<T>::PostOrder(BTNode<T> *r) { if(r!=NULL) { PostOrder(r->lChild); PostOrder(r->rChild); cout<<r->data<<' '; } } template<class T> int BinaryTree<T>::NodeCount(const BTNode<T> *r)const { if(r==NULL) return 0; else return 1+NodeCount(r->lChild)+NodeCount(r->rChild); } template<class T> int BinaryTree<T>::Height(const BTNode<T> *r)const { if(r==NULL) return 0; else { int lh,rh; lh=Height(r->lChild); rh=Height(r->rChild); return 1+(lh>rh?lh:rh); } } template<class T> void BinaryTree<T>::Destroy(BTNode<T> *&r) { if(r!=NULL) { Destroy(r->lChild); Destroy(r->rChild); delete r; r=NULL; } } template<class T> void BinaryTree<T>::Change(BTNode<T> *r)//將二叉樹bt所有結點的左右子樹交換 { BTNode<T> *p; if(r){ p=r->lChild; r->lChild=r->rChild; r->rChild=p; //左右子女交換 Change(r->lChild); //交換左子樹上所有結點的左右子樹 Change(r->rChild); //交換右子樹上所有結點的左右子樹 } } template<class T> void BinaryTree<T>::MakeTree(T pData,BinaryTree<T> leftTree,BinaryTree<T> rightTree) { root = new BTNode<T>(); root->data = pData; root->lChild = leftTree.root; root->rChild = rightTree.root; }
(3)MinHeap.h 最小堆實現

#include <iostream> using namespace std; template<class T> class MinHeap { private: T *heap; //元素數組,0號位置也儲存元素 int CurrentSize; //目前元素個數 int MaxSize; //可容納的最多元素個數 void FilterDown(const int start,const int end); //自上往下調整,使關鍵字小的節點在上 void FilterUp(int start); //自下往上調整 public: MinHeap(int n=1000); ~MinHeap(); bool Insert(const T &x); //插入元素 T RemoveMin(); //刪除最小元素 T GetMin(); //取最小元素 bool IsEmpty() const; bool IsFull() const; void Clear(); }; template<class T> MinHeap<T>::MinHeap(int n) { MaxSize=n; heap=new T[MaxSize]; CurrentSize=0; } template<class T> MinHeap<T>::~MinHeap() { delete []heap; } template<class T> void MinHeap<T>::FilterUp(int start) //自下往上調整 { int j=start,i=(j-1)/2; //i指向j的雙親節點 T temp=heap[j]; while(j>0) { if(heap[i]<=temp) break; else { heap[j]=heap[i]; j=i; i=(i-1)/2; } } heap[j]=temp; } template<class T> void MinHeap<T>::FilterDown(const int start,const int end) //自上往下調整,使關鍵字小的節點在上 { int i=start,j=2*i+1; T temp=heap[i]; while(j<=end) { if( (j<end) && (heap[j]>heap[j+1]) ) j++; if(temp<=heap[j]) break; else { heap[i]=heap[j]; i=j; j=2*j+1; } } heap[i]=temp; } template<class T> bool MinHeap<T>::Insert(const T &x) { if(CurrentSize==MaxSize) return false; heap[CurrentSize]=x; FilterUp(CurrentSize); CurrentSize++; return true; } template<class T> T MinHeap<T>::RemoveMin( ) { T x=heap[0]; heap[0]=heap[CurrentSize-1]; CurrentSize--; FilterDown(0,CurrentSize-1); //調整新的根節點 return x; } template<class T> T MinHeap<T>::GetMin() { return heap[0]; } template<class T> bool MinHeap<T>::IsEmpty() const { return CurrentSize==0; } template<class T> bool MinHeap<T>::IsFull() const { return CurrentSize==MaxSize; } template<class T> void MinHeap<T>::Clear() { CurrentSize=0; }
3、貪心選擇性質
證明哈夫曼算法正確性的兩個引理
二叉樹T表示字符集C的一個最優前綴碼,證明可以對T作適當修改后得到一棵新的二叉樹T”,在T”中x和y是最深葉子且為兄弟,同時T”表示的前綴碼也是C的最優前綴碼。設b和c是二叉樹T的最深葉子,且為兄弟。設f(b)<=f(c),f(x)<=f(y)。由於x和y是C中具有最小頻率的兩個字符,有f(x)<=f(b),f(y)<=f(c)。首先,在樹T中交換葉子b和x的位置得到T',然后再樹T'中交換葉子c和y的位置,得到樹T''。如圖所示:
由此可知,樹T和T'的前綴碼的平均碼長之差為:
因此,T''表示的前綴碼也是最優前綴碼,且x,y具有相同的碼長,同時,僅最優一位編碼不同。
4、最優子結構性質
二叉樹T表示字符集C的一個最優前綴碼,x和y是樹T中的兩個葉子且為兄弟,z是它們的父親。若將z當作是具有頻率f(z)=f(x)+f(y)的字符,則樹T’=T-{x,y}表示字符集C’=C-{x, y} ∪ { z}的一個最優前綴碼。因此,有:
如果T’不是C’的最優前綴碼,假定T”是C’的最優前綴碼,那么有
,顯然T”’是比T更優的前綴碼,跟前提矛盾!故T'所表示的C'的前綴碼是最優的。
由貪心選擇性質和最優子結構性質可以推出哈夫曼算法是正確的,即HuffmanTree產生的一棵最優前綴編碼樹。
程序運行結果如圖:
5.哈夫曼算法應用
參考:北大《算法設計與分析》公開課
王曉東《算法設計與分析》
CSDN:https://blog.csdn.net/liufeng_king/article/details/8720896