[數據結構]哈夫曼樹和哈夫曼編碼、譯碼(通俗易懂)

一、哈夫曼樹的相關的幾個名詞

1、路徑:在一棵樹中,一個結點到另一個結點之間的通路,稱爲路徑。如下圖根結點到a結點之間的通路就是一條路徑。

2、路徑長度:在一條路徑中,每經過一個結點,路徑長度都要加1。如下圖根結點到b結點之間所經過的通路數爲2,也就是路徑長度爲2。

3、結點的權:每一個結點都有一個唯一的數值,稱爲結點的權。如下圖結點a的權值爲7,b的權值爲5。

4、結點的帶權路徑長度:根結點到結點間的路徑長度*結點的權值。如圖1結點b的帶權路徑長度=2*5=10。

5、樹的帶權路徑長度(WPL):樹中所有葉子結點的帶權路徑長度之和。如圖1的WPL=1*7+2*5+3*2+3*4=35。

二、什麼是哈夫曼樹?

在權爲w1,w2,...,wn的n各葉子結點的所有二叉樹中,樹的帶權路徑長度WPL最小的二叉樹稱爲最優二叉樹,也叫赫夫曼樹或哈夫曼樹。

//哈夫曼樹的結點結構
typedef struct {
	char ch;///字符結點
	int weight;///字符權值
	int parent;///父母結點
	int lchild;///左孩子在數組位置的下標
	int rchild;///右孩子在數組位置的下標
}hnode;

三、如何構建哈夫曼樹?

在構建哈夫曼樹時,只需遵循一個原則:權值越大的結點離根結點越近。(權值最大就作爲根結點)

1、構建哈夫曼樹的過程:

(1)在n個結點權值中選出最小和次最小的結點權值,兩結點組成一個新的二叉樹,其根結點的權值爲左右孩子結點的權值和。

(2)在原有的n個結點權值中刪除最小和次最小·,將新權值加入到n-2個權值的行列中。

(3)重複(1)、(2),直到所有結點構成一棵二叉樹爲止,這棵樹就是哈夫曼樹。

2、構建哈夫曼樹:

哈夫曼樹表:

結點i 字符 權值 左孩子 右孩子 雙親結點
0 a 7 -1 -1 6
1 b 5 -1 -1 5
2 c 2 -1 -1 4
3 d 4 -1 -1 4
4   6 2 3 5
5   11 1 4 6
6   18 0 5 -1

 

僞代碼實現:

void Great_hfmtree(HfmTree ht[], int n){///創建哈夫曼樹,n個結點
	int m1, m2, x1, x2;
	int i, j;
	for (i = 0; i<2 * n - 1; i++){///初始化
		ht[i].weight = 0;
		ht[i].parent = -1;
		ht[i].lchild = -1;
		ht[i].rchild = -1;
	}
	cout<<"<>輸入字符結點:";
	for (int i = 0; i<n; i++)
		cin >> ht[i].ch;
    cout<<"<>輸入各字符結點對應的權值:";
	for (i = 0; i<n; i++)
		cin >> ht[i].weight;
	for (i = 0; i<n - 1; i++){
		x1 = x2 = MAXV;
		m1 = m2 = 0;
		for (j = 0; j<n + i; j++){///n+i爲原來結點+新增結點
			if (ht[j].parent == -1 && hte[j].weight<x1){///找權值次最小結點
				x2 = x1;///記錄x1,保證權值最小一定小於於權值次小
				x1 = ht[j].weight;
				m1 = j;///記錄權值次最小結點位置
			}
			else if (ht[j].parent == -1 && ht[j].weight<x2){///找權值最小結點
				x2 = ht[j].weight;
				m2 = j;///記錄權值最小結點位置
			}
		}
		ht[m1].parent = n + i;
		ht[m2].parent = n + i;
		ht[n + i].weight = ht[m1].weight + ht[m2].weight;///權值最小結點和權值次最小結點之和
		ht[n + i].lchild = m1;
		ht[n + i].rchild = m2;
	}
}

四、哈夫曼編碼

哈夫曼編碼:一棵哈夫曼樹中,規定哈夫曼樹的左分支代表0,右分支代表1,則從根結點到每個葉結點所經過的路徑分支組成的0和1序列便爲該結點對應字符的編碼。如下圖

代碼實現方法①:

void Reverse(char c[])  	//將字符串倒置
{
	int k = 0;
	char temp;
	while (c[k + 1] != '\0')//獲取當前字符編碼的長度
	{
		k++;
	}
	for (int i = 0; i <= k / 2; i++)//交換位置
	{
		temp = c[i];
		c[i] = c[k - i];
		c[k - i] = temp;
	}
}

void HfmCode(HfmTree ht[], HCode hc[], int n)//從葉子結點到根結點遍歷編碼
{
	hc = new HCode[n];
	for (int i = 0; i<n; i++)
	{
		hc[i].data = ht[i].ch;
		int ic = i;
		int ip = ht[i].parent;
		int k = 0;
		while (ip != -1)
		{
			if (ic == ht[ip].lchild)   //左孩子標'0'
				hc[i].code[k] = '0';
			else
				hc[i].code[k] = '1';  //右孩子標'1'
			k++;
			ic = ip;
			ip = ht[ic].parent;
		}
		hc[i].code[k] = '\0';
		Reverse(hc[i].code);
	}
	for(int i=0;i<n;i++) cout<<"結點"<<hc[i].data<<"的字符編碼:"<<hc[i].code<<endl;
}

代碼實現方法②:

typedef char **huffmancode;///定義字符存儲編碼指針
void Haffmancode(HfmTree ht[], int n, huffmancode &hcode){
	char *cd = new char[n];///申請內存空間,等同char*cd=(char*)malloc(n*sizeof(char));
	hcode = new char *[n + 1];
	cd[n - 1] = '\0';
	int i, j, c, p;
	for (i = 0; i<n; i++){
		int start = n;
		c = i;
		p = ht[c].parent;
		while (p != -1){
			--start;
			if (ht[p].lchild == c) cd[start] = '0';
			else cd[start] = '1';
			c = p;
			p = ht[p].parent;
		}
		hcode[i] = new char[n - start];///分配內存空間或者建字符數組
		strcpy(hcode[i], &cd[start]);///賦予字符串地址,因爲編碼數組範圍在[n-start,n-1].
	}
	for(int i=0;i<n;i++) cout<<"結點"<<hfmtree[i].ch<<"的字符編碼:"<<hcode[i]<<endl;
	delete cd;///釋放內存空間
}

五、哈夫曼樹完整代碼的實現

#include<bits/stdc++.h>
using namespace std;
#define MAXV 10000
#define N 10000
typedef struct
{
	char ch;
	int weight;
	int lchild, rchild, parent;
}HfmTree;
typedef struct
{
	char  data;
	char  code[100];
}HCode;
void Great_hfmtree(HfmTree ht[], int n){///創建哈夫曼樹,n個結點
	int m1, m2, x1, x2;
	int i, j;
	for (i = 0; i<2 * n - 1; i++){///初始化
		ht[i].weight = 0;
		ht[i].parent = -1;
		ht[i].lchild = -1;
		ht[i].rchild = -1;
	}
	char x,y;
	cout<<"<>輸入各字符結點:";
	for (int i = 0; i<n; i++)
        cin >> ht[i].ch;

    cout<<"<>輸入各字符對應的權值:";
	for (i = 0; i<n; i++)
		cin >> ht[i].weight;
	for (i = 0; i<n - 1; i++){
		x1 = x2 = MAXV;
		m1 = m2 = 0;
		for (j = 0; j<n + i; j++){///n+i爲
			if (ht[j].parent == -1 && ht[j].weight<x1){///找權值最小結點
				x2 = x1;///記錄x1,保證權值最小一定大於權值次小
				x1 = ht[j].weight;
				m1 = j;///記錄權值最小結點位置
			}
			else if (ht[j].parent == -1 && ht[j].weight<x2){///找權值次最小結點
				x2 = ht[j].weight;
				m2 = j;///記錄權值次最小結點位置
			}
		}
		ht[m1].parent = n + i;
		ht[m2].parent = n + i;
		ht[n + i].weight =ht[m1].weight + ht[m2].weight;///權值最小結點和權值次最小結點之和
		ht[n + i].lchild = m1;
		ht[n + i].rchild = m2;
	}
}
void Reverse(char c[])  	//將字符串倒置
{
	int k = 0;
	char temp;
	while (c[k + 1] != '\0')
	{
		k++;
	}
	for (int i = 0; i <= k / 2; i++)
	{
		temp = c[i];
		c[i] = c[k - i];
		c[k - i] = temp;
	}
}

