1. 程式人生 > >【視頻編解碼·學習筆記】7. 熵編碼算法:基礎知識 & 哈夫曼編碼

【視頻編解碼·學習筆記】7. 熵編碼算法:基礎知識 & 哈夫曼編碼

html 節點 表示 效率 article tchar vector nod code

一、熵編碼概念:

熵越大越混亂

信息學中的熵:

  • 用於度量消息的平均信息量,和信息的不確定性
  • 越是隨機的、前後不相關的信息,其熵越高

信源編碼定理:

  • 說明了香農熵越信源符號概率之間的關系
  • 信息的熵為信源無損編碼後平均碼長的下限
  • 任何的無損編碼方法都不可能使編碼後的平均碼長小於香農熵,只能使其盡量接近

熵與混亂程度:
混亂度越高的信源,越難以被壓縮,需要更大量的信息來表示其排列順序
技術分享圖片

熵編碼基本思想:
是使其前後的碼字之間盡量更加隨機,盡量減小前後的相關性,更加接近其信源的香農熵。這樣在表示同樣的信息量時所用的數據長度更短。

常用的熵編碼算法:

  • 變長編碼:哈夫曼編碼 和 香農-費諾編碼。運算復雜度低,但同時編碼效率也低。
  • 算術編碼:運算復雜,但編碼效率高

二、哈夫曼編碼基本原理:

1. 哈夫曼樹簡單介紹:

  • 哈夫曼編碼是變長編碼方法的一種,該方法完全依賴於碼字出現的概率來構造整體平均長度最短的編碼
  • 關鍵步驟:建立符合哈夫曼編碼規則的二叉樹,該樹又稱作哈夫曼樹

哈夫曼樹:

  • 一種特殊的二叉樹,其終端節點的個數與待編碼的碼元的個數等同,而且每個終端節點上都帶有各自的權值
  • 每個終端節點的路徑長度乘以該節點的權值的總和稱為整個二叉樹的加權路徑長度
  • 在滿足條件的各種二叉樹中,該路徑長度最短的二叉樹即為哈夫曼樹

2. 哈夫曼樹構建過程:

  1. 將所有左,右子樹都為空的作為根節點。
  2. 在森林中選出兩棵根節點的權值最小的樹作為一棵新樹的左,右子樹,且置新樹的附加根節點的權值為其左,右子樹上根節點的權值之和。註意,左子樹的權值應小於右子樹的權值。
  3. 從森林中刪除這兩棵樹,同時把新樹加入到森林中。
  4. 重復2,3步驟,直到森林中只有一棵樹為止,此樹便是哈夫曼樹。

下面是構建哈夫曼樹的圖解過程:
技術分享圖片

3. 哈夫曼編碼:

利用哈夫曼樹求得的用於通信的二進制編碼稱為哈夫曼編碼。樹中從根到每個葉子節點都有一條路徑,對路徑上的各分支約定指向左子樹的分支表示”0”碼,指向右子樹的分支表示“1”碼,取每條路徑上的“0”或“1”的序列作為各個葉子節點對應的字符編碼,即是哈夫曼編碼。

以上圖例子來說:
A,B,C,D對應的哈夫曼編碼分別為:111,10,110,0

用圖說明如下:
技術分享圖片

4. 重要特點:

哈夫曼編碼的任意一個碼字,都不可能是其他碼字的前綴。因此通過哈夫曼編碼的信息可以緊密排列連續傳輸,而不用擔心解碼時的歧義性。

三、哈夫曼樹的構建程序:

新建一個VS工程,起名為Huffman。這個程序用來將一個英文短文中的字母,按照出現次數多少,進行哈夫曼編碼。(需自備一個英語短文,保存為txt格式,放到工程子目錄下:xxx\Huffman\Huffman)

1. 首先編寫打開和讀取文件內容部分:

#include "stdafx.h"
#include <iostream>
#include <fstream>

using namespace std;

