【DSA】樹-哈弗曼樹詳解(3)

什麼是哈弗曼樹

  • 百度百科的定義
    給定N個權值作爲N個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度達到最小,稱這樣的二叉樹爲最優二叉樹,也稱爲哈夫曼樹(Huffman Tree)。哈夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。
    哈弗曼樹

上述定義很學術,是很嚴謹的表達,但是看起來總不是那麼好理解。在這裏,我們不說理論,直接來看一顆哈弗曼樹是如何構建的,通過具象事物來理解抽象的概念。

怎樣構建哈弗曼樹

下邊,我們使用一個哈夫曼編碼的例子來理解哈弗曼樹。

  • 背景問題
    在這裏,我們通過一個示例來說明。
    現在,需要將一篇英文文章從A發送給B,要求是編碼的長度最短。英文字母一共26個,大小寫如果不同,那就是52個。那麼我們需要6位二進制來進行編碼(2^5 < 52 < 2 ^6=64)。如果這篇文章有字母10000個,那麼編碼長度就是10000*6。我們知道,在文章中,每一個字母出現的頻率是不同的,思考:對不同頻率的字母使用不通長度的編碼位數,也就是出現頻率最高的字母最短的編碼長度,給頻率最低的字母最長的編碼長度。 這樣就能使得整篇文章的編碼總長度降到最低。提高傳輸效率。

實現方式如下:
字符頻率表

字母 A B C D E F G H
頻率 80 30 20 75 40 8 55 60

根據上述表格,我們知道,A的編碼是最短的,F的編碼是最長的。這裏的頻率是我任意指定的,實際字母出現的概率在密碼學中有統計(可以參考字母頻率)。

  • 構建
    第一步:選出其中頻率最小的兩個字母F和C,用這連個字母組成二叉樹,小的爲左孩子,大的爲右孩子。並且將F和C的頻率之和作爲根節點,返回給頻率表。
    在這裏插入圖片描述
    第二步:連續重複上述操作。
    F+C 小於 B,所以28在左孩子的位置。
字母 A B D E FC G H
頻率 80 30 75 40 28 55 60

在這裏插入圖片描述
發現此時最小的是 40 和 55 (E和G)

字母 A D E FCB G H
頻率 80 75 40 58 55 60

在這裏插入圖片描述
此時最小的是58 和60 (EG和H)

字母 A D FCB EG H
頻率 80 75 58 95 60

在這裏插入圖片描述

字母 A D FCBH EG
頻率 80 75 118 95

在這裏插入圖片描述

字母 AD FCBH EG
頻率 155 118 95

在這裏插入圖片描述

字母 AD FCBHEG
頻率 155 213

在這裏插入圖片描述
至此,哈弗曼樹構造完成了,那麼前面說的編碼是怎麼實現的呢?按照二進制,輸的左邊標0,右邊標1.沿着樹的方向直至字母所在的葉子節點的0和1的序列即爲該字母的哈夫曼編碼。如下如:
在這裏插入圖片描述
下表是所有字母的最終編碼

字母 編碼
A 01
B 1101
C 11001
D 00
E 100
F 11000
G 101
H 111

假如我要傳送ABC四個字母,那麼編碼就是0 111111 1111101,一共14位,如果按照最開始的一個字母6位編碼,那麼長度就是18了。

哈弗曼樹的代碼實現

構建哈夫曼樹時,需要每次根據各個結點的權重值,篩選出其中值最小的兩個結點,然後構建二叉樹。
查找權重值最小的兩個結點的思想是:從樹組起始位置開始,首先找到兩個無父結點的結點(說明還未使用其構建成樹),然後和後續無父結點的結點依次做比較,有兩種情況需要考慮:

  • 如果比兩個結點中較小的那個還小,就保留這個結點,刪除原來較大的結點;
  • 如果介於兩個結點權重值之間,替換原來較大的結點;

哈夫曼樹的結構數據結構

// 哈夫曼樹結點結構
typedef int Type;

typedef struct HuffmanNode_
{
    Type  weight; // 節點權重
    Type  parent, left, right; //父結點、左孩子、右孩子在數組中的位置下標

}Node, *HuffmanTree; 
// 選中頻率最小的兩個數據
// HT數組中存放的哈夫曼樹,end表示HT數組中存放結點的最終位置,s1和s2傳遞的是HT數組中權重值最小的兩個結點在數組中的位置

