數據結構(二十) -- C語言版 -- 樹 - 霍夫曼樹(哈夫曼樹、赫夫曼樹、最優二叉樹)、霍夫曼編碼

零、讀前說明

  • 本文中所有設計的代碼均通過測試,並且在功能性方面均實現應有的功能。
  • 設計的代碼並非全部公開,部分無關緊要代碼並沒有貼出來。
  • 如果你也對此感興趣、也想測試源碼的話,可以私聊我,非常歡迎一起探討學習。
  • 由於時間、水平、精力有限,文中難免會出現不準確、甚至錯誤的地方,也很歡迎大佬看見的話批評指正。
  • 嘻嘻。。。。 。。。。。。。。收!

一、概述

  漂亮國數學家霍夫曼(David Huffman),也稱赫夫曼、哈夫曼等。他在1952年發明了霍夫曼編碼,所以就將在編碼中用到的特殊的二叉樹稱爲霍夫曼樹,編碼方式稱爲霍夫曼編碼。膜拜!

  在瞭解之前,我們先來看看從小就被支配的考試的分數,往往因爲那麼幾分經常喜提男女混合雙打套餐一份。在我們學習編程不久,也會有這樣一個簡單的代碼來練習手法,每次編寫這樣的程序都會想到:爲什麼 60 分才及格呢 ??(͡° ͜ʖ ͡°)

if(score < 60) printf("不及格\n");
else if(score < 70) printf("及格\n");
else if(score < 80) printf("中等\n");
else if(score < 90) printf("良好\n");
else printf("優秀\n");

  程序運行的結果肯定是沒有任何問題的,每次運行出正確的結果,心裏的自豪感爆棚呀有沒有!! ≧◠◡◠≦✌
  

圖1.1 成績分佈示意圖

  
   但是現在學習了數據結構與算法之後,我們發現了端倪:通常學生的成績呈現正態分佈,也就是說 70 - 80 分的學生佔大多數,而少於 60 分或者多於 90 分畢竟是少數,所以對於 70 - 80 分的這個分支,通常需要判斷三次才能到,那麼消耗的時間也就也多,那我們就會發現,這個程序運行的效率有問題,強迫症告訴我,搞他!!!

  那麼既然這樣的話, 我們來進行簡單的計算一下,假設成績的分佈的佔比爲下表所示這樣。

表1.1 分數佔比分佈表
分 數 0 ~ 59 60 ~ 69 70 ~ 79 80 ~ 89 90 ~ 100
佔 比 5% 15% 40% 30% 10%

  假設有100個學生,那麼根據上面的程序總共需要判斷的次數爲(佔比 乘以 判斷次數,且注意最後一個 else 不佔次數):

5 * 1 + 15 * 2 + 40 * 3 + 30 * 4 + 10 * 4 = 315 (次)
  

  那麼根據上面表格中所顯示的比例,將上面的程序的分支簡單的修改一下,將出現頻繁的分支往前面移動,出現不多的往後面移動,那麼可以將上面的圖可以修改爲下圖這樣。

圖1.1 修改和的成績分佈示意圖

  
  那麼有100個學生的話程序總共需要判斷的次數爲:

5 * 3 + 15 * 3 + 40 * 2 + 30 * 2 + 10 * 2 = 220 (次)

  
  明顯的,判斷的次數有明顯的提升,那麼上面修改後的圖示就是霍夫曼樹,也稱爲最優二叉樹

二、霍夫曼樹

2.1、基本說明

  前面已經對霍夫曼樹有了最簡單的感覺,那麼首先說明:

  路勁:從樹中一個節點到另一個節點之間的分支構成兩個節點之間的路徑
  路勁長度:路勁上分支的數目
  帶權路勁長度:從該節點到根節點之間的路勁長度與節點上權的乘積
  樹的帶權路勁長度:樹中所有葉子節點的帶權路勁長度之和,通常記爲 :WPL=i=0nwiliWPL = \sum_{i=0}^n w_il_i

  假設有 nn 個權值 {w1w_1w2w_2,…,wnw_n} ,試構造一個有 nn 個葉子節點的二叉樹,每個葉子節點的權 wiw_i ,則其中帶權路勁長度 WPLWPL 最小的二叉樹稱做最優二叉樹霍夫量樹

  所以說,上面提到的兩種風格的樹的形狀,其進行 if 判斷的次數,即爲其對應的WPLWPL,明顯地第二種形狀的樹其WPLWPL最小。

