0023演算法筆記——【貪心演算法】哈夫曼編碼問題
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產生的一棵最優字首編碼樹。
程式執行結果如圖: