哈夫曼(Huffman)編碼問題(C++)

一、題目描述

哈夫曼編碼是廣泛地用於數據文件壓縮的十分有效的編碼方法。其壓縮率通常在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%。

二、舉例

我們用一個簡單的例子,來簡單描述下哈夫曼編碼是什麼?有什麼好處?

場景:X地區需要向Y地區發送一些文本,兩地之間通過電纜(或者通過電報)連接,要求用最少的二進制流傳遞信息:ABACDAAB

可以看到該信息中一共出現4個A,2個B,C、D各1個

  • 如果用常見的二進制形式編碼,那麼A:00,B:01,C:10,D:11;信息轉二進制流爲:0001001011000001,一共是16位。
  • 如果用哈夫曼編碼,則其的一種爲A:1,B:01,C:000,D:001;信息轉二進制流爲:10110000011101,一共是14位,比普通的少2位。

這時,我們可能就明白了哈夫曼編碼不就是頻率出現越高,其編碼越短嗎?

我們嘗試這樣來賦值,A:0,B:1,C:01,D:10;二進制流爲:0100110001,這個只有10位,比哈夫曼還少呢!

上面的說法似乎有道理,但是我們忽略了哈夫曼的用途可能是信息傳輸和壓縮。當我們把編碼規則和二進制流告訴接收方時,他們需要把這些二進制流還原爲看得懂的信息。普通編碼和哈夫曼編碼都可以順利還原,而第三種則不可能還原,0100…到底是ABAA…還是CAA…呢?

所以哈夫曼編碼必須是前綴碼,而且還是最優前綴碼(前綴碼定義:在一個字符集中,任何一個字符的編碼都不是另一個字符編碼的前綴)

因此,必須達到以上兩點構造出的編碼纔是哈夫曼碼。

三、算法設計與分析

1、前綴碼:
對每一個字符規定一個0,1串作爲其代碼,並要求任一字符的代碼都不是其他字符代碼的前綴。這種編碼稱爲前綴碼。編碼的前綴性質可以使譯碼方法非常簡單;例如001011101可以唯一的分解爲0,0,101,1101,因而其譯碼爲aabe。
譯碼過程需要方便的取出編碼的前綴,因此需要表示前綴碼的合適的數據結構。爲此,可以用二叉樹作爲前綴碼的數據結構:樹葉表示給定字符;從樹根到樹葉的路徑當作該字符的前綴碼;代碼中每一位的0或1分別作爲指示某節點到左兒子或右兒子的“路標”。
在這裏插入圖片描述圖-1a 與固定長度編碼對應的樹; 圖-1b 對應於最優前綴編碼的樹

從上圖可以看出,表示最優前綴碼的二叉樹總是一棵完全二叉樹,即樹中任意節點都有2個兒子。圖a表示定長編碼方案不是最優的,其編碼的二叉樹不是一棵完全二叉樹。在一般情況下,若C是編碼字符集,表示其最優前綴碼的二叉樹中恰有|C|個葉子。每個葉子對應於字符集中的一個字符,該二叉樹有|C|-1個內部節點。

給定編碼字符集C及頻率分佈f,即C中任一字符c以頻率f©在數據文件中出現。C的一個前綴碼編碼方案對應於一棵二叉樹T。字符c在樹T中的深度記爲dT©。dT©也是字符c的前綴碼長。則平均碼長定義爲:
在這裏插入圖片描述

使平均碼長達到最小的前綴碼編碼方案稱爲C的最優前綴碼。

2、構造哈夫曼編碼:
哈夫曼提出構造最優前綴碼的貪心算法,由此產生的編碼方案稱爲哈夫曼編碼。其構造步驟如下:

  • (1)哈夫曼算法以自底向上的方式構造表示最優前綴碼的二叉樹T。
  • (2)算法以|C|個葉結點開始,執行|C|-1次的“合併”運算後產生最終所要求的樹T
  • (3)假設編碼字符集中每一字符c的頻率是f©。以f爲鍵值的優先隊列Q用在貪心選擇時有效地確定算法當前要合併的2棵具有最小頻率的樹。一旦2棵具有最小頻率的樹合併後,產生一棵新的樹,其頻率爲合併的2棵樹的頻率之和,並將新樹插入優先隊列Q。經過n-1次的合併後,優先隊列中只剩下一棵樹,即所要求的樹T。

構造過程如圖-2所示:
在這裏插入圖片描述圖-2 哈夫曼樹構造過程

三、結果與分析

本實驗以算法導論書中的例題爲測試用例,來驗證算法的正確性。即
在這裏插入圖片描述實驗結果截圖如下圖-7,結果與題目描述中給出的變長代碼字一樣:
在這裏插入圖片描述

圖-7 實驗結果截圖

五、實驗總結

1、實驗結果與給出的變長代碼字一樣,算法正確,且哈夫曼編碼問題是一個貪心算法問題。採用哈夫曼編碼技術可以最小化總的編碼長度,從而實現數據文件的壓縮存儲。
2、構造好哈夫曼樹後,可用排列樹回溯法來打印哈夫曼編碼,即遇到左子樹向左走,vector添加記錄0;遇到右子樹向右走,vector添加記錄1;走到葉子節點並打印出葉節點的編碼後回溯,同時往上退一層,則vector彈出一個值。如此不斷回溯下去,即可打印所有字符編碼。

六、源代碼(C++)

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

//Huffman樹的節點類
typedef struct Node
{
    char value;               //結點的字符值
    int weight;               //結點字符出現的頻度
    Node *lchild,*rchild;     //結點的左右孩子
}Node;

//自定義排序規則,即以vector中node結點weight值升序排序
bool ComNode(Node *p,Node *q)
{
    return p->weight<q->weight;
}

//構造Huffman樹,返回根結點指針
Node* BuildHuffmanTree(vector<Node*> vctNode)
{
    while(vctNode.size()>1)                            //vctNode森林中樹個數大於1時循環進行合併
    {
        sort(vctNode.begin(),vctNode.end(),ComNode);   //依頻度高低對森林中的樹進行升序排序

        Node *first=vctNode[0];    //取排完序後vctNode森林中頻度最小的樹根
        Node *second=vctNode[1];   //取排完序後vctNode森林中頻度第二小的樹根
        Node *merge=new Node;      //合併上面兩個樹
        merge->weight=first->weight+second->weight;
        merge->lchild=first;
        merge->rchild=second;

        vector<Node*>::iterator iter;
        iter=vctNode.erase(vctNode.begin(),vctNode.begin()+2);    //從vctNode森林中刪除上訴頻度最小的兩個節點first和second
        vctNode.push_back(merge);                                 //向vctNode森林中添加合併後的merge樹
    }
    return vctNode[0];            //返回構造好的根節點
}

//用回溯法來打印編碼
void PrintHuffman(Node *node,vector<int> vctchar)
{
    if(node->lchild==NULL && node->rchild==NULL)
    {//若走到葉子節點,則迭代打印vctchar中存的編碼
        cout<<node->value<<": ";
        for(vector<int>::iterator iter=vctchar.begin();iter!=vctchar.end();iter++)
            cout<<*iter;
        cout<<endl;
        return;
    }
    else
    {
        vctchar.push_back(1);     //遇到左子樹時給vctchar中加一個1
        PrintHuffman(node->lchild,vctchar);
        vctchar.pop_back();       //回溯,刪除剛剛加進去的1
        vctchar.push_back(0);     //遇到左子樹時給vctchar中加一個0
        PrintHuffman(node->rchild,vctchar);
        vctchar.pop_back();       //回溯,刪除剛剛加進去的0

    }
}

int main()
{
    cout<<"************ Huffman編碼問題 ***************"<<endl;
    cout<<"請輸入要編碼的字符,並以空格隔開(個數任意):"<<endl;
    vector<Node*> vctNode;        //存放Node結點的vector容器vctNode
    char ch;                      //臨時存放控制檯輸入的字符
    while((ch=getchar())!='\n')
    {
        if(ch==' ')continue;      //遇到空格時跳過,即沒輸入一個字符空一格空格
        Node *temp=new Node;
        temp->value=ch;
        temp->lchild=temp->rchild = NULL;
        vctNode.push_back(temp);  //將新的節點插入到容器vctNode中
    }

    cout<<endl<<"請輸入每個字符對應的頻度,並以空格隔開:"<<endl;
    for(int i=0;i<vctNode.size();i++)
        cin>>vctNode[i]->weight;

    Node *root = BuildHuffmanTree(vctNode);   //構造Huffman樹,將返回的樹根賦給root
    vector<int> vctchar;
    cout<<endl<<"對應的Huffman編碼如下:"<<endl;
    PrintHuffman(root,vctchar);

    system("pause");
}

參考自:
allinallinallin

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章