2.2、構建霍夫曼樹

  既然霍夫曼樹這麼好,那麼應該怎麼構建這個霍夫曼樹呢。霍夫曼最早給出了一個帶有一般規律的算法,一般稱爲霍夫曼算法。描述如下:

  1、根據給定的 nn 個權值{w1w_1w2w_2,…,wnw_n} 構成 nn 個二叉樹的集合 FF ={T1T_1T2T_2,…,TnT_n}, 其中每個二叉樹 T,中只有一個帶權爲 wiw_i 的根結點,其左右子樹均爲空
  2、在 F 中選取兩個根結點的權值最小的樹作爲左右子樹構造一個新的二叉樹, 置新的二叉樹的根結點的權值爲其左右子樹上根結點的權值之和
  3、在 F 中刪除這兩個樹,同時將新得到的二叉樹加入 F
  4、重複 2、3 步驟,直到 F 只含一個樹爲止,這個樹便是霍夫曼樹

(以上摘自《大話數據結構》P145)

  
  看完上面的總覺得可以了,但是大腦卻一個勁的說不行,那我們用下面的一組圖圖來簡單的描述一下上面的總結。

  假設有一個五二叉樹在 A、B、C、D、E 構成的森林,權值分別爲在 5、15、40、30、10 ,用這個創建一個霍夫曼樹。

圖2.1 二叉樹集合示意圖

  
  1、取上面二叉樹集合中兩個權值最小的葉子節點組成一個新的二叉樹,並且將權值最小的節點最爲新二叉樹(下圖中節點 N1N_1 )的左孩子。也就是節點 A (權值爲 5 )爲新節點的左孩子,節點 E (權值爲 10 )爲新節點的右孩子。新節點的權值則爲兩個孩子的權值的和10+5 ),如下圖所示。

圖2.2 組建新節點示意圖

  
  2、用 N1N_1 替換節點 A 和節點 E ,爲了統一插入,將新節點 N1N_1 插入到集合的前面,如下圖所示。

圖2.3 二叉樹集合分佈示意圖

  
  3、重複上面步驟2,再在集合中取一個權值最小的節點 B (權值爲 15 )與新節點 N1N_1 組成新的節點 N2N_2 (權值爲 15+15 ),如下圖所示。

圖2.4 組建新節點示意圖

  
  4、將新節點 N2N_2 替換節點 N1N_1 與節點 B ,此時集合中還存在三個二叉樹。分別爲 {N2N_2、C、D}
  5、重複上面步驟2,再在集合中取一個權值最小的節點 D (權值爲 30 )與新節點 N2N_2 組成新的節點 N3N_3 (權值爲 30+30 ),如下圖所示。

圖2.5 二叉樹組建示意圖

  
  6、將新節點 N3N_3 替換節點 N2N_2 與節點 D,此時集合中還存在兩個二叉樹。分別爲 {N2N_2 、C}
  7、重複上面步驟2,再在集合中取一個權值最小的節點 C(權值爲 40)與新節點 N3N_3 組成新的節點 N4N_4 ,因爲節點 C 的權值( 40)小於節點 N3N_3 的權值( 60 ),所以節點 N3N_3 爲新節點 N4N_4 的右孩子。新節點 N4N_4 的權值爲 40+60 ,如下圖所示。

圖2.6 霍夫曼樹示意圖

  
  8、此時集合中就剩下一各二叉樹 N4N_4 了,所以,霍夫曼樹的創建完成了。

  此時,可以計算出來此二叉樹的 WPLWPL

40 * 1 + 30 * 2 + 15 * 3 + 10 * 4 + 5 * 4 = 205
  

  顯然此時得到的值爲 WPLWPL = 205,比之前我們自行做修改的二叉樹的還要小,顯然此時構造出來的二叉樹纔是最優的霍夫曼樹

2.3、霍夫曼樹的存儲結構

  根據上面的描述,如果樹的集合中有 nn 個節點,那麼我就需要 2n12n-1 個空間用來保存創建的霍夫曼樹中各個節點的信息。也就是需要創建一個數組 huffmanTree[2n12n-1]用來保存霍夫曼樹,數組的元素的節點結構如下所示。

圖2.7 霍夫曼樹存儲結構示意圖

  其中:
    weight:權值域,保存該節點的權值
    lchild:指針域,節點的左孩子節點在數組中的下標
    rchild:指針域,節點的右孩子節點在數組中的下標
    parent:指針域,節點的雙親節點在數組中的下標

三、霍夫曼樹的應用 — 霍夫曼編碼

3.1、概述

  霍夫曼樹的研究是爲了當時在進行遠距離數據傳輸的的最優化的問題,並且在現如今龐大的信息面前,數據的壓縮顯的尤爲主要。而霍夫曼編碼是首個使用的壓縮編碼方案。首先我們瞭解幾個簡單的概念。

  編  碼:給每個對象標記一個二進制位串來表示一組對象,比如ASCII,指令系統等
  定長編碼:表示一組對象的二進制位串的長度相等
  變長編碼:表示一組對象的二進制位串的長度不相等,可以根據整體出現的頻率來調節
  前綴編碼:一組編碼中任一編碼都不是其他任何一個編碼的前綴

  前綴編碼保證了在解碼時不會出現歧義,而霍夫曼編碼就是一種前綴編碼。

   比如一組字符串 “hello world”,如果採用ASCII進行編碼,那麼既可以表示爲下表這樣(包含空格):

表3.1 字符串的ASCII表示表
字符串 h e l l o (空格) w o r l d
十六進制表示 0x68 0x65 0x6C 0x6C 0x6F 0x20 0x77 0x6F 0x72 0x6C 0x64
二進制表示 0110 1000 0110 0101 0110 1100 0110 1100 0110 1111 0010 0000 0111 0111 0110 1111 0111 0010 0110 1100 0110 0100

   顯然,想要保存或者傳輸這麼一個字符串的話,至少需要 12 個字節(字符串的結束符’\0’),也就是需要 12*8bit 來保存或者傳輸。

   那麼既然提到了霍夫曼編碼,那麼肯定霍夫曼編碼可以解決這個佔用多、效率低的問題了。

   首先各個字母出現的次數可以理解爲其權值,所以上述字符串中各個字母的權值可以表示爲下面這樣。

表3.2 字母權值顯示錶
字符串 h e l o (空格) w r d
權 值 1 1 3 2 1 1 1 1

   然後根據前面描述的創建霍夫曼樹的步驟(點我查看構造霍夫曼樹的詳情),我們可以創建出來字符串 “hello world” 的最優的霍夫曼樹,如下圖左邊所示。

在這裏插入圖片描述

圖3.1 霍夫曼樹示意圖

  
   然後在上圖左邊所示的霍夫曼樹中,將左分支上的原本表示權值的數值修改爲表示路徑的 0 ,將右分支上原本表示權值的數據修改成表示路徑的 1,那麼現實效果可以如上圖右邊所示。

表3.3 字母霍夫曼編碼顯示錶
字符串 h e l l o (空格) w o r l d
霍夫曼編碼 1111 110 1111 111 0 0 10 110 1111 10 10 1111 0 0 1110

   由此可見,數據的存儲或者傳輸的空間大大的縮小,那麼隨着字符的增加和多自負權重的不同,這種壓縮會更加的顯示出其優勢。

3.2、霍夫曼編碼的代碼實現

   那麼根據上面的描述,霍夫曼樹、霍夫曼編碼的創建的實現代碼可以如下編寫。

/**
 *  功  能:
 *      創建一個霍夫曼樹
 *  參  數:
 *      weight :保存權值的數組
 *      num    :權值數組的長度,也就是權值的個數 
 *  返回值:
 *      成功 :創建成功的霍夫曼樹的首地址
 *      失敗 :NULL
 **/
