數據結構圖文解析之:哈夫曼樹與哈夫曼編碼詳解及C++模板實現

0. 數據結構圖文解析系列

數據結構系列文章
數據結構圖文解析之:數組、單鏈表、雙鏈表介紹及C++模板實現
數據結構圖文解析之:棧的簡介及C++模板實現
數據結構圖文解析之:隊列詳解與C++模板實現
數據結構圖文解析之:樹的簡介及二叉排序樹C++模板實現.
數據結構圖文解析之:AVL樹詳解及C++模板實現
數據結構圖文解析之:二叉堆詳解及C++模板實現
數據結構圖文解析之:哈夫曼樹與哈夫曼編碼詳解及C++模板實現
數據結構圖文解析之:直接插入排序及其優化(二分插入排序)解析及C++實現

1. 哈夫曼編碼簡介

哈夫曼編碼(Huffman Coding)是一種編碼方式,也稱爲「赫夫曼編碼」,是David A. Huffman1952年發明的一種構建極小多餘編碼的方法。
在計算機數據處理中,霍夫曼編碼使用變長編碼表對源符號進行編碼,出現頻率較高的源符號採用較短的編碼,出現頻率較低的符號採用較長的編碼,使編碼以後的字符串字符串的平均長度 、指望值下降,以達到無損壓縮數據的目的。
舉個例子,如今咱們有一字符串:html

this is an example of a huffman treenode

這串字符串有36個字符,若是按普通方式存儲這串字符串,每一個字符佔據1個字節,則共須要36 * 1 * 8 = 288bit。
通過分析咱們發現,這串字符串中各字母出現的頻率不一樣,若是咱們可以按以下編碼:git

字母 頻率 編碼 --- 字母 頻率 編碼
space 7 111 s 2 1011
a 4 010 t 2 0110
e 4 000 l 1 11001
f 3 1101 o 1 00110
h 2 1010 p 1 10011
i 2 1000 r 1 11000
m 2 0111 u 1 00111
n 2 0010 x 1 10010

編碼這串字符串,只須要:
(7+4+4)x3 + (3+2+2+2+2+2+2)x4 + (1+1+1+1+1+1)x 5 = 45+60+30 = 135bit
編碼這串字符串只須要135bit!單單這串字符串,就壓縮了288-135 = 153bit。github

那麼,咱們如何獲取每一個字符串的編碼呢?這就須要哈夫曼樹了。
源字符編碼的長短取決於其出現的頻率,咱們把源字符出現的頻率定義爲該字符的權值。數組

2. 哈夫曼樹簡介

哈夫曼又稱最優二叉樹。是一種帶權路徑長度最短的二叉樹。它的定義以下。數據結構

哈夫曼樹的定義

假設有n個權值{w1,w2,w3,w4...,wn},構造一棵有n個節點的二叉樹,若樹的帶權路徑最小,則這顆樹稱做哈夫曼樹。這裏面涉及到幾個概念,咱們由一棵哈夫曼樹來解釋
函數

  • 路徑與路徑長度:從樹中一個節點到另外一個節點之間的分支構成了兩個節點之間的路徑,路徑上的分支數目稱做路徑長度。若規定根節點位於第一層,則根節點到第H層的節點的路徑長度爲H-1.如樹b:100到60 的路徑長度爲1;100到30的路徑長度爲2;100到20的路徑長度爲3。post

  • 樹的路徑長度:從根節點到每一節點的路徑長度之和。樹a的路徑長度爲1+1+2+2+2+2 = 10;樹b的路徑長度爲1+1+2+2+3+3 = 12.
  • 節點的權:將樹中的節點賦予一個某種含義的數值做爲該節點的權值,該值稱爲節點的權;
  • 帶權路徑長度:從根節點到某個節點之間的路徑長度與該節點的權的乘積。例如樹b中,節點10的路徑長度爲3,它的帶權路徑長度爲10 * 3 = 30;
  • 樹的帶權路徑長度:樹的帶權路徑長度爲全部葉子節點的帶權路徑長度之和,稱爲WPL。樹a的WPL = 2*(10+20+30+40) = 200 ;樹b的WPL = 1x40+2x30+3x10+3x20 = 190.而哈夫曼樹就是樹的帶權路徑最小的二叉樹性能

3. 構造哈夫曼樹

3.1 哈夫曼樹的節點結構

/*哈夫曼樹的節點定義*/
template <typename T>
struct HuffmanNode
{
    HuffmanNode(T k,HuffmanNode<T>*l=nullptr,HuffmanNode<T>* r=nullptr)
        :key(k),lchild(l), rchild(r){}
    ~HuffmanNode(){};
 
    T key;                                     //節點的權值
    HuffmanNode<T>* lchild;        //節點左孩
    HuffmanNode<T>* rchild;        //節點右孩
};
  1. value: 節點的權值
  2. lchild:節點左孩子
  3. rchild:節點右孩子

3.2 哈夫曼樹的抽象數據類型

template <typename T>
class Huffman
{
public:
    void preOrder();              //前序遍歷哈夫曼樹
    void inOrder();                  //中序遍歷哈夫曼樹
    void postOrder();              //後序遍歷哈夫曼樹
 
    void creat(T a[], int size);  //建立哈夫曼樹
    void destory();                  //銷燬哈夫曼樹
    void print();                  //打印哈夫曼樹

