1 前言
霍夫曼樹是二叉樹的一種特殊形式,又稱為最優二叉樹,其主要作用在于數據壓縮和編碼長度的優化。
2 重要概念
2.1 路徑和路徑長度
在一棵樹中,從一個結點往下可以達到的孩子或孫子結點之間的通路,稱為路徑。通路中分支的數目稱為路徑長度。若規定根結點的層數為1,則從根結點到第L層結點的路徑長度為L-1。
圖2.1所示二叉樹結點A到結點D的路徑長度為2,結點A到達結點C的路徑長度為1。
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所示兩棵二叉樹
葉子結點為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.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:
2)則現在可以看作由T1,A,B構造霍夫曼樹,繼續執行步驟1。
選則B和T1構成一棵二叉樹如圖3.4:
3)現只有T2和A兩個結點,繼續執行步驟1。
選擇A和T2構成一棵二叉樹如圖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 結語
本文主要介紹了霍夫曼樹的實際意義和如何構造一棵二叉樹。學習霍夫曼樹主要是掌握霍夫曼樹的構造思想以及構造過程,至于代碼實現則是次要的,而且霍夫曼編碼實現過程中運用到了貪心算法。