huffmanTree *Create_huffmanTree(unsigned int *weight, unsigned int num)
{
    huffmanTree *hTree = NULL;
    if (weight == NULL || num <= 1) // 如果只有一個編碼就相當於0
        goto END;

    hTree = (huffmanTree *)malloc((2 * num - 1 + 1) * sizeof(htNode)); // 0號下標預留。用來表示初始化的狀態,所以要在原來的基礎上加1
    if (hTree == NULL)
        goto END;

    // 初始化哈夫曼樹中的所有結點,均初始化爲0
    memset(hTree, 0, (2 * num - 1 + 1) * sizeof(htNode));
    // 將權值賦值成傳入的權值,並且從數組的第二個位置開始,i=1
    for (unsigned int i = 1; i <= num; i++)
        (hTree + i)->weight = *(weight + i - 1);

    // 構建哈夫曼樹,將新創建的節點在原本節點的後邊,從num+1開始
    for (unsigned int offset = num + 1; offset < 2 * num; offset++)
    {
        unsigned int index1, index2;
        select_minimum_index(hTree, offset - 1, &index1, &index2); // 獲取權值最小的節點的下標

        // printf("index1 = %d, index2 = %d, hTree[1] = %d, hTree[2] = %d\n",
        //        index1, index2, hTree[index1].weight, hTree[index2].weight);

        (hTree + offset)->weight = (hTree + index1)->weight +
                                   (hTree + index2)->weight; // 將權值最小的兩個節點的權值相加醉成新節點的權值
        (hTree + index1)->parent = offset;                   // 權值最小的節點的雙親結點爲此新節點
        (hTree + index2)->parent = offset;                   // 值次小的節點的雙親結點爲此新節點
        (hTree + offset)->lchild = index1;                   // 此新節點的左孩子爲權值最小的節點
        (hTree + offset)->rchild = index2;                   // 此新節點的右孩子爲權值次小的節點
    }

END:
    return hTree;
}

/**
 *  功  能:
 *       查詢/計算在特定霍夫曼樹下的對應圈權值的霍夫曼編碼
 *  參  數:
 *      htree :參考的霍夫曼樹
 *      nums  :原本權值的個數
 *      weight:權值
 *  返回值:
 *      成功:返回權值weight對應的編碼
 *      失敗:NULL
 **/
huffmanCode *Create_HuffmanCode_One(huffmanTree *htree, unsigned int nums, unsigned int weight)
{
    huffmanCode *hCode = NULL, *tmpCode = NULL;
    unsigned int i = 0, index = 0, parent = 0;

    if (htree == NULL || weight < 1)
        goto END;

    // 找到權值匹配的數組的下標
    while (htree[i].weight != weight && i <= nums)
        i++;
    if (i > nums) // 如果成立,額說明沒有找到對應的權值,是爲假權值
        goto END;

    tmpCode = (char *)malloc(sizeof(char) * nums);
    if (tmpCode == NULL)
        goto END;

    memset(tmpCode, 0, sizeof(char) * nums);
    // 霍夫曼編碼的起始點,一般情況來說,最長的編碼的長度爲權值個數-1
    index = nums - 1;
    //從葉子到根結點求編碼
    parent = (htree + i)->parent;
    while (parent != 0)
    {
        if ((htree + parent)->lchild == (unsigned int)i) //從右到左的順序編碼入數組內
            tmpCode[--index] = '0';                      //左分支標0
        else
            tmpCode[--index] = '1'; //右分支標1

        i = parent;
        parent = (htree + parent)->parent; //由雙親節點向霍夫曼樹的根節點移動
    }

    hCode = (char *)malloc((nums - index) * sizeof(char)); //字符串,需要以'\0'爲結束
    if (hCode == NULL)
        goto END;

    memset(hCode, 0, (nums - index) * sizeof(char));
    strcpy(hCode, &tmpCode[index]);

END:
    if (tmpCode != NULL)
        free(tmpCode);
    tmpCode = NULL;

    return hCode;
}

/**
 *  功  能:
 *      霍夫曼樹的數組權值最小兩個節點的下標
 *  參  數:
 *      htree  :輸入,霍夫曼樹
 *      num    :輸入,霍夫曼樹數組中有效節點的個數
 *      index1 :輸出,權值最小的節點的下標
 *      index2 :輸出,權值次小的節點的下標
 *  返回值:
 *      無
 **/
static void select_minimum_index(huffmanTree *htree, unsigned int num, unsigned int *index1, unsigned int *index2)
{
    unsigned int i = 1;
    // 記錄最小權值所在的下標
    unsigned int indexMin;
    if (htree == NULL || index1 == NULL || index2 == NULL)
        goto END;

    // 遍歷目前全部節點,找出最前面第一個沒有被構建的節點
    while (i <= num && (htree + i)->parent != 0)
        i++;

    indexMin = i;

    //繼續遍歷全部結點,找出權值最小的單節點
    while (i <= num)
    {
        // 如果節點沒有被構建,並且此節點的的權值比 下標爲目前記錄的下標的節點的權值小
        if ((htree + i)->parent == 0 && (htree + i)->weight < (htree + indexMin)->weight)
            indexMin = i; // 找到了最小權值的節點,下標爲i
        i++;
    }
    *index1 = indexMin; // 最小的節點已經找到了,下標爲 indexMin

    // 開始查找次小權值的下標
    i = 1;
    while (i <= num)
    {
        // 找出下一個沒有被構建的節點,且沒有被 index1 指向
        if ((htree + i)->parent == 0 && i != (*index1))
            break;
        i++;
    }
    indexMin = i;

    // 繼續遍歷全部結點,找到權值次小的那一個
    i = 1;
    while (i <= num)
    {
        if ((htree + i)->parent == 0 && i != (*index1))
        {
            // 如果此結點的權值比 indexMin 位置的節點的權值小
            if ((htree + i)->weight < (htree + indexMin)->weight)
                indexMin = i;
        }
        i++;
    }
    // 次小的節點已經找到了,下標爲 indexMin
    *index2 = indexMin;

END:
    return;
}

3.3、測試案例及其運行效果

   測試案例就是主函數實現的代碼,主要代碼如下。

#include "../src/huffman/huffman.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, const char *argv[])
{
    int ret = 0;
    char chars[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', '\0'};
    unsigned int weight[] = {5, 7, 2, 4, 1, 9, 8};
    unsigned char length = sizeof(weight) / sizeof(int);

    /* 根據已知的權重進行編碼,解碼 */
    // 創建霍夫曼樹
    huffmanTree *htree = fhuffmanTree.buildHTree(weight, length);
    // 打印輸出霍夫曼樹的關係表格
    fhuffmanTree.printHTree(htree, 2 * length);
    // 創建霍夫曼編碼
    huffmanCode **hcode = fhuffmanTree.getHCode(htree, length);
    // 打印輸出霍夫曼編碼
    fhuffmanTree.printHCode(hcode);

    /* 根據已知的字符串進行編碼 */
    char *string = (char *)"ADZFBCD";
    char *stringCode = NULL;
    fhuffmanTree.toHcode(hcode, chars, length, string, &stringCode);
    printf("The input string '%s' Encoded by Huffman is : \n\n\t%s\n\n", string, stringCode);

    /* 根據已知的字符串進行解碼 */
    char *coding = (char *)"110001101110001001";
    char *codeString = NULL;
    fhuffmanTree.toString(htree, chars, length, coding, &codeString);
    printf("The input coding '%s' Decoded by Huffman is : \n\n\t%s\n\n", coding, codeString);

    fhuffmanTree.destoryHTree(htree);
    fhuffmanTree.destoryHCode(hcode);

    free(stringCode);
    stringCode = NULL;
    free(codeString);
    codeString = NULL;

    printf("\nsystem exited with return code %d\n", ret);

    return ret;
}

   工程管理使用常見的 cmake 進行管理,項目工程結構如下圖左上角所示,比較清楚不再贅述。
   項目創建、編譯、運行在下圖中均已經顯示明白,不再贅述。
   具體的測試效果如下圖所示。

圖3.2 霍夫曼運行效果示意圖

  
  好啦,廢話不多說,總結寫作不易,如果你喜歡這篇文章或者對你有用,請動動你發財的小手手幫忙點個贊,當然 關注一波 那就更好了,好啦,就到這兒了,麼麼噠(*  ̄3)(ε ̄ *)。

上一篇:數據結構(十九) – C語言版 – 樹 - 樹、森林、二叉樹的江湖愛恨情仇、相互轉換
下一篇:

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