深入學習二叉樹(三) 霍夫曼樹

1 前言

霍夫曼樹是二叉樹的一種特殊形式,又稱為最優二叉樹,其主要作用在于數據壓縮和編碼長度的優化。

2 重要概念

2.1 路徑和路徑長度

在一棵樹中,從一個結點往下可以達到的孩子或孫子結點之間的通路,稱為路徑。通路中分支的數目稱為路徑長度。若規定根結點的層數為1,則從根結點到第L層結點的路徑長度為L-1。

圖2.1

圖2.1所示二叉樹結點A到結點D的路徑長度為2,結點A到達結點C的路徑長度為1。

2.2 結點的權及帶權路徑長度

若將樹中結點賦給一個有著某種含義的數值,則這個數值稱為該結點的權。結點的帶權路徑長度為:從根結點到該結點之間的路徑長度與該結點的權的乘積。
圖2.2展示了一棵帶權的二叉樹


圖2.2

2.3 樹的帶權路徑長度

樹的帶權路徑長度規定為所有葉子結點的帶權路徑長度之和,記為WPL。
圖2.2所示二叉樹的WPL:
WPL = 6 * 2 + 3 * 2 + 8 * 2 = 34;

3 霍夫曼樹

3.1 定義

給定n個權值作為n個葉子結點,構造一棵二叉樹,若帶權路徑長度達到最小,稱這樣的二叉樹為最優二叉樹,也稱為霍夫曼樹(Huffman Tree)。
如圖3.1所示兩棵二叉樹


圖3.1

葉子結點為A、B、C、D,對應權值分別為7、5、2、4。
3.1.a樹的WPL = 7 * 2 + 5 * 2 + 2 * 2 + 4 * 2 = 36
3.1.b樹的WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3 = 35
由ABCD構成葉子結點的二叉樹形態有許多種,但是WPL最小的樹只有3.1.b所示的形態。則3.1.b樹為一棵霍夫曼樹。

3.2 構造霍夫曼樹

構造霍夫曼樹主要運用于編碼,稱為霍夫曼編碼。現考慮使用3.1中ABCD結點以及對應的權值構成如下長度編碼。
AACBCAADDBBADDAABB。
編碼規則:從根節點出發,向左標記為0,向右標記為1。
采用上述編碼規則,將圖3.1編碼為圖3.2所示:

圖3.2

構造過程:
3.1.a所示二叉樹稱為等長編碼,由于共有4個結點,故需要2位編碼來表示,編碼結果為:

結點 編碼
A 00
B 01
C 10
D 11

則AACBCAADDBBADDAABB對應編碼為:
00 00 10 01 10 00 00 11 11 01 01 00 11 11 00 00 01 01
長度為36。
3.1.b構造過程如下:
1)選擇結點權值最小的兩個結點構成一棵二叉樹如圖3.3:


圖3.3

2)則現在可以看作由T1,A,B構造霍夫曼樹,繼續執行步驟1。
選則B和T1構成一棵二叉樹如圖3.4:


圖3.4

3)現只有T2和A兩個結點,繼續執行步驟1。
選擇A和T2構成一棵二叉樹如圖3.5:


圖3.5

經過上述步驟則可以構造完一棵霍夫曼樹。通過觀察可以發現,霍夫曼樹中權值越大的結點距離根結點越近。
按照圖3.5霍夫曼樹編碼結果:

結點 編碼
A 0
B 10
C 110
D 111

則AACBCAADDBBADDAABB對應編碼為:
0 0 110 10 110 0 0 111 111 10 10 0 111 111 0 0 10 10
編碼長度為35。
由此可見,采用二叉樹可以適當降低編碼長度,尤其是在編碼長度較長,且權值分布不均勻時,采用霍夫曼編碼可以大大縮短編碼長度。

3.3 代碼實現