static bool open_input_file(ifstream &input, const char *inputFileName)
{
    input.open(inputFileName);
    if (!input.is_open())
    {
        return false;
    }
    return true;
}

int _tmain(int argc, _TCHAR* argv[])
{
    ifstream inputFile;
    if (!open_input_file(inputFile, "input.txt"))
    {
        cout << "Error: opening input file failed!" << endl;
        return -1;
    }

    char buf = inputFile.get();
    while (inputFile.good())
    {
        cout << buf;
        buf = inputFile.get();
    }
    
    inputFile.close();
    return 0;
}

2. 統計字符出現頻次:

創建一個結構體,保存字符及其頻次:

typedef struct
{
    unsigned char   character;
    unsigned int    frequency;
} CharNode;

在之前的代碼中添加統計部分:

    char buf = inputFile.get();
    // 下面直接用字符的ascii碼作為索引,一個ascii碼占一個字節,共256種可能
    CharNode nodeArr[256] = { {0,0} };
    while (inputFile.good())
    {
        cout << buf;
        nodeArr[buf].character = buf;
        nodeArr[buf].frequency++;
        buf = inputFile.get();
    }

    cout << endl << endl;
    for (int i = 0; i < 256; i++)
    {
        if (nodeArr[i].frequency > 0)
        {
            cout << "Node " << i << ": [" << nodeArr[i].character << ", " << nodeArr[i].frequency << "]" << endl;
        }
    }

輸出如下(Node 10那塊換行是因為,ascii中10對應的是“換行”):
技術分享圖片

3. 根據頻次對字符排序:

首先添加幾個需要用到的庫:

#include <queue>
#include <vector>
#include <string>

排序用到了priority_queue重載運算符 方面的知識,對此不了解的可以看以下幾篇講解:

http://blog.csdn.net/keshacookie/article/details/19612355
http://blog.csdn.net/xiaoquantouer/article/details/52015928
https://www.cnblogs.com/zhaoheng/p/4513185.html

然後定義一個哈夫曼樹節點,並重載比較運算符:

// 哈夫曼樹節點
struct MinHeapNode
{
    char data;                  //字符
    unsigned freq;              //頻次(權值)
    MinHeapNode *left, *right;  //左右子樹
    MinHeapNode(char data, unsigned freq)   //構造函數
    {
        left = right = NULL;
        this->data = data;
        this->freq = freq;
    }
};
typedef MinHeapNode MinHeapNode;

struct compare
{
    // 重載()運算符,定義youxai
    bool operator()(MinHeapNode* l, MinHeapNode* r)
    {
        // 由小到大排列采用">"號,如果要由大到小排列,則采用"<"號
        return (l->freq > r->freq);
    }
};

將節點放入到優先級隊列中(會自動排序):

    // 創建優先級隊列,由小到大排列
    priority_queue<MinHeapNode*, vector<MinHeapNode*>, compare> minHeap;
    for (int i = 0; i < 256; i++)
    {
        if (nodeArr[i].frequency > 0)
        {
            minHeap.push(new MinHeapNode(nodeArr[i].character, nodeArr[i].frequency));
        }
    }

4. 構建哈夫曼樹,並進行哈夫曼編碼:

用排好的隊列構建哈夫曼樹:

    //用排好的隊列實現哈夫曼樹
    MinHeapNode *leftNode = NULL, *rightNode = NULL, *topNode = NULL;
    while (!minHeap.size != 1)
    {
        leftNode = minHeap.top();
        minHeap.pop();

        rightNode = minHeap.top();
        minHeap.pop();

        // 將合並節點的data設置為-1
        topNode = new MinHeapNode(-1, leftNode->freq + rightNode->freq);
        topNode->left = leftNode;
        topNode->right = rightNode;
        minHeap.push(topNode);
    }

新建函數,對構建好的哈夫曼樹進行哈夫曼編碼:

