數據結構——哈夫曼(Huffman)樹+哈夫曼編碼


前天acm實驗課,老師教了幾種排序,抓的一套題上有一個哈夫曼樹的題,正好之前離散數學也講過哈夫曼樹,這里我就結合課本,整理一篇關於哈夫曼樹的博客。

主要摘自https://www.cnblogs.com/skywang12345/p/3706821.html感謝大佬

https://www.cnblogs.com/kubixuesheng/p/4397798.html這位大佬舉例很好

哈夫曼樹的介紹

Huffman Tree,中文名是哈夫曼樹或霍夫曼樹,它是最優二叉樹。

定義:給定n個權值作為n個葉子結點,構造一棵二叉樹,若樹的帶權路徑長度達到最小,則這棵樹被稱為哈夫曼樹。 這個定義里面涉及到了幾個陌生的概念,下面就是一顆哈夫曼樹,我們來看圖解答。

(01) 路徑和路徑長度

定義:在一棵樹中,從一個結點往下可以達到的孩子或孫子結點之間的通路,稱為路徑。通路中分支的數目稱為路徑長度。若規定根結點的層數為1,則從根結點到第L層結點的路徑長度為L-1。  例子:100和80的路徑長度是1,50和30的路徑長度是2,20和10的路徑長度是3。

(02) 結點的權及帶權路徑長度

定義:若將樹中結點賦給一個有着某種含義的數值,則這個數值稱為該結點的權。結點的帶權路徑長度為:從根結點到該結點之間的路徑長度與該結點的權的乘積。  例子:節點20的路徑長度是3,它的帶權路徑長度= 路徑長度 * 權 = 3 * 20 = 60。

(03) 樹的帶權路徑長度

定義:樹的帶權路徑長度規定為所有葉子結點的帶權路徑長度之和,記為WPL。  例子:示例中,樹的WPL= 1*100 + 2*50 + 3*20 + 3*10 = 100 + 100 + 60 + 30 = 290。

比較下面兩棵樹

上面的兩棵樹都是以{10, 20, 50, 100}為葉子節點的樹。

左邊的樹WPL=2*10 + 2*20 + 2*50 + 2*100 = 360  右邊的樹WPL=350

左邊的樹WPL > 右邊的樹的WPL。你也可以計算除上面兩種示例之外的情況,但實際上右邊的樹就是{10,20,50,100}對應的哈夫曼樹。至此,應該堆哈夫曼樹的概念有了一定的了解了,下面看看如何去構造一棵哈夫曼樹。

哈夫曼樹的圖文解析

假設有n個權值,則構造出的哈夫曼樹有n個葉子結點。 n個權值分別設為 w1、w2、…、wn,哈夫曼樹的構造規則為:

1. 將w1、w2、…,wn看成是有n 棵樹的森林(每棵樹僅有一個結點); 

2. 在森林中選出根結點的權值最小的兩棵樹進行合並,作為一棵新樹的左、右子樹,且新樹的根結點權值為其左、右子樹根結點權值之和; 

3. 從森林中刪除選取的兩棵樹,並將新樹加入森林; 

4. 重復(02)、(03)步,直到森林中只剩一棵樹為止,該樹即為所求得的哈夫曼樹。

以{5,6,7,8,15}為例,來構造一棵哈夫曼樹。

第1步:創建森林,森林包括5棵樹,這5棵樹的權值分別是5,6,7,8,15。 

第2步:在森林中,選擇根節點權值最小的兩棵樹(5和6)來進行合並,將它們作為一顆新樹的左右孩子(誰左誰右無關緊要,這里,我們選擇較小的作為左孩子),並且新樹的權值是左右孩子的權值之和。即,新樹的權值是11。 然后,將"樹5"和"樹6"從森林中刪除,並將新的樹(樹11)添加到森林中。 

第3步:在森林中,選擇根節點權值最小的兩棵樹(7和8)來進行合並。得到的新樹的權值是15。 然后,將"樹7"和"樹8"從森林中刪除,並將新的樹(樹15)添加到森林中。 

第4步:在森林中,選擇根節點權值最小的兩棵樹(11和15)來進行合並。得到的新樹的權值是26。 然后,將"樹11"和"樹15"從森林中刪除,並將新的樹(樹26)添加到森林中。 

