数据结构基础:P5.2-树(三)--->哈夫曼树与哈夫曼编码

本系列文章为浙江大学陈越、何钦铭数据结构学习笔记,前面的系列文章链接如下
数据结构基础:P1-基本概念
数据结构基础:P2.1-线性结构—>线性表
数据结构基础:P2.2-线性结构—>堆栈
数据结构基础:P2.3-线性结构—>队列
数据结构基础:P2.4-应用实例—>多项式加法运算
数据结构基础:P2.5-应用实例—>多项式乘法与加法运算-C实现
数据结构基础:P3.1-树(一)—>树与树的表示
数据结构基础:P3.2-树(一)—>二叉树及存储结构
数据结构基础:P3.3-树(一)—>二叉树的遍历
数据结构基础:P3.4-树(一)—>小白专场:树的同构-C语言实现
数据结构基础:P4.1-树(二)—>二叉搜索树
数据结构基础:P4.2-树(二)—>二叉平衡树
数据结构基础:P4.3-树(二)—>小白专场:是否同一棵二叉搜索树-C实现
数据结构基础:P4.4-树(二)—>线性结构之习题选讲:逆转链表
数据结构基础:P5.1-树(三)—>堆


一、什么是哈夫曼树

1.1 为什么需要哈夫曼树

编码这个问题是计算机里面最最基础的一个也是比较核心的一个问题。

----编码有很多种方式,比方说我们的整数用二进制的形式0101存在我们计算机里面。我们有一种编码叫等长码,每个ASCII 码可以用7位来进行编码。如果有一篇文章有1万个字符,那么就是要7万位进行编码。当然计算机里面,实际上一个ASCII码占一个字节,也就是说总共占8位,最高那位是0。那么一万个字符所构成的一篇文章就是总共占用的是10000个字节。
----而实际上,我们每个字符出现的频率是不一样的。比方说 e 这个字符里出现的频率要大于很多字符的频率。如果我们能够用不等长编码,比方说我们 e 既然出现的频率高,我用5位进行编码。频率出现没那么高的用6位7位8位甚至9位10位都没关系,这样的话效率就能得到提高。所以由于这样的一种背景,大家就在想:已知的频率不一样,我应该怎么进行编码能达到比较好的效果。这就是树跟哈夫曼编码所涉及到的一个重要的问题。


我们来看一个具体的一个例子:将百分制的考试成绩转换成五分制的成绩

我们写出对应的C语言代码如下:

if( score < 60 ) grade =1;
else if( score < 70 ) grade =2;
else if( score < 80 ) grade =3;
else if( score < 90 ) grade =4;
else grade =5;

上面的判别规则实际上对应一棵判定树。
在这里插入图片描述
在这个判定树里面,我们看到判断不及格的时候只需要做一步判断,90多分的时候我要做4次判断。如果我们的成绩绝大多数都是90分,60分的人很少,我们会发现绝大多数人都要做四步判断才能得出结论,而我们需要判断一步的人非常少甚至没有。显然这样的一种判定树是不够优化的。


我们考虑每个分数段要查询的频率是不一样的,下面这个表中给出了这个不同分数段将来可能会查询的一个频率。
在这里插入图片描述
那么对应的查找效率(这些比例乘它对应的比较次数)为: 0.05 × 1 + 0.15 × 2 + 0.4 × 3 + 0.3 × 4 + 0.1 × 4 = 3.15 0.05×1+0.15×2+0.4×3+0.3×4+0.1×4 = 3.15 0.05×1+0.15×2+0.4×3+0.3×4+0.1×4=3.15


同样的这样的一种分布,我们换一种判别的这个流程。
在这里插入图片描述
对应的C语言代码如下:

if( score < 80 ) 
{ 
	if( score < 70 )
		if( score < 60 ) grade =1;
		else grade = 2;
	else grad=	3;
}else if( score < 90 ) grade =4;
else grade =5;

