1. 程式人生 > >0023演算法筆記——【貪心演算法】哈夫曼編碼問題

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產生的一棵最優字首編碼樹。

     程式執行結果如圖: