1. 程式人生 > >霍夫曼編碼(C++ 優先佇列)

霍夫曼編碼(C++ 優先佇列)

霍夫曼編碼 一般採用字首編碼 -- -- 對字符集進行編碼時,要求字符集中任一字元的編碼都不是其它字元的編碼的字首,這種編碼稱為字首(編)碼。

演算法思想:

構造哈夫曼樹非常簡單,將所有的節點放到一個佇列中,用一個節點替換兩個頻率最低的節點,新節點的頻率就是這兩個節點的頻率之和。這樣,

新節點就是兩個被替換節點的父節點了。如此迴圈,直到佇列中只剩一個節點(樹根)。 其實這就是一個貪心策略,屬於貪心演算法的典型應用。

例子:

我們直接來看示例,如果我們需要來壓縮下面的字串:

 “beep boop beer!” 

首先,我們先計算出每個字元出現的次數,我們得到下面這樣一張表 :

 

 

字元 次數
‘b’ 3
‘e’ 4
‘p’ 2
‘ ‘ 2
‘o’ 2
‘r’ 1
‘!’ 1

 


然後,我把把這些東西放到Priority Queue中(用出現的次資料當 priority),我們可以看到,Priority Queue 是以Prioirry排序一個數組,如果Priority一樣,會使用出現的次序排序:下面是我們得到的Priority Queue:

接下來就是我們的演算法——把這個Priority Queue 轉成二叉樹。我們始終從queue的頭取兩個元素來構造一個二叉樹(第一個元素是左結點,第二個是右結點),並把這兩個元素的priority相加,並放回Priority中(再次注意,這裡的Priority就是字元出現的次數),然後,我們得到下面的資料圖表:

同樣,我們再把前兩個取出來,形成一個Priority為2+2=4的結點,然後再放回Priority Queue中 :

繼續我們的演算法(我們可以看到,這是一種自底向上的建樹的過程):

最終我們會得到下面這樣一棵二叉樹:

此時,我們把這個樹的左支編碼為0,右支編碼為1,這樣我們就可以遍歷這棵樹得到字元的編碼,比如:‘b’的編碼是 00,’p’的編碼是101, ‘r’的編碼是1000。我們可以看到出現頻率越多的會越在上層,編碼也越短,出現頻率越少的就越在下層,編碼也越長

最終我們可以得到下面這張編碼表:

 

 

字元 編碼
‘b’ 00
‘e’ 11
‘p’ 101
‘ ‘ 011
‘o’ 010
‘r’ 1000
‘!’ 1001

 


這裡需要注意一點,當我們encode的時候,我們是按“bit”來encode,decode也是通過bit來完成,比如,如果我們有這樣的bitset “1011110111″ 那麼其解碼後就是 “pepe”。所以,我們需要通過這個二叉樹建立我們Huffman編碼和解碼的字典表。

 

這裡需要注意的一點是,我們的Huffman對各個字元的編碼是不會衝突的,也就是說,不會存在某一個編碼是另一個編碼的字首,不然的話就會大問題了。因為encode後的編碼是沒有分隔符的。

於是,對於我們的原始字串  beep boop beer!

其對就能的二進位制為 : 0110 0010 0110 0101 0110 0101 0111 0000 0010 0000 0110 0010 0110 1111 0110 1111 0111 0000 0010 0000 0110 0010 0110 0101 0110 0101 0111 0010 0010 0001

我們的Huffman的編碼為: 0011 1110 1011 0001 0010 1010 1100 1111 1000 1001

從上面的例子中,我們可以看到被壓縮的比例還是很可觀的。


程式碼

#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>

using namespace std;

const int M = 6;  // 待編碼字元個數

typedef struct Tree
{
    int freq;  // 出現頻率,即權重
    char key;
    Tree *left;
    Tree *right;
    Tree(int fr = 0, char k = '\0', Tree *l = nullptr, Tree *r = nullptr):
        freq(fr), key(k), left(l), right(r) {};
}Tree, *pTree;

struct cmp
{
    bool operator() (Tree *a, Tree *b)
    {
        return a->freq > b->freq;  // 升序排列
    }
};

priority_queue<pTree, vector<pTree>, cmp> pque;  // 小頂堆

// 利用中序遍歷的方法輸出霍夫曼編碼
// 左0右1,迭代完一次st回退一個字元
void printCode(Tree *proot, string st)
{
    if (proot == nullptr)
    {
        return;
    }

    if (proot->left)
    {
        st += '0';
    }
    printCode(proot->left, st);

    if (!proot->left && !proot->right)  // 葉子結點
    {
        printf("%c's code: ", proot->key);
        for (size_t i = 0; i < st.size(); ++i)
        {
            printf("%c", st[i]);
        }
        printf("\n");
    }
    st.pop_back();  // 退回一個字元

    if (proot->right)
    {
        st += '1';
    }
    printCode(proot->right, st);
}

// 清空堆上分配的記憶體空間
void del(Tree *proot)
{
    if (proot == nullptr)
    {
        return;
    }
    del(proot->left);
    del(proot->right);

    delete proot;
}

// 霍夫曼編碼
void huffman()
{
    int i;
    char c;
    int fr;

    /* 讀入測試資料
     *   a 45
     *   b 13
     *   c 12
     *   d 16
     *   e 9
     *   f 5
     */
    for (i = 0; i < M; ++i)
    {
        Tree *pt = new Tree;
        scanf("%c%d", &c, &fr);
        getchar();
        pt->key = c;
        pt->freq = fr;
        pque.push(pt);
    }

    //將森林中最小的兩個頻度組成樹,放回森林。直到森林中只有一棵樹。
    while (pque.size() > 1)
    {
        Tree *proot = new Tree;
        pTree pl, pr;
        pl = pque.top(); pque.pop();
        pr = pque.top(); pque.pop();

        proot->freq = pl->freq + pr->freq;
        proot->left = pl;
        proot->right = pr;

        pque.push(proot);
    }

    string s = "";
    printCode(pque.top(), s);
    del(pque.top());
}

int main()
{
    huffman();

    return 0;
}

執行結果

對應的二叉樹為:

 演算法以freq為鍵值的優先佇列Q用在貪心選擇時有效地確定演算法當前要合併的2棵具有最小頻率的樹。一旦2棵具有最小頻率的樹合併後,產生一棵新的樹,

其頻率為合併的2棵樹的頻率之和,並將新樹插入優先佇列Q。經過n-1次的合併後,優先佇列中只剩下一棵樹,即所要求的樹proot。演算法huffman用最

小堆實現優先佇列Q。初始化優先佇列需要O(n)計算時間,由於最小堆的節點刪除、插入均需O(logn)時間,n-1次的合併總共需要O(nlogn)計算時間。

因此,關於n個字元的哈夫曼演算法的計算時間為O(nlogn) 。
 

參考資料:

https://blog.csdn.net/daniel_ustc/article/details/17613359

https://coolshell.cn/articles/7459.html