void HfmCode(HfmTree ht[], HCode hc[], int n)      //輸出哈弗曼編碼
{
	hc = new HCode[n];
	for (int i = 0; i<n; i++)
	{
		hc[i].data = ht[i].ch;
		int ic = i;
		int ip = ht[i].parent;
		int k = 0;
		while (ip != -1)
		{
			if (ic == ht[ip].lchild)   //左孩子標'0'
				hc[i].code[k] = '0';
			else
				hc[i].code[k] = '1';  //右孩子標'1'
			k++;
			ic = ip;
			ip = ht[ic].parent;
		}
		hc[i].code[k] = '\0';
		Reverse(hc[i].code);
	}
	for(int i=0;i<n;i++) cout<<"結點"<<hc[i].data<<"的字符編碼:"<<hc[i].code<<endl;
}
void TransCode(HfmTree ht[], HCode hc[], int n, char *s)    //哈夫曼譯碼
{
	cout << "解碼數據爲:";
	int i = 2 * (n - 1);      //根結點
	while (*s != '\0')
	{
		if (*s == '0')
			i = ht[i].lchild;
		else
			i = ht[i].rchild;
		if (ht[i].lchild == -1)
		{
			cout << ht[i].ch;
			i = 2 * n - 2;
		}
		s++;
	}
	cout << endl;
}
void menu(){
	cout << "\t\t◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆" << endl;
	cout << "\t\t*    ********哈夫曼編碼/譯碼器********        *" << endl;
	cout << "\t\t*             1.創建哈夫曼樹;                 *" << endl;
	cout << "\t\t*             2.進行哈夫曼編碼;               *" << endl;
	cout << "\t\t*             3.進行哈夫曼譯碼;               *" << endl;
	cout << "\t\t*             4.退出;                         *" << endl;
	cout << "\t\t◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆" << endl;
	cout << "\t\t       <注意>'+'代表' ','-'代表'!'          " << endl;
	cout << endl;
}
int main(){
	HfmTree ht[N]={};
	HCode hc[N];
	char s[N];
	int n;
	char x,y;
	menu();
	system("color b0");
	while (true){
		///menu();
		int op;
		cout << "<>請選擇你要進行的功能<1.創建,2.編碼,3.譯碼,4.退出>:";
		cin >> op;
		switch (op){
		case 1:{
		          cout<<"<>輸入結點個數:";
				   cin>>n;
				   Great_hfmtree(ht,n);
				   printf("創建哈夫曼樹成功!\n\n");
				   printf("結點i\t字符\t權值\t左孩子\t右孩子\t雙親結點\n");
				   for (int i = 0; i<2 * n - 1; i++)
					   cout << i << "\t" << ht[i].ch << "\t" << ht[i].weight << "\t" << ht[i].lchild << "\t" << ht[i].rchild << "\t" << ht[i].parent << endl;
				   puts("");
				   break;
		}
		case 2: {
					HfmCode(ht,hc,n);
					puts("");
					break;
		}
		case 3: {
					cout << "輸入要進行譯碼的字符串:";
					cin >> s;
					TransCode(ht, hc, n, s);
					cout<<"譯碼成功!";
					//cout << a << endl;
					cout << endl;
					break;
		}
		case 4:{
				   cout << "退出成功!" << endl;
				   exit(0);
				   break;
		}
		default:
			break;
		}
	}
}
/*
顏色屬性由兩個十六進制數字指定 -- 第一個爲背景,第二個則爲
前景。每個數字可以爲以下任何值之一:

    0 = 黑色       8 = 灰色
    1 = 藍色       9 = 淡藍色
    2 = 綠色       A = 淡綠色
    3 = 淺綠色     B = 淡淺綠色
    4 = 紅色       C = 淡紅色
    5 = 紫色       D = 淡紫色
    6 = 黃色       E = 淡黃色
    7 = 白色       F = 亮白色

如果沒有給定任何參數,該命令會將顏色還原到 CMD.EXE 啓動時
的顏色。這個值來自當前控制檯窗口、/T 命令行開關或
DefaultColor 註冊表值。

如果用相同的前景和背景顏色來執行 COLOR 命令,COLOR 命令
會將 ERRORLEVEL 設置爲 1。

例如: "COLOR fc" 在亮白色上產生亮紅色
*/

運行結果如下: 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章