數據結構與算法 -- 哈夫曼樹&哈夫曼編碼

前言

  上一篇瞭解學習了線索化二叉樹的一些知識,這一篇,對哈夫曼樹哈夫曼編碼來做一個瞭解學習。

首先,我們先看一個經典的問題,

等級 優秀 良好 中等 及格 不及格
考分 90 <= 分數 <= 100 80 <= 分數 < 90 70 <= 分數 < 80 60 <= 分數 < 70 0<=分數<60

一般我們會這樣判斷:

那麼,在二叉樹中是這樣的

一般情況下,每個班的成績及格不及格有一定的分佈概率

那這樣就會造成對中等良好的成績判斷時,會經過很多步,當成績比較多時,就會出現效率問題。

那麼怎麼能優化解決一下這個問題呢?其實前輩們已經解決了這個問題,我們接下來學習一下

1. 哈夫曼樹

在上面的成績算法,

節點D的路徑爲4

樹的總路徑爲:1 + 1 + 2 + 2 + 3 + 3 + 4 + 4 = 20

根據上面成績的分佈這棵樹的WPL爲:WPL = 1 * 5 + 2 * 15 + 3 * 40 + 4 * 30 + 4 * 10 = 315

假如我們對成績的判斷順序進行如下的調整:

那麼節點D的路徑長度爲2

樹的總路徑爲1 + 2 + 3 + 3 + 2 + 1 + 2 + 2 = 16

根據上面成績的分佈這棵樹的WPL爲:WPL = 5 * 3 + 3 * 15 + 2 * 40 + 2 * 30 + 2 * 10 = 220

第二種情況,明顯比第一種情況更優。

那麼怎麼構建一個最優的樹呢,我們接下來看一下哈夫曼樹的構建。

假設:A B C D E的權重如下:

A B C D E
5 15 40 30 10

首先,找到權重最小的兩個AE,組成二叉樹:

此時N1的權重相當於 15,依次找到 最小的 B,組建二叉樹:

此時N2的權重相當於 30,依次找到 最小的 D,組建二叉樹:

此時N3的權重相當於 60,依次找到 最小的 C,組建二叉樹(小的在左子樹位置):

該二叉樹的WPL40 * 1 + 30 * 2 + 15 * 3 + 10 * 4 + 5 * 4 = 205

這種排列組成的二叉樹的WPL,明顯小於上面兩種,以這種方式排列的二叉樹即爲哈夫曼樹

小結:
先對給出的節點的權重排序
依次找出最小的兩個節點,組成二叉樹,小的在左子樹位置

2. 哈夫曼編碼

假設如下:

字符 A B C D E F
權重 27 8 15 15 30 5
編碼 000 001 010 011 100 101

如上表中,每個字符的編碼位數都是一樣的,但是每個字符出現的權重是不同的,這樣就會造成權重低的字符的空間浪費

我們可以利用 哈夫曼樹 找到最佳的二叉樹排列,然後生成 哈夫曼編碼 來解決這個問題。

首先,對上面的字符生成哈夫曼樹

  • 找到權重最小的 BF,組成二叉樹(小的在左邊)

  • 找到 C,組成二叉樹(小的在左邊)

  • 依次查找

最終的 哈夫曼樹 如下:

然後,開始哈夫曼編碼

哈夫曼編碼的原則:左子樹上的權重,替換爲 0 ,右子樹權重替換爲 1

然後從根節點,到各個葉子節點,組成每個葉子節點的編碼,如下:

字符 A B C D E F
權重 27 8 15 15 30 5
編碼 01 1001 101 00 11 1000

那麼對於同一個字符串BADCADFEED的新舊二進制編碼如下:

舊編碼: 001 000 011 010 000 011 101 100 100 011 (共30個字符)

新編碼: 1001 01 00 101 01 00 1001 11 11 00 (共25個字符)

可以明顯看出,新編碼(哈夫曼編碼)比舊編碼,有更高的存儲利用率。

小結:
先生成哈夫曼樹
從根節點開始,左子樹的權值爲0,右子樹的權值 爲 1
從根節點開始,到葉子節點,組成葉子節點的編碼

3. 哈夫曼樹 & 哈夫曼編碼 的實現

可以定義哈夫曼樹(順序)的節點如下:

const int MaxValue = 10000;//初始設定的權值最大值
const int MaxBit = 4;//初始設定的最大編碼位數
const int MaxN = 10;//初始設定的最大結點個數

// 定義節點
typedef struct HaffNode{
    int weight;     // 權值
    int flag;       // 標記該節點是否合併到哈夫曼樹 0:未合併,1:合併
    int parent;     // 雙親節點下標
    int leftChild;  // 左孩子下標
    int rightChild; // 右孩子下標
}HaffNode;

// 存放哈夫曼編碼的數據元素結構
typedef struct Code {
    int bit[MaxBit];  // 數組
    int start;        // 編碼的起始下標
    int weight;       // 字符的權重
}Code;

// 構建哈夫曼樹
void Haffman(int weight[],int n,HaffNode *haffTree){
    int j, m1, m2, x1, x2;
    // 哈夫曼樹初始化
    // n個葉子節點,有(2n - 1)個節點
    for (int i = 0; i < 2*n - 1; i++) {
        if (i < n) {
            haffTree[i].weight = weight[i]; // 葉子節點賦值權重
        } else {
            haffTree[i].weight = 0;
        }
        haffTree[i].parent     = 0;         // 無雙親,下標爲0
        haffTree[i].flag       = 0;         // 未合併到哈夫曼樹,爲0
        haffTree[i].leftChild  = -1;        // 無左右孩子,給特殊在-1
        haffTree[i].rightChild = -1;
    }
    // 構建哈夫曼樹
    for (int i = 0; i < n - 1; i++) {
        m1 = m2 = MaxValue;
        x1 = x2 = 0;
        // 循環找到所有權重中最小的兩個
        for (j = 0; j < n + i; j++) {
            if (haffTree[j].weight < m1 && haffTree[j].flag == 0) {
                m2 = m1;
                x2 = x1;
                m1 = haffTree[j].weight;
                x1 = j;
            } else if(haffTree[j].weight < m2 && haffTree[j].flag == 0){
                m2 = haffTree[j].weight;
                x2 = j;
            }
        }
        
        // 將找到權重最小的兩個子樹合併爲一個子樹
        haffTree[x1].parent = n + i;
        haffTree[x2].parent = n + i;
        
        haffTree[x1].flag   = 1;
        haffTree[x2].flag   = 1;
        // 修改合併後的子樹(n+i)的權重
        haffTree[n+i].weight = haffTree[x1].weight + haffTree[x2].weight;
        // 修改合併後的子樹(n+i)的左右子樹
        haffTree[n+i].leftChild  = x1;
        haffTree[n+i].rightChild = x2;
    }
}

// 哈夫曼編碼
void HaffmanCode(HaffNode haffTree[], int n, Code haffCode[])
{
    // 創建一個結點
    Code *cd = (Code *)malloc(sizeof(Code));
    
    int child, parent;
    
    // 求n個結點的哈夫曼編碼
    for (int i = 0; i < n; i++) {
        // 從0開始計數
        cd->start = 0;
        // 取得編碼對應權值的字符
        cd->weight = haffTree[i].weight;
        // 當前葉子節點爲 i
        child = i;
        // 根據葉子節點找到雙親節點
        parent = haffTree[i].parent;
        
        // 由葉子節點向上到根節點
        while (parent != 0) {
            if (haffTree[parent].leftChild == child) {
                cd->bit[cd->start] = 0; //左孩子結點編碼0
            } else {
                cd->bit[cd->start] = 1; //右孩子結點編碼1
            }
            // 編碼自增
            cd->start++;
            // 修改子節點爲當前雙親節點(向上查找)
            child = parent;
            // 根據子節點找到雙親
            parent = haffTree[child].parent;
        }
        
        // 此時的編碼是倒序的,翻轉 011 start = 3
        int temp = 0;
        for (int j = cd->start - 1; j >= 0; j--) {
            temp = cd->start-j-1;
            haffCode[i].bit[temp] = cd->bit[j];
        }
        
        //把cd中的數據賦值到haffCode[i]中.
        //保存好haffCode 的起始位以及權值;
        haffCode[i].start = cd->start;
        //保存編碼對應的權值
        haffCode[i].weight = cd->weight;
    }
}

// 調用
void show{
        int i, j, n = 4, m = 0;
        
        //權值
        int weight[] = {2,4,5,7};
        
        //初始化哈夫曼樹, 哈夫曼編碼
        HaffNode *myHaffTree = malloc(sizeof(HaffNode)*2*n-1);
        Code *myHaffCode = malloc(sizeof(Code)*n);
        
        //當前n > MaxN,表示超界. 無法處理.
        if (n>MaxN)
        {
            printf("定義的n越界,修改MaxN!");
            exit(0);
        }
        
        //1. 構建哈夫曼樹
        Haffman(weight, n, myHaffTree);
        //2.根據哈夫曼樹得到哈夫曼編碼
        HaffmanCode(myHaffTree, n, myHaffCode);
        //3.
        for (i = 0; i<n; i++)
        {
            printf("Weight = %d\n",myHaffCode[i].weight);
            for (j = 0; j<myHaffCode[i].start; j++)
                printf("%d",myHaffCode[i].bit[j]);
            m = m + myHaffCode[i].weight*myHaffCode[i].start;
             printf("\n");
        }
        printf("Huffman's WPS is:%d\n",m);
}


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