貪心法之哈夫曼編碼問題


 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;
}
View Code

  (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;
}
View Code

(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;
}
View Code

 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM