數據結構(15)--哈夫曼樹以及哈夫曼編碼的實現

參考書籍:數據結構(C語言版)嚴蔚敏吳偉民編著清華大學出版社

本文中的代碼可從這裏下載:https://github.com/qingyujean/data-structure

1.哈夫曼樹

    假設有n個權值{w1, w2, ..., wn},試構造一棵含有n個葉子結點的二叉樹,每個葉子節點帶權威wi,則其中帶權路徑長度WPL最小的二叉樹叫做最優二叉樹或者哈夫曼樹

    特點:哈夫曼樹中沒有度爲1的結點,故由n0 = n2+1以及m= n0+n1+n2,n1=0可推出m=2*n0-1,即一棵有n個葉子節點的哈夫曼樹共有2n-1個節點。

2.哈夫曼編碼

    通信傳送的目標是使總碼長儘可能的短。

    變長編碼的原則:
    1.使用頻率高的字符用儘可能短的編碼(這樣可以減少數據傳輸量);
    2.任一字符的編碼都不能作爲另一個字符編碼的開始部分(這樣就使得在兩個字符的編碼之間不需要添加分隔符號)。這種編碼稱爲前綴編碼

    根據每種字符在電文中出現的次數構造哈夫曼樹,將哈夫曼樹中每個分支結點的左分支標上0,右分支標上1,把從根結點到每個葉子結點的路徑上的標號連接起來,作爲葉結點所代表的字符的編碼。這樣得到的編碼稱爲哈夫曼編碼

    思考爲什麼哈夫曼編碼符合變長編碼的原則?哈夫曼樹所構造出的編碼的長度是不是最短的?

     哈夫曼樹求得編碼爲最優前綴碼的原因: 在構造哈夫曼樹的過程中:

    1.權值大的在上層,權值小的在下層。滿足出現頻率高的碼長短。
 2.樹中沒有一片葉子是另一葉子的祖先,每片葉子對應的編碼就不可能是其它葉子編碼的前綴。即上述編碼是二進制的前綴碼。
    假設每種字符在電文中出現的次數爲wi (出現頻率即爲權值),其碼長爲li,電文中只有n種字符,則編碼後電文總碼長爲,而哈夫曼樹是WPL最小的二叉樹,因此哈夫曼編碼的碼長最小。

3.哈夫曼編碼實例

四種字符以及他們的權值:a:30, b:5, c:10, d:20

第一步:構建哈夫曼樹

第二步:爲哈夫曼樹的每一條邊編碼

第三步:生成哈夫曼編碼表

4.代碼實現

4.1哈夫曼樹定義

哈夫曼樹的存儲結構:採用靜態三叉鏈表

 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

#define N 4//帶權值的葉子節點數或者是需要編碼的字符數
#define M 2*N-1//n個葉子節點構造的哈夫曼樹有2n-1個結點
#define MAX 10000
typedef char TElemType;
//靜態三叉鏈表存儲結構
typedef struct{
	//TElemType data;
	unsigned int weight;//權值只能是正數
	int parent;
	int lchild;
	int rchild;
}HTNode;//, *HuffmanTree;
typedef HTNode HuffmanTree[M+1];//0號單元不使用

typedef char * HuffmanCode[N+1];//存儲每個字符的哈夫曼編碼表,是一個字符指針數組,每個數組元素是指向字符指針的指針

 

4.2構造哈夫曼樹

 

 

//構造哈夫曼樹
void createHuffmanTree(HuffmanTree &HT, int *w, int n){
	if(n <= 1)
		return;
	//對樹賦初值
	for(int i = 1; i <= n; i++){//HT前n個分量存儲葉子節點,他們均帶有權值
		HT[i].weight = w[i];
		HT[i].lchild = 0;
		HT[i].parent = 0;
		HT[i].rchild = 0;
	}
	for(; i <=M; i++){//HT後m-n個分量存儲中間結點,最後一個分量顯然是整棵樹的根節點
		HT[i].weight = 0;
		HT[i].lchild = 0;
		HT[i].parent = 0;
		HT[i].rchild = 0;
	}
	//開始構建哈夫曼樹,即創建HT的後m-n個結點的過程,直至創建出根節點。用哈夫曼算法
	for(i = n+1; i <= M; i++){
		int s1, s2;
		select(HT, i-1, s1, s2);//在HT[1...i-1]裏選擇parent爲0的且權值最小的2結點,其序號分別爲s1,s2,parent不爲0說明該結點已經參與構造了,故不許再考慮
		HT[s1].parent = i;
		HT[s2].parent = i;
		HT[i].lchild = s1;
		HT[i].rchild = s2;
		HT[i].weight = HT[s1].weight + HT[s2].weight;
	}
}

 

 

 

//在HT[1...k]裏選擇parent爲0的且權值最小的2結點,其序號分別爲s1,s2,parent不爲0說明該結點已經參與構造了,故不許再考慮
void select(HuffmanTree HT, int k, int &s1, int &s2){
	//假設s1對應的權值總是<=s2對應的權值
	unsigned int tmp = MAX, tmpi = 0;
	for(int i = 1; i <= k; i++){
		if(!HT[i].parent){//parent必須爲0
			if(tmp > HT[i].weight){
				tmp = HT[i].weight;//tmp最後爲最小的weight
				tmpi = i;
			}
		}
	}
	s1 = tmpi;
	
	tmp = MAX;
	tmpi = 0;
	for(i = 1; i <= k; i++){
		if((!HT[i].parent) && i!=s1){//parent爲0
			if(tmp > HT[i].weight){
				tmp = HT[i].weight;
				tmpi = i;
			}
		}
	}
	s2 = tmpi;
}

 

 

