1. 程式人生 > 程式設計 >哈夫曼樹(Huffman樹)原理分析及實現(C++)

哈夫曼樹(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…