    Huffman();
    ~Huffman(){};
 
private:
    void preOrder(HuffmanNode<T>* pnode);
    void inOrder(HuffmanNode<T>* pnode);
    void postOrder(HuffmanNode<T>*pnode);
    void print(HuffmanNode<T>*pnode);
    void destroy(HuffmanNode<T>*pnode);
 
private:
    HuffmanNode<T>* root;     //哈夫曼樹根節點
    deque<HuffmanNode<T>*> forest;//森林
};
  1. root:哈夫曼樹的根結點。
  2. forset : 森林,這裏使用deque來存儲森林中樹的根節點。

3.3 哈夫曼樹的構造步驟

假設有n個權值,則構造出的哈夫曼樹有n個葉子節點.n個權值記爲{w1,w2,w3...wn},哈夫曼樹的構造過程爲:測試

  1. 將w1,w2,w3...wn當作具備n棵樹的森林,每棵樹僅有一個節點。
  2. 從森林中,選取兩棵根節點權值最小的樹,兩棵樹分別做爲左子樹與右子樹,構建一棵新樹。新樹的權值等於左右子樹權值之和。
  3. 從森林中刪除兩棵權值最小的樹,將構建完成後的新樹加入森林中。
  4. 重複二、3步驟,直到森林只剩一棵樹爲止。這棵樹即是哈夫曼樹。

圖一的樹b爲一棵哈夫曼樹,它的葉子節點爲{10,20,30,40},以這4個權值構建樹b的過程爲:

這個過程很編碼實現爲:

/*建立哈夫曼樹*/
template<typename T>
void Huffman<T>::creat(T a[],int size)
{
    for (int i = 0; i < size; i++) //每一個節點都做爲一個森林
    {
        //爲初始序列的元素構建節點。每一個節點做爲一棵樹加入森林中。
        HuffmanNode<T>* ptr = new HuffmanNode<T>(a[i],nullptr,nullptr);  
        forest.push_back(ptr);
    }
    for (int i = 0; i < size - 1; i++)
    {
        //排序,以選出根節點權值最小兩棵樹
        sort(forest.begin(), forest.end(), [](HuffmanNode<T>* a, HuffmanNode<T>*b){return a->key< b->key; });    
        HuffmanNode<T>*node = new HuffmanNode<T>(forest[0]->key + forest[1]->key, forest[0], forest[1]); //構建新節點
        forest.push_back(node);  //新節點加入森林中
        forest.pop_front(); //刪除兩棵權值最小的樹
        forest.pop_front();
    }
    root = forest.front();
    forest.clear();
};
  1. 這裏僅僅示範構建哈夫曼樹的過程,沒有通過性能上的優化和完善的異常處理。這裏使用deque雙端隊列來存儲森林中樹根節點,使用庫函數sort來對根節點依權值排序。這裏也能夠使用咱們以前介紹的小頂堆來完成工做。

3.4 哈夫曼樹的其餘操做

其餘操做在前幾篇博文中都有介紹過,這裏就再也不囉嗦,能夠在文章底部連接取得完整的工程源碼。
這裏貼出測試時須要的代碼:

/*打印哈夫曼樹*/
template<typename T>
void Huffman<T>::print(HuffmanNode<T>* pnode)
{
    if (pnode != nullptr)
    {
        cout << "當前結點:" << pnode->key<<".";
        if (pnode->lchild != nullptr)
            cout << "它的左孩子節點爲:" << pnode->lchild->key << ".";
        else cout << "它沒有左孩子.";
        if (pnode->rchild != nullptr)
            cout << "它的右孩子節點爲:" << pnode->rchild->key << ".";
        else cout << "它沒有右孩子.";
        cout << endl;
        print(pnode->lchild);
        print(pnode->rchild);
    }
};

3.5 哈夫曼樹代碼測試

咱們構建上圖中的哈夫曼樹,它的四個權值分別爲{10,20,30,40}:
測試代碼:

int _tmain(int argc, _TCHAR* argv[])
{
    Huffman<int> huff;
    int a[] = { 10,20,30,40 };
    huff.creat(a, 4);    //構建一棵哈夫曼樹
    huff.print();        //打印節點間關係
    getchar();
    return 0;
}

測試結果:

當前結點:100.它的左孩子節點爲:40.它的右孩子節點爲:60.
當前結點:40.它沒有左孩子.它沒有右孩子.
當前結點:60.它的左孩子節點爲:30.它的右孩子節點爲:30.
當前結點:30.它沒有左孩子.它沒有右孩子.
當前結點:30.它的左孩子節點爲:10.它的右孩子節點爲:20.
當前結點:10.它沒有左孩子.它沒有右孩子.
當前結點:20.它沒有左孩子.它沒有右孩子.

根據節點關係能夠畫出以下二叉樹,正是上面咱們構建的哈夫曼樹。

4. 再看哈夫曼編碼

爲{10,20,30,40}這四個權值構建了哈夫曼編碼後,咱們能夠由以下規則得到它們的哈夫曼編碼:

  • 從根節點到每個葉子節點的路徑上,左分支記爲0,右分支記爲1,將這些0與1連起來即爲葉子節點的哈夫曼編碼。以下圖:

(字母)權值 編碼
10 100
20 101
30 11
40 0

因而可知,出現頻率越高的字母(也即權值越大),其編碼越短。這便使編碼以後的字符串的平均長度、指望值下降,從而達到無損壓縮數據的目的。

5. 哈夫曼樹完整代碼

哈夫曼樹完整代碼:https://github.com/huanzheWu/Data-Structure/blob/master/Huffman/Huffman/Huffman.h
更多數據結構C++實現代碼:https://github.com/huanzheWu/Data-Structure

原創文章,轉載請註明出處:http://www.cnblogs.com/QG-whz/p/5175485.html

相關文章
相關標籤/搜索