#include <iostream>
#include <stdlib.h>
using namespace std;
const int MaxValue = 10000;//初始設定的權值最大值
const int MaxBit = 4;//初始設定的最大編碼位數
const int MaxN = 10;//初始設定的最大結點個數
struct HaffNode//哈夫曼樹的結點結構
{
    int weight;//權值
    int flag;//標記
    int parent;//雙親結點下標
    int leftChild;//左孩子下標
    int rightChild;//右孩子下標
};
struct Code//存放哈夫曼編碼的數據元素結構
{
    int bit[MaxBit];//數組
    int start;//編碼的起始下標
    int weight;//字符的權值
};
void Haffman(int weight[], int n, HaffNode haffTree[])
//建立葉結點個數為n權值為weight的哈夫曼樹haffTree
{
    int j, m1, m2, x1, x2;
    //哈夫曼樹haffTree初始化。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;
        //注意這里沒打else那{},故無論是n個葉子節點還是n-1個非葉子節點都會進行下面4步的初始化
        haffTree[i].parent = 0;
        haffTree[i].flag = 0;
        haffTree[i].leftChild = -1;
        haffTree[i].rightChild = -1;
    }
    //構造哈夫曼樹haffTree的n-1個非葉結點
    for (int i = 0; i<n - 1; i++)
    {
        m1 = m2 = MaxValue;//Maxvalue=10000;(就是一個相當大的數)
        x1 = x2 = 0;//x1、x2是用來保存最小的兩個值在數組對應的下標
 
        for (j = 0; j<n + i; j++)//循環找出所有權重中,最小的二個值--morgan
        {
            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;
        haffTree[n + i].weight = haffTree[x1].weight + haffTree[x2].weight;
        haffTree[n + i].leftChild = x1;
        haffTree[n + i].rightChild = x2;
    }
}
void HaffmanCode(HaffNode haffTree[], int n, Code haffCode[])
//由n個結點的哈夫曼樹haffTree構造哈夫曼編碼haffCode
{
    Code *cd = new Code;
    int child, parent;
    //求n個葉結點的哈夫曼編碼
    for (int i = 0; i<n; i++)
    {
        //cd->start=n-1;//不等長編碼的最后一位為n-1,
        cd->start = 0;//,----修改從0開始計數--morgan
        cd->weight = haffTree[i].weight;//取得編碼對應權值的字符
        child = i;
        parent = haffTree[child].parent;
        //由葉結點向上直到根結點
        while (parent != 0)
        {
            if (haffTree[parent].leftChild == child)
                cd->bit[cd->start] = 0;//左孩子結點編碼0
            else
                cd->bit[cd->start] = 1;//右孩子結點編碼1
                                      //cd->start--;
            cd->start++;//改成編碼自增--morgan
            child = parent;
            parent = haffTree[child].parent;
        }
        //保存葉結點的編碼和不等長編碼的起始位
        //for(intj=cd->start+1;j<n;j++)
        for (int j = cd->start - 1; j >= 0; j--)//重新修改編碼,從根節點開始計數--morgan
            haffCode[i].bit[cd->start - j - 1] = cd->bit[j];
 
        haffCode[i].start = cd->start;
        haffCode[i].weight = cd->weight;//保存編碼對應的權值
    }
}
int main()
{
    int i, j, n = 4, m = 0;
    int weight[] = { 2,4,5,7 };
    HaffNode*myHaffTree = new HaffNode[2 * n - 1];
    Code*myHaffCode = new Code[n];
    if (n>MaxN)
    {
        cout << "定義的n越界,修改MaxN!" << endl;
        exit(0);
    }
    Haffman(weight, n, myHaffTree);
    HaffmanCode(myHaffTree, n, myHaffCode);
    //輸出每個葉結點的哈夫曼編碼
    for (i = 0; i<n; i++)
    {
        cout << "Weight=" << myHaffCode[i].weight << "  Code=";
        //for(j=myHaffCode[i].start+1;j<n;j++)
        for (j = 0; j<myHaffCode[i].start; j++)
            cout << myHaffCode[i].bit[j];
        m = m + myHaffCode[i].weight*myHaffCode[i].start;
        cout << endl;
    }
    cout << "huffman's WPL is:";
    cout << m;
    cout << endl;
    return 0;
}

4 結語

本文主要介紹了霍夫曼樹的實際意義和如何構造一棵二叉樹。學習霍夫曼樹主要是掌握霍夫曼樹的構造思想以及構造過程,至于代碼實現則是次要的,而且霍夫曼編碼實現過程中運用到了貪心算法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 四、樹與二叉樹 1. 二叉樹的順序存儲結構 二叉樹的順序存儲就是用數組存儲二叉樹。二叉樹的每個結點在順序存儲中都有...
    MinoyJet閱讀 1,592評論 0 7
  • 定義指針變量,如果不賦給它地址,系統會隨機給它分配一個地址。 C++標準庫 C++ Standard Librar...
    縱我不往矣閱讀 304評論 0 1
  • 普通樹與二叉樹的相互轉化及哈夫曼樹的了解 二叉樹與普通樹的轉化 二叉樹的種種特性使得它更便于處理,如果能將普通樹轉...
    sunhaiyu閱讀 1,616評論 0 3
  • 概念 樹是什么 樹(Tree)是n(n>=0)個結點的有限集。 n = 0的樹是空樹。 在任意一棵非空樹中: 有且...
    剛剛悟道閱讀 5,151評論 1 16
  • 二叉樹 二叉樹是每個節點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(...
    n油炸小朋友閱讀 804評論 0 1