1. 程式人生 > 實用技巧 >哈夫曼樹

哈夫曼樹

哈夫曼樹

簡介

哈夫曼樹(Huffman Tree),又名:最優二叉樹,赫夫曼樹

其標準含義是:給定N個權值作為N個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度達到最小,稱這樣的二叉樹為最優二叉樹,也稱為哈夫曼樹。哈夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。

相關名詞

由於本篇存在一定的難度,因此在開始相關的學習之前,請讓我們來鞏固以下本文所涉及的名詞知識點。

a) 路徑:在一棵樹中,一個結點到另一個結點之間的通路,稱為路徑。

b) 路徑長度:在一條路徑中,每經過一個結點,路徑長度都要加 1 。例如在一棵樹中,規定根結點所在層數為1層,那麼從根結點到第 i 層結點的路徑長度為 i - 1 。

c) 結點的權:給每一個結點賦予一個新的數值,被稱為這個結點的權。

d) 結點的帶權路徑長度:指的是從根結點到該結點之間的路徑長度與該結點的權的乘積。

e) 樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和。通常記作 “WPL”。

構建哈夫曼樹

在構建哈夫曼樹時,只需要遵循一個原則,那就是權重越大的結點距離樹根越近。

因此,在構建過程中,有如下的規律:

首先,選出我們資料中最小的兩個資料,構建成二叉樹的左孩子和右孩子,而根的資料為兩者之和

其次,將剛才合成的資料作為右孩子,左孩子從未處理的資料中選出最小的一個,作為左孩子,他們的根同樣為左右孩子的權值和

不斷重複上述的步驟,直到將所有的資料全部處理完並構建出二叉樹,這棵二叉樹就是我們的哈夫曼樹。

如圖這顆哈夫曼樹的WPL值為:WPL= 8*1+ 6*2 + 1*3 + 4*3 = 273

哈弗曼樹中結點結構

構建哈夫曼樹時,首先需要確定樹中結點的構成。由於哈夫曼樹的構建是從葉子結點開始,不斷地構建新的父結點,直至樹根,所以結點中應包含指向父結點的指標。但是在使用哈夫曼樹時是從樹根開始,根據需求遍歷樹中的結點,因此每個結點需要有指向其左孩子和右孩子的指標。

所以,哈夫曼樹中結點構成用程式碼表示為:

//哈夫曼樹結點結構
typedef struct {
    int weight;//結點權重
    int
parent, left, right;//父結點、左孩子、右孩子在陣列中的位置下標 }HTNode, *HuffmanTree;

哈弗曼樹中的查詢演算法

構建哈夫曼樹時,需要每次根據各個結點的權重值,篩選出其中值最小的兩個結點,然後構建二叉樹。

查詢權重值最小的兩個結點的思想是:從樹組起始位置開始,首先找到兩個無父結點的結點(說明還未使用其構建成樹),然後和後續無父結點的結點依次做比較,有兩種情況需要考慮:

    • 如果比兩個結點中較小的那個還小,就保留這個結點,刪除原來較大的結點;
    • 如果介於兩個結點權重值之間,替換原來較大的結點;
//HT陣列中存放的哈夫曼樹,end表示HT陣列中存放結點的最終位置,s1和s2傳遞的是HT陣列中權重值最小的兩個結點在陣列中的位置
void Select(HuffmanTree HT, int end, int *s1, int *s2)
{
    int min1, min2;
    //遍歷陣列初始下標為 1
    int i = 1;
    //找到還沒構建樹的結點
    while(HT[i].parent != 0 && i <= end){
        i++;
    }
    min1 = HT[i].weight;
    *s1 = i;
   
    i++;
    while(HT[i].parent != 0 && i <= end){
        i++;
    }
    //對找到的兩個結點比較大小,min2為大的,min1為小的
    if(HT[i].weight < min1){
        min2 = min1;
        *s2 = *s1;
        min1 = HT[i].weight;
        *s1 = i;
    }else{
        min2 = HT[i].weight;
        *s2 = i;
    }
    //兩個結點和後續的所有未構建成樹的結點做比較
    for(int j=i+1; j <= end; j++)
    {
        //如果有父結點,直接跳過,進行下一個
        if(HT[j].parent != 0){
            continue;
        }
        //如果比最小的還小,將min2=min1,min1賦值新的結點的下標
        if(HT[j].weight < min1){
            min2 = min1;
            min1 = HT[j].weight;
            *s2 = *s1;
            *s1 = j;
        }
        //如果介於兩者之間,min2賦值為新的結點的位置下標
        else if(HT[j].weight >= min1 && HT[j].weight < min2){
            min2 = HT[j].weight;
            *s2 = j;
        }
    }
}

構建哈弗曼樹的程式碼實現

//HT為地址傳遞的儲存哈夫曼樹的陣列,w為儲存結點權重值的陣列,n為結點個數
void CreateHuffmanTree(HuffmanTree *HT, int *w, int n)
{
    if(n<=1) return; // 如果只有一個編碼就相當於0
    int m = 2*n-1; // 哈夫曼樹總節點數,n就是葉子結點
    *HT = (HuffmanTree) malloc((m+1) * sizeof(HTNode)); // 0號位置不用
    HuffmanTree p = *HT;
    // 初始化哈夫曼樹中的所有結點
    for(int i = 1; i <= n; i++)
    {
        (p+i)->weight = *(w+i-1);
        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }
    //從樹組的下標 n+1 開始初始化哈夫曼樹中除葉子結點外的結點
    for(int i = n+1; i <= m; i++)
    {
        (p+i)->weight = 0;
        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }
    //構建哈夫曼樹
    for(int i = n+1; i <= m; i++)
    {
        int s1, s2;
        Select(*HT, i-1, &s1, &s2);
        (*HT)[s1].parent = (*HT)[s2].parent = i;
        (*HT)[i].left = s1;
        (*HT)[i].right = s2;
        (*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
    }
}

哈夫曼編碼

哈夫曼編碼就是在哈夫曼樹的基礎上構建的,這種編碼方式最大的優點就是用最少的字元包含最多的資訊內容。

根據傳送資訊的內容,通過統計文字中相同字元的個數作為每個字元的權值,建立哈夫曼樹。對於樹中的每一個子樹,統一規定其左孩子標記為 0 ,右孩子標記為 1 。這樣,用到哪個字元時,從哈夫曼樹的根結點開始,依次寫出經過結點的標記,最終得到的就是該結點的哈夫曼編碼。

文字中字元出現的次數越多,在哈夫曼樹中的體現就是越接近樹根。編碼的長度越短。


  圖 3 哈夫曼編碼

如圖 3 所示,字元 a 用到的次數最多,其次是字元 b 。字元 a 在哈夫曼編碼是0,字元 b 編碼為10,字元 c 的編碼為110,字元 d 的編碼為111

使用程式求哈夫曼編碼有兩種方法:

  1. 從葉子結點一直找到根結點,逆向記錄途中經過的標記。例如,圖 3 中字元 c 的哈夫曼編碼從結點 c 開始一直找到根結點,結果為:0 1 1 ,所以字元 c 的哈夫曼編碼為:1 1 0(逆序輸出)。
  2. 從根結點出發,一直到葉子結點,記錄途中經過的標記。例如,求圖 3 中字元 c 的哈夫曼編碼,就從根結點開始,依次為:1 1 0。


採用方法 1 的實現程式碼為:

//HT為哈夫曼樹,HC為儲存結點哈夫曼編碼的二維動態陣列,n為結點的個數
void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){
    *HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
    char *cd = (char *)malloc(n*sizeof(char)); //存放結點哈夫曼編碼的字串陣列
    cd[n-1] = '\0';//字串結束符
   
    for(int i=1; i<=n; i++){
        //從葉子結點出發,得到的哈夫曼編碼是逆序的,需要在字串陣列中逆序存放
        int start = n-1;
        //當前結點在陣列中的位置
        int c = i;
        //當前結點的父結點在陣列中的位置
        int j = HT[i].parent;
        // 一直尋找到根結點
        while(j != 0){
            // 如果該結點是父結點的左孩子則對應路徑編碼為0,否則為右孩子編碼為1
            if(HT[j].left == c)
                cd[--start] = '0';
            else
                cd[--start] = '1';
            //以父結點為孩子結點,繼續朝樹根的方向遍歷
            c = j;
            j = HT[j].parent;
        }
        //跳出迴圈後,cd陣列中從下標 start 開始,存放的就是該結點的哈夫曼編碼
        (*HC)[i] = (char *)malloc((n-start)*sizeof(char));
        strcpy((*HC)[i], &cd[start]);
    }
    //使用malloc申請的cd動態陣列需要手動釋放
    free(cd);
}

採用第二種演算法的實現程式碼為:

//HT為哈夫曼樹,HC為儲存結點哈夫曼編碼的二維動態陣列,n為結點的個數
void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){
    *HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
    int m=2*n-1;
    int p=m;
    int cdlen=0;
    char *cd = (char *)malloc(n*sizeof(char));
    //將各個結點的權重用於記錄訪問結點的次數,首先初始化為0
    for (int i=1; i<=m; i++) {
        HT[i].weight=0;
    }
    //一開始 p 初始化為 m,也就是從樹根開始。一直到p為0
    while (p) {
        //如果當前結點一次沒有訪問,進入這個if語句
        if (HT[p].weight==0) {
            HT[p].weight=1;//重置訪問次數為1
            //如果有左孩子,則訪問左孩子,並且儲存走過的標記為0
            if (HT[p].left!=0) {
                p=HT[p].left;
                cd[cdlen++]='0';
            }
            //當前結點沒有左孩子,也沒有右孩子,說明為葉子結點,直接記錄哈夫曼編碼
            else if(HT[p].right==0){
                (*HC)[p]=(char*)malloc((cdlen+1)*sizeof(char));
                cd[cdlen]='\0';
                strcpy((*HC)[p], cd);
            }
        }
        //如果weight為1,說明訪問過一次,即是從其左孩子返回的
        else if(HT[p].weight==1){
            HT[p].weight=2;//設定訪問次數為2
            //如果有右孩子,遍歷右孩子,記錄標記值 1
            if (HT[p].right!=0) {
                p=HT[p].right;
                cd[cdlen++]='1';
            }
        }
        //如果訪問次數為 2,說明左右孩子都遍歷完了,返回父結點
        else{
            HT[p].weight=0;
            p=HT[p].parent;
            --cdlen;
        }
    }
}

完整程式碼

#include<stdlib.h>
#include<stdio.h>
#include<string.h>
//哈夫曼樹結點結構
typedef struct {
    int weight;//結點權重
    int parent, left, right;//父結點、左孩子、右孩子在陣列中的位置下標
}HTNode, *HuffmanTree;
//動態二維陣列,儲存哈夫曼編碼
typedef char ** HuffmanCode;
//HT陣列中存放的哈夫曼樹,end表示HT陣列中存放結點的最終位置,s1和s2傳遞的是HT陣列中權重值最小的兩個結點在陣列中的位置
void Select(HuffmanTree HT, int end, int *s1, int *s2)
{
    int min1, min2;
    //遍歷陣列初始下標為 1
    int i = 1;
    //找到還沒構建樹的結點
    while(HT[i].parent != 0 && i <= end){
        i++;
    }
    min1 = HT[i].weight;
    *s1 = i;
   
    i++;
    while(HT[i].parent != 0 && i <= end){
        i++;
    }
    //對找到的兩個結點比較大小,min2為大的,min1為小的
    if(HT[i].weight < min1){
        min2 = min1;
        *s2 = *s1;
        min1 = HT[i].weight;
        *s1 = i;
    }else{
        min2 = HT[i].weight;
        *s2 = i;
    }
    //兩個結點和後續的所有未構建成樹的結點做比較
    for(int j=i+1; j <= end; j++)
    {
        //如果有父結點,直接跳過,進行下一個
        if(HT[j].parent != 0){
            continue;
        }
        //如果比最小的還小,將min2=min1,min1賦值新的結點的下標
        if(HT[j].weight < min1){
            min2 = min1;
            min1 = HT[j].weight;
            *s2 = *s1;
            *s1 = j;
        }
        //如果介於兩者之間,min2賦值為新的結點的位置下標
        else if(HT[j].weight >= min1 && HT[j].weight < min2){
            min2 = HT[j].weight;
            *s2 = j;
        }
    }
}
//HT為地址傳遞的儲存哈夫曼樹的陣列,w為儲存結點權重值的陣列,n為結點個數
void CreateHuffmanTree(HuffmanTree *HT, int *w, int n)
{
    if(n<=1) return; // 如果只有一個編碼就相當於0
    int m = 2*n-1; // 哈夫曼樹總節點數,n就是葉子結點
    *HT = (HuffmanTree) malloc((m+1) * sizeof(HTNode)); // 0號位置不用
    HuffmanTree p = *HT;
    // 初始化哈夫曼樹中的所有結點
    for(int i = 1; i <= n; i++)
    {
        (p+i)->weight = *(w+i-1);
        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }
    //從樹組的下標 n+1 開始初始化哈夫曼樹中除葉子結點外的結點
    for(int i = n+1; i <= m; i++)
    {
        (p+i)->weight = 0;
        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }
    //構建哈夫曼樹
    for(int i = n+1; i <= m; i++)
    {
        int s1, s2;
        Select(*HT, i-1, &s1, &s2);
        (*HT)[s1].parent = (*HT)[s2].parent = i;
        (*HT)[i].left = s1;
        (*HT)[i].right = s2;
        (*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
    }
}
//HT為哈夫曼樹,HC為儲存結點哈夫曼編碼的二維動態陣列,n為結點的個數
void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){
    *HC = (HuffmanCode) malloc((n+1) * sizeof(char *));
    char *cd = (char *)malloc(n*sizeof(char)); //存放結點哈夫曼編碼的字串陣列
    cd[n-1] = '\0';//字串結束符
   
    for(int i=1; i<=n; i++){
        //從葉子結點出發,得到的哈夫曼編碼是逆序的,需要在字串陣列中逆序存放
        int start = n-1;
        //當前結點在陣列中的位置
        int c = i;
        //當前結點的父結點在陣列中的位置
        int j = HT[i].parent;
        // 一直尋找到根結點
        while(j != 0){
            // 如果該結點是父結點的左孩子則對應路徑編碼為0,否則為右孩子編碼為1
            if(HT[j].left == c)
                cd[--start] = '0';
            else
                cd[--start] = '1';
            //以父結點為孩子結點,繼續朝樹根的方向遍歷
            c = j;
            j = HT[j].parent;
        }
        //跳出迴圈後,cd陣列中從下標 start 開始,存放的就是該結點的哈夫曼編碼
        (*HC)[i] = (char *)malloc((n-start)*sizeof(char));
        strcpy((*HC)[i], &cd[start]);
    }
    //使用malloc申請的cd動態陣列需要手動釋放
    free(cd);
}
//列印哈夫曼編碼的函式
void PrintHuffmanCode(HuffmanCode htable,int *w,int n)
{
    printf("Huffman code : \n");
    for(int i = 1; i <= n; i++)
        printf("%d code = %s\n",w[i-1], htable[i]);
}
int main(void)
{
    int w[5] = {2, 8, 7, 6, 5};
    int n = 5;
    HuffmanTree htree;
    HuffmanCode htable;
    CreateHuffmanTree(&htree, w, n);
    HuffmanCoding(htree, &htable, n);
    PrintHuffmanCode(htable,w, n);
    return 0;
}
View Code

總結


圖 4 程式執行效果圖


本節的程式中對權重值分別為 2,8,7,6,5 的結點構建的哈夫曼樹如圖 4(A)所示。圖 4(B)是另一個哈夫曼樹,兩棵樹的帶權路徑長度相同。

程式執行效果圖之所以是(A)而不是(B),原因是在構建哈夫曼樹時,結點 2 和結點 5 構建的新的結點 7 儲存在動態樹組的最後面,所以,在程式繼續選擇兩個權值最小的結點時,直接選擇了的葉子結點 6 和 7 。