数据结构笔记3(哈夫曼树和哈夫曼编码)

哈夫曼树的目的是构造效率更高的搜索树

定义
在这里插入图片描述

例如
在这里插入图片描述
如果换一种排练顺序,即把权值低的放在浅层,权值高的放在深层。如下图,此时的WPL为50
在这里插入图片描述
所以如何构造一个让WPL最小的树,就是哈夫曼树解决的问题。

哈夫曼树的构造
先把各节点按从小到大进行排序;
每次把权值最小的两个节点进行合并,并在一起,形成新的二叉树,该二叉树的权值就是并在一起的两个节点的权值之和
如下图,将1 和 2 并在一起形成了新的二叉树,二叉树权值为3.
在这里插入图片描述

接下来从新的权值,也就是3 3 4 5四个里面找两个最小的,再进行合并,如下图,合并得到6
在这里插入图片描述

从6 4 5 中再挑两个,得到9
在这里插入图片描述
再次合并,得到结果
在这里插入图片描述

代码实现:
为了实现每次找到两个权值最小的节点并合并,有两种办法,一种是每次取出节点前对所有点进行排序,另一种是将该序列构建成最小堆再进行操作。显然后者的方法更便捷。
所以代码实现时先构建成最小堆,再每次从堆里取出权值最小的节点,构建有三个节点小哈夫曼树,将其插入最小堆中。不断执行上述操作,执行n-1次(n个点,就要比较n-1次),最终该最小堆就变成了哈夫曼树。

在这里插入图片描述时间复杂度:循环N次,每次循环的时间复杂度不超过log 2 N ,所以为Nlog 2 N

哈夫曼树的特点

  1. 没有度为1的节点 (因为每次都是两个节点并在一起形成新的节点)
  2. 有n个叶子节点的哈夫曼树共有2n-1个结点(前面讲过二叉树中节点分为 n0:叶节点总数,n1:只有一个儿子的节点总数,n2:有两个儿子的节点总数,其中n2=n01-1,哈夫曼树由于没有度为1的节点,所以总的节点数就是n0+n2,即2n-1)
  3. 哈夫曼树的任意非叶节点的左右子树交换后仍旧是哈夫曼树(因为哈夫曼树本就是两个节点合并得到的,并未强调左右关系)
  4. 对同一组权值{w1,w2,…,wn},存在不同构的哈夫曼树但其WPL相同。

    在这里插入图片描述
    哈夫曼编码

给定一串字符串,如何对其编码,使其最终占用的存储空间最小?
在这里插入图片描述
上面的例子,有三种编码方案如下,明显第三种编码方式最好
在这里插入图片描述
但是这种方法同样有很多问题,比如下图,对a编码为1,e为0,s为10,t为11,那么如何理解一串编码如1011?
在这里插入图片描述
可以看到这里可以理解为aeaa,aet,st三种序列,产生歧义。,也就是我们说的二义性。要解决二义性问题,该编码方式需要解决前缀码这一问题:
在这里插入图片描述
上图中a是1,而s是10,a的编码是s的前缀,所以会出现二义性。采用二叉树进行编码能够解决这一问题。

在这里插入图片描述

如上图,进行编码的二叉树左右分支分别代表0和1,而要进行编码的字符只在叶节点上。如上图这里
a:00 u:01 x:10 z:11
可以看到这四个字符的编码没有出现前缀码问题。而如果调整一下结构,把a放在这里,a和s,t就会出现二义性问题。如下图:

在这里插入图片描述

所以解决该问题的关键是进行编码的字符只放在叶节点上。
同时,在构造树时为了让代价最小,所以应该对每个字符出现的频率进行统计,出现频率高的字符放在浅层的叶节点(这样该字符的编码位数更少),频率低的放在深层的叶节点。
如下图,左图是按普通的方法构造,右图是根据频率,频率高的放在上层,比如a,相比之下这样编码占用的空间代价就会变小很多。
在这里插入图片描述
可以看到,构造一棵这样代价最小的二叉树,恰好是构造哈夫曼树的流程

一个例子如下。七个字符和其出现的频率列成表格在图上。根据该表格构造一个哈夫曼树(先挑两个频率最小的,合并,再挑两个,再合并…)最终构造哈夫曼树如下。构造好之后左右分支分别为0,1,就得到了每个字符的编码,如图右侧:

在这里插入图片描述

练习题:
在这里插入图片描述

解析:在这里插入图片描述

集合的表示及数组存储树

有时需要查询存储在树中的某个节点属于哪个集合,如下图:

在这里插入图片描述

图中有S1,S2,S3三个集合,分别存在三个子树中。与二叉树等不同,这里采用双亲表示法,即每个儿子节点都有一个指针指向其父亲,如果要查询2属于哪个集合,可以马上得到它的根节点是1,而1属于集合S1。

对于这样的树,可以很容易用数组进行存储,如下:

在这里插入图片描述

如图所示,每个节点有两个值,一个是Data存储数据,还有一个Parent,用来存储某个数组元素的下标,即存储其父节点的位置。
以这里的2为例,黄色一行可以看到2这个点存储的parent为0,可以看到下标为0的点即为1,这样已知一个节点,就很容易找到他的根节点
而对于值为1,3,6这三个点,他们没有父节点,所以其parent表示为 -1
这样就用一个简单的结构数组表示了该结构。

查找操作

在这里插入图片描述

代码实现如上图,要查找值为X的元素所属的集合,首先从数组的头开始遍历找到该元素的位置,找到后再用另一个for循环向上找他的父亲节点,只要当前节点的Parent>0就继续往上查,直到某个节点的Parent为-1,说明找到了这棵树的根节点,即找到了所属集合。

集合的并运算

例如给出两个元素,把两个元素所在的集合并起来。首先找到两个元素的位置再找到他们的根节点,最后将其中一棵树的根节点的Parent指针设为另一棵树的根节点的下标即可。具体步骤如下:

在这里插入图片描述

用find()操作找到两棵树的树根,接下来判断是否为同一棵树,如果不是就改变其中一棵树的根节点的Parent指向。比如下图,将2和5所在的集合合并,即让3的parent指向1
在这里插入图片描述

这样直接进行合并也会导致一些问题,如导致树的深度越来越大,执行find()时开销越来越大。所以要尽可能让小的树挂在大的树上,这样就相对减少了树的深度的增加。判断树的大小应该通过这棵树的元素多少。想到采用另一种办法:
即本来的根节点的parent为-1,现在这个值用来表示元素数量。如下图:
在这里插入图片描述
这样每次比较数量大小,再进行合并。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值