資料結構之赫夫曼樹的演算法介紹和實現
一、基礎知識:
(1)最優二叉樹(赫夫曼樹)的介紹:
a、路徑長度:從樹中一個結點到另一個結點之間的分支構成這兩個結點之間的路徑,路徑上分支數目稱做路徑長度。
b、樹的路徑長度:從樹根到每一個結點之間的路徑長度之和。上一篇介紹的完全二叉樹就是這種路徑長度最短的二叉樹。
c、帶權路徑長度:結點的帶權路徑長度為從該結點到樹根之間的路徑產度與結點上權的乘積。樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和,通常記作WPL。
d、最優二叉樹(赫夫曼樹):假設有n個權值w1,w2,···wn,試構造一棵有n個葉子結點的二叉樹,每個葉子結點帶權為wi,則其中帶權路徑長度WPL最小的二叉樹稱做最優二叉樹或赫夫曼樹。
(2)構造赫夫曼樹:
1、根據給定的n個權值{w1,w2,···wn}構成 n棵二叉樹的集合F={T1,T2,···Tn},其中每個二叉樹Ti中只有一個帶權為wi的根結點,其左右子樹均為空。
2、選擇兩棵根結點的權值最小的輸作為左右子樹構造一棵新的二叉樹,且置新的二叉樹的根結點的權值為其左、右子樹上的根結點的權值之和。
3、在F中刪除這兩棵樹,同時將新得到的二叉樹加入F中。
4、重複2和3,直到F只含一棵樹為止。這棵樹便是赫夫曼樹。
(3)赫夫曼編碼
目前,進行快速遠距離通訊的主要手段是電報,即將需傳送的文字轉換成由二進位制的字元組成的字串。當然,在傳送電文時,希望總長儘可能地短。如果對每個字元設計長度不同的編碼,且讓電文中出現次數較多的字符采用盡可能短的編碼,則傳送電文的總長便可減少。因此,若要設計長短不等的編碼,則必須是任一個字元的編碼都不是另一個字元的編碼的字首,這種編碼稱做字首編碼
那麼,如何得到使電文最短的二進位制字首編碼呢? 假設每種字元在電文中出現的次數為wi,其編碼長度為li,電文中只有n中字元,則電文總長為w1*l1+w2*l2+···wn*ln。對應到二叉樹上,若置wi為葉子結點的權,li恰為從根到葉子的路徑長度。則w1*l1+w2*l2+···wn*ln恰為二叉樹的帶權路徑長度。由此可見,設計電文總長最短的二進位制字首編碼即為以n中字元出現的頻率作權,設計一棵赫夫曼樹的問題,由此得到的二進位制字首編碼便稱為赫夫曼編碼。
二、演算法分析設計:
由於赫夫曼樹中沒有度為1的結點(又稱為正則二叉樹),則一棵有n個葉子結點的赫夫曼樹共有2n-1個結點,可以儲存在一個大小為2n-1的一維陣列中。如何選定結點結構?由於在構成赫夫曼樹之後,為求編碼需從葉子結點出發走一條從葉子到根的路徑;而為譯碼需從根出發走一條從根到葉子的路徑。則對每個結點而言,既需要知道雙親的資訊,又需要知道孩子結點的資訊,由此,設定如下的儲存結構:
struct HNode{ //赫夫曼樹結點資料結構
int parent;
int lchild;
int rchild;
int weight;
};
HNode HT[2 * N]; //赫夫曼樹的一維陣列儲存結構(共需要2n-1個單元,n個葉子,n-1個度為2的結點,第0個單元未用)
構造赫夫曼樹的演算法程式碼如下:
void set_huftree() //建立赫夫曼樹
{
int s1=0, s2=0;
for (int i = 1; i <= 2*N-1; ++i)
{
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
//建立赫夫曼樹
for (int i = N + 1; i <= 2 * N-1; ++i)
{
select(i - 1, s1, s2); //選擇parent為0且weight最小的兩個結點,序號分別為s1和s2,用於構造新的二叉樹
HT[s1].parent = HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
}
求赫夫曼編碼的演算法程式碼如下:
<span style="font-size:18px;">//從葉子到根逆向求每個字元的赫夫曼編碼
void set_hufcode()
{
CodeType c;
int f, s; //s指定當前結點的序號,f為s結點的雙親結點的序號
for (int i = 1; i <= N; ++i)
{
c.start = N + 1;
for (s = i, f = HT[s].parent; f != 0; s = f, f = HT[s].parent) //f為0,表示已到當前結點已為根結點,
{
c.start--; //</span><span style="font-family: Arial, Helvetica, sans-serif;"><span style="font-size:14px;">該轉到下一個葉子去求編碼</span></span><span style="font-size:18px;">
if (s == HT[f].lchild)
c.bits[c.start] = '0';
else
c.bits[c.start] = '1';
}
HC[i].code = c;
}
}</span>
譯碼過程演算法程式碼如下:
//從根到葉子輸出赫夫曼編碼
void print_hufcode()
{
CodeType c;
cout << "輸入8個字元:" << endl;
for (int i = 1; i <= N; ++i)
{
cin >> HC[i].data;
}
cout << "輸入8個字元的權值:" << endl;
for (int i = 1; i <= N; ++i)
{
cin>> HT[i].weight;
}
set_huftree(); //建立赫夫曼樹
set_hufcode(); //求赫夫曼編碼
for (int i = 1; i <= N; ++i)
{
c = HC[i].code;
cout << HC[i].data << " -->";
for (int j = c.start; j <= N; ++j) <span style="font-family: Arial, Helvetica, sans-serif;">//從根到葉子譯出n個字元的赫夫曼編碼</span>
{
cout << c.bits[j];
}
cout << endl;
}
三、完整程式程式碼:
#include "stdafx.h"
#include <iostream>
#define N 8
#define infinity 32767
using namespace std;
struct HNode{ //赫夫曼樹結點資料結構
int parent;
int lchild;
int rchild;
int weight;
};
HNode HT[2 * N]; //赫夫曼樹的一維陣列儲存結構(共需要2n-1個單元,n個葉子,n-1個度為2的結點,第0個單元未用)
struct CodeType{ //單個赫夫曼編碼資料結構
int start;
char bits[N + 1]; //第0個單元未用
};
struct HCode{
char data;
CodeType code;
};
HCode HC[N + 1]; //赫夫曼編碼表
void select(int s, int &x1, int &x2) //選出parent為0且weight最小的兩個結點
{
int v1, v2;
v1 = v2 = infinity;
x1 = x2 = 0;
for (int i = 1; i <= s; ++i)
{
if (HT[i].parent == 0)
{
if (HT[i].weight < v1)
{
v2 = v1;
x2 = x1;
v1 = HT[i].weight;
x1 = i;
}
else if (HT[i].weight < v2)
{
x2 = i;
v2 = HT[i].weight;
}
}
}
}
void set_huftree() //建立赫夫曼樹
{
int s1=0, s2=0;
for (int i = 1; i <= 2*N-1; ++i)
{
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
//建立赫夫曼樹
for (int i = N + 1; i <= 2 * N-1; ++i)
{
select(i - 1, s1, s2); //選擇parent為0且weight最小的兩個結點,序號分別為s1和s2,用於構造新的二叉樹
HT[s1].parent = HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
}
//從葉子到根逆向求每個字元的赫夫曼編碼
void set_hufcode()
{
CodeType c;
int f, s;
for (int i = 1; i <= N; ++i)
{
c.start = N + 1;
for (s = i, f = HT[s].parent; f != 0; s = f, f = HT[s].parent)
{
c.start--;
if (s == HT[f].lchild)
c.bits[c.start] = '0';
else
c.bits[c.start] = '1';
}
HC[i].code = c;
}
}
//從根到葉子輸出赫夫曼編碼
void print_hufcode()
{
CodeType c;
cout << "輸入8個字元:" << endl;
for (int i = 1; i <= N; ++i)
{
cin >> HC[i].data;
}
cout << "輸入8個字元的權值:" << endl;
for (int i = 1; i <= N; ++i)
{
cin>> HT[i].weight;
}
set_huftree();
set_hufcode();
for (int i = 1; i <= N; ++i)
{
c = HC[i].code;
cout << HC[i].data << " -->";
for (int j = c.start; j <= N; ++j)
{
cout << c.bits[j];
}
cout << endl;
}
}
int _tmain(int argc, _TCHAR* argv[])
{
//輸出赫夫曼編碼
print_hufcode();
return 0;
}
參考:嚴蔚敏《資料結構》(c語言版)