此时的查找效率为: 0.05 × 3 + 0.15 × 3 + 0.4 × 2 + 0.3 × 2 + 0.1 × 2 = 2.2 0.05×3+0.15×3+0.4×2+0.3×2+0.1×2 = 2.2 0.05×3+0.15×3+0.4×2+0.3×2+0.1×2=2.2


这个就给我们一个启示:我们可以有不同的方法来构造搜索树,而不同搜索树它的效率是不一样的。所以怎么根据不同的频率来构造一个效率比较好甚至是最好的搜索树,这个就是哈夫曼树要解决的问题。


1.2 哈夫曼树的定义

带权路径长度(WPL):设二叉树有 n \rm{n} n 个叶子结点,每个叶子结点带有权值(频率) w k {w_k} wk,从根结点到每个叶子结点的长度为 l k {l_k} lk,则每个叶子结点的带权路径长度之和就是 W P L = ∑ k = 1 n w k l k WPL = \sum\limits_{k = 1}^n {{w_k}{l_k}} WPL=k=1nwklk
最优二叉树或哈夫曼树: WPL最小的二叉树

举个简单例子

有五个叶子结点,它们的权值为{1,2,3,4,5},用此权值序列可以构造出形状不同的多个二叉树。
在这里插入图片描述
他们对应的WPL分别为:
W P L 1 = 5 × 1 + 4 × 2 + 3 × 3 + 2 × 4 + 1 × 4 = 34 WP{L_1} = 5 \times 1 + 4 \times 2 + 3 \times 3 + 2 \times 4 + 1 \times 4 = 34 WPL1=5×1+4×2+3×3+2×4+1×4=34
W P L 2 = 1 × 1 + 2 × 2 + 3 × 3 + 4 × 4 + 5 × 4 = 50 WP{L_2} = 1 \times 1 + 2 \times 2 + 3 \times 3 + 4 \times 4 + 5 \times 4 = 50 WPL2=1×1+2×2+3×3+4×4+5×4=50
W P L 3 = 1 × 3 + 2 × 3 + 3 × 2 + 4 × 2 + 5 × 2 = 33 WP{L_3} = 1 \times 3 + 2 \times 3 + 3 \times 2 + 4 \times 2 + 5 \times 2 = 33 WPL3=1×3+2×3+3×2+4×2+5×2=33


二、哈夫曼树的构造

2.1 哈夫曼树的构造

哈夫曼树的构造过程可以用一句话总结:每次把权值最小的两棵二叉树合并

具体流程如下

假设我现在有1 2 3 4 5排好序的这5个权值
在这里插入图片描述
我先把权值最小的1和2合并,得到了一个新的结点3,这个结点的权值就是1跟2的和。
在这里插入图片描述
然后再从新的结点与剩余节点挑两个最小的,即3 3 4 5里面挑两个(3 3)。再合并,就得到了新的结点6。
在这里插入图片描述
从6 4 5里面再挑两个最小的进行合并(4 5)。得到一个新结点9。
在这里插入图片描述
最后只剩两个节点6 9,直接合并。这就是最终的哈夫曼树。
在这里插入图片描述

在上面的构造过程中,最重要的需要解决的一个问题就是:

给你N个数,我要找两个最小的,合并完了之后形成新的值,实际上就是堆要解决的问题。如果我们能够把这个结点的权值构造成一个最小堆,从里面挑两个最小的并在一起形成一个新的结点,再把它插到堆里去,那么用堆就能很好的实现。当然刚才那个过程也可以用排序的方法从小到大排好,删除两个小的并让其合在一起,然后再把一个新的结点再插到已经排序好的结点序列里去。显然这样的一种做法没有直接用堆效率要高。

哈夫曼树构造过程对应代码如下:

typedef struct TreeNode *HuffmanTree;
struct TreeNode{
	int Weight;
	HuffmanTree Left, Right;
}

HuffmanTree Huffman( MinHeap H )
{ /* 假设H->Size个权值已经存在H->Elements[]->Weight里 */
	int i; HuffmanTree T;
	BuildMinHeap(H); /*将H->Elements[]按权值调整为最小堆*/
	for (i = 1; i < H->Size; i++) { /*做H->Size-1次合并*/
		T = malloc( sizeof( struct TreeNode) ); /*建立新结点*/
		T->Left = DeleteMin(H);
		/*从最小堆中删除一个结点,作为新T的左子结点*/
		T->Right = DeleteMin(H);
		/*从最小堆中删除一个结点,作为新T的右子结点*/
		T->Weight = T->Left->Weight+T->Right->Weight;
		/*计算新权值*/
		Insert( H, T ); /*将新T插入最小堆*/
	}
	T = DeleteMin(H);
	return T;
}

可以看出循环 n − 1 \rm{n-1} n1 次,每次插入删除操作复杂度不超过 l o g 2 N {\rm{lo}}{{\rm{g}}_{\rm{2}}}{\rm{N}} log2N,因此总体复杂度为: O ( N l o g N ) {\rm{O(NlogN)}} O(NlogN)


2.2 哈夫曼树的特点:

①没有度为1的结点;
n \rm{n} n 个叶子结点的哈夫曼树共有 2 n − 1 \rm{2n-1} 2n1 个结点;
③哈夫曼树的任意非叶结点的左右子树交换后仍是哈夫曼树;
④对同一组权值 { w 1 , w 2 , . . . . . . , w n } {\rm{\{ }}{{\rm{w}}_{\rm{1}}}{\rm{,}}{{\rm{w}}_{\rm{2}}}{\rm{,}}......{\rm{,}}{{\rm{w}}_{\rm{n}}}{\rm{\} }} {w1,w2,......,wn},是有可能存在不同构的两棵哈夫曼树的。但他们的WPL相等。
例如:对一组权值{ 1, 2 , 3, 3 },不同构的两棵哈夫曼树:
在这里插入图片描述


三、哈夫曼编码

:假设有一段文本,包含58个字符,并由以下7个字符构成:a,e,i,s,t,空格(sp),换行(nl);这7个字符出现的次数不同。如何对这7个字符进行编码,使得总编码空间最少?

分析

①用等长ASCII编码:58 ×8 = 464位;
②用等长3位编码:58 ×3 = 174位;
③不等长编码:出现频率高的字符用的编码短些,出现频率低的字符则可以编码长些


我们看看如何进行不等长的编码

比方说我们想对前面的7个字符进行编码,我们是否能够按照下面这种规则进行编码?
在这里插入图片描述
答案是:不能。当我给你一个字符串的序列1011的时候,你怎么理解这个串?你有很多理解方法:
在这里插入图片描述
这种情况就是叫二义性。我们该如何避免这种情况呢?只要这个编码满足前缀码这样一种条件就不会有二义性。所谓前缀码就是任何字符的编码都不是另外一个字符串编码的前缀。刚才那个例子中。对于 s(10)来讲,它的前缀1可以理解为 a,所以 a 变成了 s 的一个前缀了,这样的话就会出现我们所说的二义性。


如何保证我们这个编码不会出现二义性

我们可以用二叉树来表示我们的编码:
(1)左右分支:0、1
(2)字符只在叶结点上
在这里插入图片描述
所有的结点都在叶结点上的时候,就不可能会出现一个字符的编码是另外一个字符的前缀。当你的编码有一个结点出现在另外一个结点的编码当中的时候(a出现在s和t中)就会出现前缀。
在这里插入图片描述


二叉树的构造方法

假设现在各个字符出现的频率如下:
a = 4 , u = 1 , x = 2 , z = 1 \rm{a=4, u=1, x=2, z=1} a=4,u=1,x=2,z=1
刚才的那种树对应的代价为16
在这里插入图片描述
如果我们换一种树的构造方法,如下所示,代价为14
在这里插入图片描述