打印哈夫曼樹

 

 

//打印哈夫曼滿樹
void printHuffmanTree(HuffmanTree HT, char ch[]){
	printf("\n");
	printf("data, weight, parent, lchild, rchild\n");
	for(int i = 1; i <= M; i++){
		if(i > N){
			printf("  -, %5d, %5d, %5d, %5d\n", HT[i].weight, HT[i].parent, HT[i].lchild, HT[i].rchild);
		}else{
			printf("  %c, %5d, %5d, %5d, %5d\n", ch[i], HT[i].weight, HT[i].parent, HT[i].lchild, HT[i].rchild);
		}
	}
	printf("\n");
}

 

4.3編碼

 

爲哈夫曼樹的每一條分支編碼,並生成哈夫曼編碼表HC

 

//爲每個字符求解哈夫曼編碼,從葉子到根逆向求解每個字符的哈夫曼編碼
void encodingHuffmanCode(HuffmanTree HT, HuffmanCode &HC){
	//char *tmp = (char *)malloc(n * sizeof(char));//將每一個字符對應的編碼放在臨時工作空間tmp裏,每個字符的編碼長度不會超過n
	char tmp[N];
	tmp[N-1] = '\0';//編碼的結束符
	int start, c, f;
	for(int i = 1; i <= N; i++){//對於第i個待編碼字符即第i個帶權值的葉子節點
		start = N-1;//編碼生成以後,start將指向編碼的起始位置
		c = i;
		f = HT[i].parent;

		while(f){//f!=0,即f不是根節點的父節點
			if(HT[f].lchild == c){
				tmp[--start] = '0';
			}else{//HT[f].rchild == c,注意:由於哈夫曼樹中只存在葉子節點和度爲2的節點,所以除開葉子節點,節點一定有左右2個分支
				tmp[--start] = '1';
			}
			c = f;
			f = HT[f].parent;
		}
		HC[i] = (char *)malloc((N-start)*sizeof(char));//每次tmp的後n-start個位置有編碼存在
		strcpy(HC[i], &tmp[start]);//將tmp的後n-start個元素分給H[i]指向的的字符串
	}
}

打印哈夫曼編碼表,當編碼表生成以後,以後就可以對字符串進行編碼了,只要對應編碼表進行轉換即可

 

 

//打印哈夫曼編碼表
void printHuffmanCoding(HuffmanCode HC, char ch[]){
	printf("\n");
	for(int i = 1; i <= N; i++){
		printf("%c:%s\n", ch[i], HC[i]);
	}
	printf("\n");
}

 

4.4解碼

 

 

//解碼過程:從哈夫曼樹的根節點出發,按字符'0'或'1'確定找其左孩子或右孩子,直至找到葉子節點即可,便求得該字串相應的字符
void decodingHuffmanCode(HuffmanTree HT, char *ch, char testDecodingStr[], int len, char *result){
	int p = M;//HT的最後一個節點是根節點,前n個節點是葉子節點
	int i = 0;//指示測試串中的第i個字符
	//char result[30];//存儲解碼以後的字符串
	int j = 0;//指示結果串中的第j個字符
	while(i<len){
		if(testDecodingStr[i] == '0'){
			p = HT[p].lchild;
		}
		if(testDecodingStr[i] == '1'){
			p = HT[p].rchild;
		}

		if(p <= N){//p<=N則表明p爲葉子節點,因爲在構造哈夫曼樹HT時,HT的m個節點中前n個節點爲葉子節點
			result[j] = ch[p];
			j++;
			p = M;//p重新指向根節點
		}
		i++;
	}
	result[j] = '\0';//結果串的結束符	
}

 

4.5演示

 

 

void main(){
	HuffmanTree HT;
	
	TElemType ch[N+1];//0號單元不使用,存儲n個等待編碼的字符
	int w[N+1];//0號單元不使用,存儲n個字符對應的權值
	printf("請輸入%d個字符以及該字符對應的權值(如:a,20):\n", N);
	for(int i = 1; i <= N; i++){
		scanf("%c,%d", &ch[i], &w[i]);
		getchar();//吃掉換行符
	}//即w裏第i個權值對應的是ch裏第i個字符元素


	createHuffmanTree(HT, w , N);//構建哈夫曼樹
	printHuffmanTree(HT, ch);
	
	HuffmanCode HC;//HC有n個元素,每個元素是一個指向字符串的指針,即每個元素是一個char *的變量
	encodingHuffmanCode(HT, HC);//爲每個字符求解哈夫曼編碼
	printHuffmanCoding(HC, ch);

	//解碼測試用例:abaccda----01000101101110
	char * testDecodingStr = "01000101101110";
	int testDecodingStrLen = 14;
	printf("編碼%s對應的字符串是:", testDecodingStr);
	char result[30];//存儲解碼以後的字符串
	decodingHuffmanCode(HT, ch, testDecodingStr, testDecodingStrLen, result);//解碼(譯碼),通過一段給定的編碼翻譯成對應的字符串
	printf("%s\n", result);
}


 

 

 

 

 

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