static void get_huffman_code(MinHeapNode *root, string code)
{
    if (!root)
    {
        return;
    }
    // 由於之前設置了合並節點的data為-1,因此檢測到不是-1時,即為葉子結點,進行輸出
    if (root->data != -1)
    {
        cout << root->data << ": " << code << endl;
    }

    // 遞歸調用
    get_huffman_code(root->left, code + "0");
    get_huffman_code(root->right, code + "1");
}

最後在主函數中調用此函數即可:

get_huffman_code(topNode, "");

編譯運行程序,輸出如下:
技術分享圖片

由於文本不同,每個字符出現的頻率不同,因此對於不同文本的編碼也不同。這就要求在解碼的時候,也需要有編碼表才行。

完整程序如下:

#include "stdafx.h"
#include <iostream>
#include <fstream>
#include <queue>
#include <vector>
#include <string>

using namespace std;

// 每一個符號(字母和標點等)定義為一個結構體,包括字符和出現頻次
typedef struct
{
    unsigned char   character;
    unsigned int    frequency;
} CharNode;

// 哈夫曼樹節點
struct MinHeapNode
{
    char data;                  //字符
    unsigned freq;              //頻次(權值)
    MinHeapNode *left, *right;  //左右子樹
    MinHeapNode(char data, unsigned freq)   //構造函數
    {
        left = right = NULL;
        this->data = data;
        this->freq = freq;
    }
};
typedef MinHeapNode MinHeapNode;

struct compare
{
    // 重載()運算符來定義優先級
    bool operator()(MinHeapNode* l, MinHeapNode* r)
    {
        // 由小到大排列采用">"號,如果要由大到小排列,則采用"<"號
        return (l->freq > r->freq);
    }
};


static bool open_input_file(ifstream &input, const char *inputFileName)
{
    input.open(inputFileName);
    if (!input.is_open())
    {
        return false;
    }
    return true;
}

static void get_huffman_code(MinHeapNode *root, string code)
{
    if (!root)
    {
        return;
    }
    // 由於之前設置了合並節點的data為-1,因此檢測到不是-1時,即為葉子結點
    if (root->data != -1)
    {
        cout << root->data << ": " << code << endl;
    }

    // 遞歸調用
    get_huffman_code(root->left, code + "0");
    get_huffman_code(root->right, code + "1");
}


int _tmain(int argc, _TCHAR* argv[])
{
    ifstream inputFile;
    if (!open_input_file(inputFile, "input.txt"))
    {
        cout << "Error: opening input file failed!" << endl;
        return -1;
    }

    char buf = inputFile.get();
    // 下面直接用字符的ascii碼作為索引,一個ascii碼占一個字節,共256種可能
    CharNode nodeArr[256] = { {0,0} };
    while (inputFile.good())
    {
        cout << buf;
        nodeArr[buf].character = buf;
        nodeArr[buf].frequency++;
        buf = inputFile.get();
    }
    cout << endl << endl;

    // 創建優先級隊列,由小到大排列
    priority_queue<MinHeapNode*, vector<MinHeapNode*>, compare> minHeap;
    for (int i = 0; i < 256; i++)
    {
        if (nodeArr[i].frequency > 0)
        {
            cout << "Node " << i << ": [" << nodeArr[i].character << ", " << nodeArr[i].frequency << "]" << endl;
            minHeap.push(new MinHeapNode(nodeArr[i].character, nodeArr[i].frequency));
        }
    }

    //用排好的隊列實現哈夫曼樹
    MinHeapNode *leftNode = NULL, *rightNode = NULL, *topNode = NULL;
    while (minHeap.size() != 1)
    {
        leftNode = minHeap.top();
        minHeap.pop();

        rightNode = minHeap.top();
        minHeap.pop();

        // 將合並節點的data設置為-1
        topNode = new MinHeapNode(-1, leftNode->freq + rightNode->freq);
        topNode->left = leftNode;
        topNode->right = rightNode;
        minHeap.push(topNode);
    }

    get_huffman_code(topNode, "");

    inputFile.close();

    return 0;
}

【視頻編解碼·學習筆記】7. 熵編碼算法:基礎知識 & 哈夫曼編碼