如何选择最优的构造方法

在前面例子中,我们7个字符在58个字符串中出现的次数如下:
在这里插入图片描述
我们先构造哈夫曼树
①首先我们将这些字符排列:
在这里插入图片描述
②我们先挑两个最小的,1和3
在这里插入图片描述
③然后构造一个新的结点4
在这里插入图片描述
④然后再找两个最小的结点,4和4,构造一个新结点8
在这里插入图片描述
⑤然后再找两个最小的结点,8和10,构造一个新结点18
在这里插入图片描述
⑥再找两个最小的结点,12和13,构造一个新结点25
在这里插入图片描述
⑦再找两个最小的结点,15和18,构造一个新结点25
在这里插入图片描述
⑧最后将25和33合并
在这里插入图片描述
所以对应编码就出来了
在这里插入图片描述
对应的代价值是146
在这里插入图片描述


四、小测验

1、如果哈夫曼树有67个结点,则可知叶结点总数为:

A. 22
B. 33
C. 34
D. 不确定

答案:C

2、为五个使用频率不同的字符设计哈夫曼编码,下列方案中哪个不可能是哈夫曼编码?

A. 00,100,101,110,111
B. 000,001,01,10,11
C. 0000,0001,001,01,1
D. 000,001,010,011,1

答案:A

3、一段文本中包含对象{a,b,c,d,e},其出现次数相应为{3,2,4,2,1},则经过哈夫曼编码后,该文本所占总位数为:

A. 12
B. 27
C. 36
D. 其它都不是

答案:B

数据结构算法分析》实验报告 姓名 学号_ _____ __年 __月__ __日 上机题目:以静态链表为存储结构,编写给定权值{7,19,2,6,32,3}构造哈夫曼树算法。 (输出以存储结构表示或以型显示(90度旋转)) 需求分析 1. 输入数据必须为int的整形数据,其数值范围为:-~47 2. 输出的数据格式为:%d 3. 测试数据的数据为:{7,19,2,6,32,3} 详细设计 1. 该程序采用顺序表的存储结构,其数据结构定义如下: #define n 6 #define m 2*n-1 #define max 100typedef struct {int data; int lchild,rchild,prnt; }hufmtree; 所用数据类型中每个操作的伪码算法如下: 创建哈夫曼树 Program hufm(hufmtree t[m]) FOR i=0;i<m;i++ TO t[i].data=0; t[i].lchild=0; t[i].rchild=0; t[i].prnt=0; End FOR 输入结点值 FOR i=n;i<m;i++ TO p1=0;p2=0; small1=max;small2=max FOR j=0;j<=i-1;j++ TO IFt[j].prnt?=0 IF(t[j].data<small1) small2=small1; small1=t[j].data; p2=p1; p1=j;} ELSE IF(t[j].data<small2) small2=t[j].data; p2=j; t[p1].prnt=i+1; t[p2].prnt=i+1; t[i].lchild=p1+1; t[i].rchild=p2+1; t[i].data=t[p1].data+t[p2].data; END IF END FOR END Hufman 调试分析 1. 调试过程中主要遇到哪些问题?是如何解决的? 开始的时候main函数的数据结构类型定义的与主函数不同,而且缺少返回值,导致最 后的结果陷入死循环,通过看书,向同学询问,得以解决。 2. 经验和体会 哈夫曼树又称最优二叉树,此次实验创建哈夫曼树算法,虽然依旧犯了不少错误,但 仍解决了。在其中学习了很多,对有了更深的了解。 测试结果 附件 见 058詹奇.Cpp ----------------------- 数据结构算法分析实验报告全文共3页,当前为第1页。 数据结构算法分析实验报告全文共3页,当前为第2页。 数据结构算法分析实验报告全文共3页,当前为第3页。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知初与修一

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值