哈夫曼樹(Huffman樹)原理分析及實現(C++)
1 構造原理
假設有n個權值,則構造出的哈夫曼樹有n個葉子結點。 n個權值分別設為 w1、w2、…、wn,則哈夫曼樹的構造規則為:
(1) 將w1、w2、…,wn看成是有n 棵樹的森林(每棵樹僅有一個結點);
(2) 在森林中選出兩個根結點的權值最小的樹合併,作為一棵新樹的左、右子樹,且新樹的根結點權值為其左、右子樹根結點權值之和;
(3)從森林中刪除選取的兩棵樹,並將新樹加入森林;
(4)重複(2)、(3)步,直到森林中只剩一棵樹為止,該樹即為所求得的哈夫曼樹。
顯然對n個權值,構造哈夫曼樹樹需要合併n-1次,形成的樹結點總數為2n-1;
2 程式碼實現
根據哈夫曼樹的構造原理,為方便實現,我們使用陣列來儲存每個結點,其命名為Tree;
2.1 節點結構
節點具有以下結構:
weight:權值
parent:父節點在陣列中的位置(同時用來判斷該節點是否已經參與了合併)
lchild,rchild:左右孩子結點在陣列中的位置
code:該結點所分配的編碼
//結點結構
struct HNode{
float weight; //權值
int parent; //父節點,父節點主要判定一個結點是否已經加入哈夫曼樹
int lchild,rchild; //左孩子,右孩子
string code; //記錄結點編碼
HNode() {
weight = 0;
parent = lchild = rchild = -1 ;
code = "";
}
HNode(float w) {
weight = w;
parent = lchild = rchild = -1;
code = "";
}
};
複製程式碼
2.2 關鍵成員函式解析
2.2.1 建構函式
為了判定一個結點是否已經加入哈夫曼樹中,可通過parent域的值來確定。初始時parent的值為-1,當某結點加入到樹中時,該節點parent域的值為其雙親結點在陣列中的下標。
構造哈夫曼樹時,首先將n個權值的葉子結點存放到陣列haftree的前n個分量中,然後不斷將兩棵子樹合併為一棵子樹,並將新子樹的根節點順序存放到陣列Tree的前n個分量的後面。
哈夫曼演演算法用虛擬碼描述為:
1、陣列haftree初始化,所有陣列元素的雙親、左右孩子都置為-1;
2、陣列haftree的前n個元素的權值置給定權值;
3、進行n-1次合併:
3.1 在二叉樹集合中選取兩個權值最小的根節點,其下標分別為i1,i2;
3.2 將二叉樹i1、i2合併為一棵新的二叉樹k。
建構函式輸入為權值陣列和陣列長度
//構造哈夫曼樹,輸入權值陣列 和 陣列長度n
void Create(float* a,int n) {
TreeSize = 2 * n - 1; //確定哈夫曼樹的結點個數
Tree = new HNode[TreeSize];
//權值陣列儲存在哈夫曼樹陣列前n個數值
for (int i = 0; i < n; i++)
{
Tree[i].weight = a[i];
}
//開始進行n-1次合併操作,形成一顆哈夫曼樹
int s1,s2;
int nextPos = (TreeSize + 1) / 2; //記錄下一個插入位置,起始為n
for (int i =0; i < (TreeSize-1)/2; i++) {
SelectTwoMin(nextPos,s1,s2); //找到陣列中權值最小的兩個點
//對Tree中新節點的孩子結點及權值賦值
Tree[nextPos].lchild = s1;
Tree[nextPos].rchild = s2;
Tree[nextPos].weight = Tree[s1].weight + Tree[s2].weight;
//給這兩個結點分配父節點(意味著兩個結點已加入哈夫曼樹)
Tree[s1].parent = Tree[s2].parent = nextPos;
nextPos++; //插入位置後移一位
}
}
複製程式碼
2.2.2 尋找最小的兩個點
先尋找到前兩個還未合併的點(parent=-1),在和後面未合併過的點比較權值,確定最終最小的兩個點,並確保權值小的在前面;
//尋找陣列中權值最小的兩個點
void SelectTwoMin(int nextPos,int &s1,int &s2)
{
//找到第一個未加入哈夫曼樹的結點,標記為s1
int i=0;
while (Tree[i].parent != -1) {
i++;
}
//從s2後一位開始找到第二個未加入哈夫曼樹的結點,標記為s2
s1 = i;
int j = i + 1;
while (Tree[j].parent != -1) {
j++;
}
s2 = j;
//調整s1和s2對應權值大小,確保s1對應點的權值更小,從而尋找更小的點時只需和s2比較
KeepOrder(s1,s2);
//從s2後一位開始,和s1,s2比較,確定陣列中權值最小的兩個點
for (int k = j + 1; k <nextPos; k++)
{
if (Tree[k].parent == -1) {
if (Tree[k].weight < Tree[s2].weight) {
s2 = k;
//找到小於s2的點後,需和s1比較確定大小順序
KeepOrder(s1,s2);
}
}
}
}
//確保n1對應結點的權值小於n2
void KeepOrder(int& n1,int& n2) {
if (Tree[n1].weight > Tree[n2].weight)
{
float tmp = n1;
n1 = n2;
n2 = tmp;
}
}
複製程式碼
2.2.3 對構造好的哈夫曼樹進行編碼並輸出
這裡使用層次遍歷,藉助一個佇列實現;孩子結點的編碼由父節點的code加上'1'/'0'(根據其處於父節點的左孩子還是右孩子而定,左孩子為'1');
只輸出葉子結點的編碼.也可以在編碼完成後直接輸出Tree陣列的前n個的結點的編碼;
//對樹中結點編碼並輸出
void PrintCoder()
{
cout << "index weight code" << endl;
queue<int> codeQueue; //只儲存編號
int tmp;
codeQueue.push(TreeSize - 1);
while (!codeQueue.empty())
{
tmp = codeQueue.front();
codeQueue.pop();
if (Tree[tmp].lchild ==-1 && Tree[tmp].rchild == -1) {
//葉子節點直接輸出
cout << setw(5) << tmp << setw(6) << Tree[tmp].weight << setw(10) << Tree[tmp].code << endl;
}
else {//因為哈夫曼樹中只有N0和N2
codeQueue.push(Tree[tmp].lchild);
Tree[Tree[tmp].lchild].code = Tree[tmp].code + "1"; //對左孩子編碼
codeQueue.push(Tree[tmp].rchild);
Tree[Tree[tmp].rchild].code = Tree[tmp].code + "0"; //對右孩子編碼
}
}
}
};
複製程式碼
2.2.4 輸出整個哈夫曼樹所有的結點的全部資訊(除編碼);
//輸出構建完成的哈夫曼樹
void PrintTree() {
cout << "index weight parent lchild rchild" << endl;
for (int i = 0; i < TreeSize; i++) {
cout << setw(5) << i;
cout << setw(6) << Tree[i].weight;
cout << setw(6) << Tree[i].parent;
cout << setw(6) << Tree[i].lchild;
cout << setw(6) << Tree[i].rchild;
cout << endl;
}
}
複製程式碼
2.2 完整程式碼
#pragma once
#include <iostream>
#include <iomanip>
#include <Queue>
#include <string>
using namespace std;
//用來獲取陣列長度
template <class T>
int getArrayLen(T& array)
{
return (sizeof(array) / sizeof(array[0]));
}
//結點結構
struct HNode{
float weight; //權值
int parent; //父節點,父節點主要判定一個結點是否已經加入哈夫曼樹
int lchild,rchild; //左孩子,右孩子
string code; //記錄結點編碼
HNode() {
weight = 0;
parent = lchild = rchild = -1;
code = "";
}
HNode(float w) {
weight = w;
parent = lchild = rchild = -1;
code = "";
}
};
class HuffmanTree {
HNode* Tree; //哈夫曼樹結點陣列頭部
int TreeSize; //樹中結點樹總數,TreeSize=葉子結點n(要編碼的字元個數) * 2 - 1
public:
HuffmanTree() {
Tree = NULL;
TreeSize = 0;
}
~HuffmanTree() {
delete[] Tree;
}
//構造哈夫曼樹,輸入權值陣列 和 陣列長度n
void Create(float* a,s2); //找到陣列中權值最小的兩個點
//對Tree中新節點的孩子結點及權值賦值
Tree[nextPos].lchild = s1;
Tree[nextPos].rchild = s2;
Tree[nextPos].weight = Tree[s1].weight + Tree[s2].weight;
//給這兩個結點分配父節點(意味著兩個結點已加入哈夫曼樹)
Tree[s1].parent = Tree[s2].parent = nextPos;
nextPos++; //插入位置後移一位
}
}
//尋找陣列中權值最小的兩個點
void SelectTwoMin(int nextPos,s2);
}
}
}
}
//確保n1對應結點的權值小於n2
void KeepOrder(int& n1,int& n2) {
if (Tree[n1].weight > Tree[n2].weight)
{
float tmp = n1;
n1 = n2;
n2 = tmp;
}
}
//輸出構建完成的哈夫曼樹
void PrintTree() {
cout << "index weight parent lchild rchild" << endl;
for (int i = 0; i < TreeSize; i++) {
cout << setw(5) << i;
cout << setw(6) << Tree[i].weight;
cout << setw(6) << Tree[i].parent;
cout << setw(6) << Tree[i].lchild;
cout << setw(6) << Tree[i].rchild;
cout << endl;
}
}
//對樹中結點編碼並輸出
void PrintCoder()
{
cout << "index weight code" << endl;
queue<int> codeQueue; //只儲存編號
int tmp;
codeQueue.push(TreeSize - 1);
while (!codeQueue.empty())
{
tmp = codeQueue.front();
codeQueue.pop();
if (Tree[tmp].lchild ==-1 && Tree[tmp].rchild == -1) {
//葉子節點直接輸出
cout << setw(5) << tmp << setw(6) << Tree[tmp].weight << setw(10) << Tree[tmp].code << endl;
}
else {//因為哈夫曼樹中只有N0和N2
codeQueue.push(Tree[tmp].lchild);
Tree[Tree[tmp].lchild].code = Tree[tmp].code + "1"; //對左孩子編碼
codeQueue.push(Tree[tmp].rchild);
Tree[Tree[tmp].rchild].code = Tree[tmp].code + "0"; //對右孩子編碼
}
}
}
};
複製程式碼
3 測試程式碼及輸出
///哈夫曼樹
HuffmanTree hTree;
float x[] = { 5,29,7,8,14,23,3,11 };
hTree.Create(x,getArrayLen(x));//避免輸入錯誤的陣列長度導致錯誤
hTree.PrintTree();
hTree.PrintCoder();
複製程式碼
正確輸出:
4 參考資料
1.哈夫曼樹演演算法及C++實現:www.cnblogs.com/smile233/p/…
2.百度百科·哈夫曼樹:baike.baidu.com/item/哈夫曼樹/2…
3.資料結構:Huffman樹(哈夫曼樹)原理及C++實現:blog.csdn.net/weixin_4142…