void select(HuffmanTree HT, int *pos1, int *pos2, int end)
{
    int min1 = 0, min2 = 0;

    int i = 1; // 數組的 0 號元素作爲根節點的位置所以不使用

    // 找到沒有構建成樹的第一個節點
    while (HT[i].parent != 0 && i <= end)
    {
        i++;
    }
    min1 = HT[i].weight;
    *pos1 = i;

    i++;
    // 找到沒有構建成樹的第二個節點
    while(HT[i].parent != 0 && i <= end) 
    {
        i++;
    }

    min2 = HT[i].weight;
    if (min2 < min1)
    {
        min2 = min1;
        *pos2 = *pos1;
        min1 = HT[i].weight;
        *pos1 = i;
    }
    else
    {
        *pos2 = i;
    }

    // 取得兩個節點之後,跟之後所有沒有構建成樹的節點逐一比較,最終獲取最小的兩個節點
    for (int j = i+1; j <= end; ++j)
    {
        // 如果已經存在父節點,也就是已經被構建樹了,則跳過
        if (HT[j].parent != 0)
        {
            continue;
        }

        // 如果比min1 還小,將min2 = 敏, min1修改爲新的節點下標
        if (HT[j].weight < min1)
        {
            min2 = min1;
            min1 = HT[j].weight;
            *pos2 = *pos1;
            *pos1 = j;
        }
        else if (HT[j].weight < min2 && HT[j].weight > min1)
        {
            // 如果大於 min1 小於 min2
            min2 = HT[j].weight;
            *pos2 = j;
        }
    }
}
// 創建完整的哈夫曼樹
// HT爲地址傳遞的存儲哈夫曼樹的數組,w爲存儲結點權重值的數組,n爲結點個數

HuffmanTree init_huffman_tree(Type *weight, int node_num)
{
    if (node_num <= 1)
    {
        // 只有一個節點那麼編碼就是 0
        return NULL;
    }

    int tree_node_num = node_num * 2 - 1; // 根節點不使用
    HuffmanTree p = (HuffmanTree)malloc((tree_node_num+1) * sizeof(Node));

    // 初始化哈夫曼數組中的所有節點
    for (int i = 1; i <= tree_node_num; ++i)
    {        
        if (i <= node_num)
        {
            (p+i)->weight = *(weight+i-1); // 第0個位置不使用
        }
        else
        {
            (p+i)->weight = 0;
        }

        (p+i)->parent = 0;
        (p+i)->left = 0;
        (p+i)->right = 0;
    }

    return p;
}

void close_huffman_tree(HuffmanTree HT)
{
    if (HT)
    {
        free(HT);
        HT = NULL;
    }
}

void create_huffman_tree(HuffmanTree HT, int node_num)
{
    if (NULL == HT || node_num <= 1)
    {
        return;
    }

    int tree_node_num = node_num * 2 - 1; // 根節點不使用
    for (int i = node_num + 1; i <= tree_node_num; ++i)
    {
        int pos1 = -1, pos2 = -1;
        // 找到頻率最小的連個節點
        select(HT, &pos1, &pos2, i-1);
        printf("當前最小的兩個節點 [%d %d]\n", HT[pos1].weight, HT[pos2].weight);
        // 這裏使用下表來表示父子關係
        HT[pos1].parent = HT[pos2].parent = i; // pos1 位置的元素和pos2位置的元素 的父節點就是,第 i個位置的元素
        HT[i].left = pos1;  // 父節點的左後孩子賦值
        HT[i].right = pos2;
        HT[i].weight = HT[pos1].weight + HT[pos2].weight; // 父節點的權重等於 左右孩子權重的和
    }
}
  • 測試代碼
void print(HuffmanTree HT, int node_num)
{
    if (NULL == HT)
    {
        printf("數組爲空\n");
        return;
    }

    int tree_node_num;

    for (int i = 1; i < tree_node_num; ++i)
    {
        printf("%d 的父節點:%d 左孩子:%d 右孩子:%d\n", HT[i].weight, HT[HT[i].parent].weight, HT[i].left, HT[i].right);
    }

}

int main(int argc, char const *argv[])
{
    
    Type weight[8] = {80, 30, 20, 75, 40, 8, 55, 60};

    int node_num = sizeof(weight) / sizeof(Type);

    HuffmanTree HT = init_huffman_tree(weight, node_num);

    create_huffman_tree(HT, node_num);

    print(HT, node_num);

    close_huffman_tree(HT);

    return 0;
}
  • 測試結果
    !在這裏插入圖片描述

爲什麼要設計哈弗曼樹

  • 哈弗曼樹主要用於哈夫曼編碼,其主要作用就是利用頻率屬性進行編碼,最終達到目的:讓高頻的數據擁有短編碼,而低頻的數據擁有長編碼。
  • 哈夫曼編碼並不是適用於所有場景,它更適用於頻率變化多端的數據編碼。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章