第5步:在森林中,選擇根節點權值最小的兩棵樹(15和26)來進行合並。得到的新樹的權值是41。 然后,將"樹15"和"樹26"從森林中刪除,並將新的樹(樹41)添加到森林中。  此時,森林中只有一棵樹(樹41)。這棵樹就是我們需要的哈夫曼樹!

哈夫曼樹代碼

直接使用了PJQ師姐的代碼,之后有空會更新。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct node
{
    int key;
    struct node *l;
    struct node *r;
};
typedef struct node *pnode;
int mark[100];
struct node  huffman[100];
void PrintNode(const pnode node)
{
    printf("key = %d \n", node->key);
}
void PreOrder(pnode T)
{
    if(T)
    {
        PrintNode(T);
        PreOrder(T->l);
        PreOrder(T->r);
    }
}
void Select(int *mark, struct node *huffman, int size, int *choose)
{

    int i;
    for(i = 0;  i< size;  i++)
    {
        if(mark[i])
        {
            choose[0] = i;
            i++;
            break;
        }
    }
    choose[1] = choose[0];
    for(; i < size; i++)
    {
        if(mark[i])
        {
            if(huffman[choose[0]].key >= huffman[i].key)
            {
                choose[1] = choose[0];
                choose[0] = i;
            }
            else if(huffman[choose[1]].key > huffman[i].key)
            {
                choose[1] = i;
            }
        }

    }
}
void Choose(int *mark, struct node *huffman, int size, int *choose)
{
    int i;
    int minkey = 0;
    int tkey = 0;
    int temp = 0;
    for(i = 0;  i< size;  i++)
    {
        if(mark[i])
        {
            minkey = i;
            i++;
            break;
        }
    }
    tkey = minkey;
    for(;  i< size;  i++)
    {
        if(mark[i])
        {
            if(huffman[i].key < huffman[minkey].key)
            {
                tkey = minkey;
                minkey = i;
            }
            if(tkey == minkey)
                tkey = i;
            if(huffman[tkey].key > huffman[i].key && i != minkey)
            {
                tkey = i;
            }
        }
    }
    choose[0] = minkey;
    choose[1] = tkey;
}
pnode HuffmanTree(int *mark, struct node *huffman, int size)
{
    int choose[2];
    int i;
    pnode mynode;
    for(i = 0;  i < size-1;  i++)
    {
        Select(mark, huffman, size, choose);
        mynode = (pnode)malloc(sizeof(struct node));
        mynode->key = huffman[choose[0]].key+huffman[choose[1]].key;//更新key值
        mynode->l = (pnode)malloc(sizeof(struct node));
        mynode->l->key = huffman[choose[0]].key;
        mynode->l->l = huffman[choose[0]].l;
        mynode->l->r = huffman[choose[0]].r;
        mynode->r = &huffman[choose[1]];
        huffman[choose[0]] = *mynode;
        mark[choose[1]] = 0;
        free(mynode);
    }
    return &huffman[choose[0]];
}
int main(void)
{
    int key[8] = {5,29,7,8,14,23,3,11};
    int i;
    pnode huffmantree;
    memset(mark, -1, sizeof(mark));
    memset(huffman, 0, sizeof(huffman));
    for(i = 0;  i < 8;  i++)
    {
        huffman[i].key = key[i];
    }
    huffmantree = HuffmanTree(mark, huffman, 8);
    PreOrder(huffmantree);
    return 0;
}

在解決acm競賽題時,可以直接使用C++ STL里的優先隊列實現,因為優先隊列具有直接排序的功能,可以模擬節點和合並。

這個代碼是之前遇到的大頂堆問題,但它並不能建立樹形結構,只能用來求樹的最小帶權路徑長度。

#include<cstdio>
#include<cstring>
#include<queue>
#include<vector>
#include<algorithm>
#define ll long long int
using namespace std;
int main()
{
    int n,i;
    int x,y;
    int a;
    ll ans=0;
    priority_queue<int,vector<int>,greater<int> >q;
    scanf("%d",&n);
    for(i=0;i<n;i++)
    {
        scanf("%d",&a);
        q.push(a);
    }
    while(q.size()>1)
    {
        x=q.top();
        q.pop();
        y=q.top();
        q.pop();
        ans+=x+y;
        q.push(x+y);
    }
    printf("%lld\n",ans);
    return 0;
}

哈夫曼編碼

哈夫曼樹的應用很廣,哈夫曼編碼就是其在電訊通信中的應用之一。廣泛地用於數據文件壓縮的十分有效的編碼方法。其壓縮率通常在20%~90%之間。在電訊通信業務中,通常用二進制編碼來表示字母或其他字符,並用這樣的編碼來表示字符序列。 

例:如果需傳送的電文為 ‘ABACCDA’,它只用到四種字符,用兩位二進制編碼便可分辨。假設 A, B, C, D 的編碼分別為 00, 01,10, 11,則上述電文便為 ‘00010010101100’(共 14 位),譯碼員按兩位進行分組譯碼,便可恢復原來的電文。

能否使編碼總長度更短呢?

實際應用中各字符的出現頻度不相同,用短(長)編碼表示頻率大(小)的字符,使得編碼序列的總長度最小,使所需總空間量最少

數據的最小冗余編碼問題

在上例中,若假設 A, B, C, D 的編碼分別為 0,00,1,01,則電文 ‘ABACCDA’ 便為 ‘000011010’(共 9 位),但此編碼存在多義性:可譯為: ‘BBCCDA’、‘ABACCDA’、‘AAAACCACA’ 等。

譯碼的惟一性問題

要求任一字符的編碼都不能是另一字符編碼的前綴,這種編碼稱為前綴編碼(其實是非前綴碼)。 在編碼過程要考慮兩個問題,數據的最小冗余編碼問題,譯碼的惟一性問題,利用最優二叉樹可以很好地解決上述兩個問題

用二叉樹設計二進制前綴編碼

以電文中的字符作為葉子結點構造二叉樹。然后將二叉樹中結點引向其左孩子的分支標 ‘0’,引向其右孩子的分支標 ‘1’; 每個字符的編碼即為從根到每個葉子的路徑上得到的 0, 1 序列。如此得到的即為二進制前綴編碼。

 

編碼: A:0, C:10,B:110,D:111 

 

任意一個葉子結點都不可能在其它葉子結點的路徑中。

 

用哈夫曼樹設計總長最短的二進制前綴編碼

假設各個字符在電文中出現的次數(或頻率)為 wi ,其編碼長度為 li,電文中只有 n 種字符,則電文編碼總長為:

 

設計電文總長最短的編碼,設計哈夫曼樹(以 n 種字符出現的頻率作權),

由哈夫曼樹得到的二進制前綴編碼稱為哈夫曼編碼   

例:如果需傳送的電文為 ‘ABACCDA’,即:A, B, C, D 

的頻率(即權值)分別為 0.43, 0.14, 0.29, 0.14,試構造哈夫曼編碼。

 

編碼: A:0, C:10,  B:110, D:111 。電文 ‘ABACCDA’ 便為 ‘0110010101110’(共 13 位)。

 

例:如果需傳送的電文為 ‘ABCACCDAEAE’,即:A, B, C, D, E 的頻率(即權值)分別為0.36, 0.1, 0.27, 0.1, 0.18,試構造哈夫曼編碼。

 

編碼: A:11,C:10,E:00,B:010,D:011 ,則電文 ‘ABCACCDAEAE’ 便為 ‘110101011101001111001100’(共 24 位,比 33 位短)。

 

譯碼
從哈夫曼樹根開始,對待譯碼電文逐位取碼。若編碼是“0”,則向左走;若編碼是“1”,則向右走,一旦到達葉子結點,則譯出一個字符;再重新從根出發,直到電文結束。

電文為 “1101000” ,譯文只能是“CAT”

哈夫曼算法的真正確性

其實想要簡單了解和使用哈夫曼算法,看到前面已經可以了,博主由於復習算法設計分析,這里增加關於哈夫曼算法正確性的推理。

要證明哈夫曼算法的正確性,只要證明最優前綴碼問題具有貪心選擇性質和最優子結構。

1、貪心選擇性質

 2、最優子結構性質

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM