哈夫曼树和哈夫曼编码

基本术语
1、路径和路径长度
在一棵树中存在着一个节点序列K1,K2,K3…Kj,使得Ki是Ki+1的双亲,则称此节点序列是从K1~Kj的路径,因为树中每个节点只有一个双亲节点,所以他也是这两个之间的唯一路径,从K1~Kj所经过的分支数称为这两个节点之间的路径长度,他等于路径上的节点数减1.

2、节点权和带权路径度
在许多应用中将树中的节点赋值一个有意义的实数,称此实数为该节点的权,节点的带权路径长度规定为从树根节点到该节点之间的路径长度与该节点权的乘积。

3、树的带权路径长度
树的带权路径长度定义为树中所有叶子节点的带权路径长度之和,通常记为:

4、哈夫曼树(Huffman tree)
哈夫曼树又称为最优二叉树。他是n个带权叶子节点构成的所有二叉树中,带权路径长度WPL最小的二叉树。因为构造这种树的算法最早是哈夫曼提出,所以称为哈夫曼树。

例如,有4个叶子节点a,b,c,d。分别带权为9,4,5,2由他们构成三颗不同的二叉树,如下 

每一颗二叉树的带权路径长度WPL为:

(1)、WPL=9*2+4*2+5*2+2*2=40

(2)、WPL=9*1+2*2+5*3+9*3=50

(3)、WPL=4*1+5*2+4*3+2*3=37

其中(3)的WPL最小。

因此,在n个带权叶子节点所构成的二叉树中,满二叉树或者完全二叉树不一定是最优二叉树,权值越大离根节点越近的树才是最优二叉树。

构造哈夫曼树
 

构造最优二叉树的具体算法叙述如下:

(1)、n个权值的节点构造只有一个节点的树集合F

(2)、在F中选出两个权值最小的树作为一颗新树的左右子节点,且设置新树的根节点是左右子节点权值之和。

(3)、新树加入F,同时删除已被选中的两颗树

(4)、重复2和3,直到F中只有一棵树为止。

根据上述的算法描述,构造哈夫曼树的图示如下:

哈夫曼编码
哈夫曼树的应用很广,哈夫曼编码就是其中一种。

在电报通信中,电文是以二进制的0,1序列传送的,在发送端需要将电文中的字符转成二进制的0,1序列(即编码),在接收端又需要将接收到的0,1转换成对应的字符序列(即译码)。

最简单的二进制编码方式是登场编码,若电文中只使用A,B,C,D,E,F这6个字符,若进行等长编码,则需要二进制的三位即可,依次编码为:000,001,010,011,100,101,110。若用这6个字符作为6个叶子节点,生成一颗二叉树,让该二叉树的的左右节点分别用0,1编码,从树根节点到每个叶子节点的路径上所经的0,1编码序列等于该叶子节点的二进制编码,则对应的编码二叉树,如下图:

通常,电文中每个字符出现的频率是不同的,在一份电报中,这个6个字符额出现频率依次为:4,2,6,8,3,2,则电文被编码后的总长度可以有计算出:

L=3*(4+2+6+8+3+2)=75

可知,采用等长编码时,传送电文的总长度为75。

那么是否可以缩短传送电文的长度呢?若采用不等长编码,让出现频率高的字符具有较短的编码,让出现频率低的字符具有较长的编码,这样有可能缩短电文的总长度。采用不等长编码要避免译码时的多义性,假设用0表示D,用01表示C,则当接收到编码后的字符串….01…时是按照0对应D译码还是按照01对应C译码,这里就产生了多义性问题。因此对一个字符集进行不等长编码时,则要求字符集中任意一个字符的编码都不能是其他字符的前缀,符合此要求的编码属于无前缀编码。

为了使不等长编码成为无前缀编码,可用该字符集中的每个字符作为叶子节点生成一颗编码二叉树。为了获得电文的最短长度,可以将每个字符出现的频率作为叶子节点的权值,求出此树的最小带权路径长度就等于求出了传送电文的最短长度。

由前面生成哈夫曼编码树的算法可以生成如下:

由哈夫曼树得到的字符编码就称为哈夫曼编码。其中A,B,C,D,E,F这6个字符对应的哈夫曼编码依次为:00,1010,01,11,100,1011。电文最终传送的长度为:

4*2+2*4+6*2+8*2+3*3+2*4=61,比等长编码时的75减少了不少。
 

哈夫曼树创建的算法实现

/*在ht[1]至ht[n]的范围内选择两个parent为0且weight最小的结点,其序号分别赋给s1,s2*/
void Select(HuffmanTree ht, int n, int* s1, int* s2) {
	int i, min1 = MAX, min2 = MAX;
	*s1 = 0;
	*s2 = 0;
	for (i = 1; i <= n; i++) {
		if (ht[i].parent == 0) {
			if (ht[i].weight < min1) {
				min2 = min1;
				*s2 = *s1;
				min1 = ht[i].weight;
				*s1 = i;
			}
			else if (ht[i].weight < min2) {
				min2 = ht[i].weight;
				*s2 = i;
			}
		}
	}
}

/*创建哈夫曼树算法*/
void CrtHuffmanTree(HuffmanTree ht, int w[], int n) {
//构造哈夫曼树ht[M+1],w[]存放n个权值
	int i;
	for (i = 1; i <= n; i++) {		//1至n号单元存放叶子结点,初始化
		ht[i].weight = w[i - 1];
		ht[i].parent = 0;
		ht[i].LChild = 0;
		ht[i].RChild = 0;
	}
	int m = 2 * n - 1;				//所有结点总数
	for (i = n + 1; i <= m; i++) {	//n+1至m号单元存放非叶结点,初始化
		ht[i].weight = 0;
		ht[i].parent = 0;
		ht[i].LChild = 0;
		ht[i].RChild = 0;
	}

	/*初始化完毕,开始创建非叶结点*/
	int s1, s2;
	for (i = n + 1; i <= m; i++) {	//创建非叶结点,建哈夫曼树
		Select(ht, i - 1, &s1, &s2);//在ht[1]至ht[i-1]的范围内选择两个parent为0且weight最小的结点,其序号分别赋给s1,s2
		ht[i].weight = ht[s1].weight + ht[s2].weight;
		ht[s1].parent = i;
		ht[s2].parent = i;
		ht[i].LChild = s1;
		ht[i].RChild = s2;
	}
}

哈夫曼编码的代码实现:

/*哈夫曼编码*/
void CrtHuffmanCode(HuffmanTree ht, int n) {
//从叶子结点到根,逆向求每个叶子结点(共n个)对应的哈夫曼编码
	char* cd;
	cd = (char*)malloc(n * sizeof(char));	//分配当前编码的工作空间
	cd[n - 1] = '\0';						//从右向左逐位存放编码,首先存放编码结束符
	for (int i = 1; i <= n; i++) {			//求n个叶子结点对应的哈夫曼编码
		int start = n - 1, c = i, p = ht[i].parent;
		while (p != 0) {
			--start;
			if (ht[p].LChild == c)			//左分支标0
				cd[start] = '0';
			else
				cd[start] = '1';			//右分支标1
			c = p;							//向上倒堆
			p = ht[p].parent;
		}
		for (int j = 0; j < n; j++) {
			if (cd[j] == '0' || cd[j] == '1') {
				printf("%c", cd[j]);		//编码输出
			}
		}
		memset(cd, 0, n);
	}
}

运行结果

运行结果

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值