数据结构(十五)— 树结构之赫夫曼树及其应用

现在我们都是讲究效率的社会,什么都要求速度, 在不能出错的情况下,做任何事情都讲究越快越好。在计算机和互联网技术中,文本压缩就是一个非常重要的技术。 玩电脑的人几乎都会应用压缩和解压缩软件来处理文档。 因为它除了可以减少文档在磁盘上的空间外,还有重要的一点,就是我们可以在网络上以压缩的形式传输大量数据,使得保存和传递都更加高效。

那么压缩而不出错是如何做到的呢?简单说,就是把我们要压缩的文本进行重新编码,以减少不必要的空间。尽管现在最新技术在编码上已经很好很强大,但这一切都来自于曾经的技术积累,我们今天就来介绍一下最基本的压缩编码方法一一赫夫曼编码。

赫夫曼树

在介绍赫夫曼编码前,我们必须得介绍赫夫曼树,什么叫做赫夫曼树呢?我们先来看一个例子。
过去我们小学、中学一般考试都是用百分制来表示学科成绩的。对于老师来讲,他在对试卷评分的时候,显然不能凭感觉给优良或及格不及格等成绩,因此一般都还是按照百分制算出每个学生的成绩后,再根据统一的标准换算得出五级分制的成绩。比如下面的代码就实现了这样的转换。

if ( a < 60 ) {
  b = "不及格" ;
} else if ( a < 70 ){
  b = "及格" ;
} else if ( a < 80 ) {
  b = "中等“ ;
} else if ( a < 90 ) { 
  b = "良好 " ;
} else {
  b = "优秀" :
}  

根据上面的代码,转换成下图,粗略看没什么问题,可是通常都认为,一张好的考卷应该是让学生成绩大部分处于中等或良好的范围,优秀和不及格都应该较少才对。而上面这样的程序,就使得所有的成绩都需要先判断是否及格,再逐级而上得到结果。输入量很大的时候,其实算法是有效率问题的。在这里插入图片描述
如果在实际的学习生活中,学生的成绩在 5 个等级上的分布规律下图所示:
在这里插入图片描述
那么 70 分以上大约占总数 80% 的成绩都需要经过 3 次以上的判断才可以得到结果,这显然不合理。

有没有好一些的办法呢?仔细观察发现,中等成绩 (70-79 分之间)比例最高,其次是良好成绩,不及格的所占比例最少。我们把上面的二叉树重新进行分配。 改成如下图的做法试试看。
在这里插入图片描述
从图中感觉,应该效率要高一些了,到底高多少呢?这样的二叉树又是如何设计出来的呢?我们来看看赫夫曼大叔是如何说的吧。

赫夫曼树定义及原理

我们先把这两棵二叉树简化成叶子结点带权的二叉树,如下图所示。其中 A 表示不及格、 B 表示及格、 C 表示中等、 D 表示良好、 E 表示优秀。每个叶子的分支线上的数字就是刚才我们提到的五级分制的成绩所占比例数。
在这里插入图片描述
赫夫曼大叔说, 从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。上图的二叉树 a 中, 根结点到结点 D 的路径长度就为 4,二叉树 b 中根结点到结点 D 的路径长度为 2。 树的路径长度就是从树根到每一结点的路径长度之和 。 二叉树 a 的树路径长度就为 1+1+2+2+3+3+4+4=20。二叉树 b 的树路径长度就为1+2+3+3+2+1+2+2=16。

如果考虑到带权的结点,结点的带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积树的带权路径长度为树中所有叶子结点的带权路径长度之和。 假设有 n 个权值{W1,W2,…,Wn},构造一棵有 n 个叶子结点的二叉树,每个叶子结点带权 Wn,每个叶子的路径长度为 1k,则其中带权路径长度 WPL 最小的二叉树称做赫夫曼树。 也有不少书中也称为最优二叉树,我个人觉得为了纪念做出巨大贡献的科学家,既然用他们的名字命名,就应该要坚持用他们的名字称呼,哪怕 "最优"更能体现这棵树的品质也应该只作为别名。

有了赫夫曼对带权路径长度的定义,我们来计算一下上图这两棵树的 WPL值。
二叉树 a 的 WPL=5 * 1 + 15 * 2 + 40 * 3 + 30 * 4 + 10 *4 = 315
注意: 这里 5 是 A 结点的权, 1 是 A 结点的路径长度,其他同理。

二叉树 b 的 WPL= 5 * 3 + 15 * 3 + 40 * 2 + 30 * 2 + 10 * 2 = 220

这样的结果意味着什么呢?如果我们现在有 10000 个学生的百分制成绩需要计算五级分制成绩,用二叉树 a 的判断方法,需要做 31500 次比较,而二叉树 b 的判断方法,只需要 22000 次比较, 差不多少了三分之一量,在性能上提高不是一点点。

那么现在的问题就是, 上图的二叉树 b 这样的树是如何构造出来的,这样的二叉树是不是就是最优的赫夫曼树呢?别急,赫夫曼大叔给了我们解决的办法。

  1. 先把有权值的叶子结点按照从小到大的顺序排列成一个有序序列,即 :A5,E10 , B15 , 030, C40。
  2. 取头两个最小权值的结点作为一个新节点 N1 的两个子结点,注意相对较小的是左孩子,这里就是 A 为 N1 的左孩子, E 为 N1 的右孩子,如下图左边所示。新结点的权值为两个叶子权值的和 5+10=15。
  3. 将 N1 替换 A 与 E,插入有序序列中, 保持从小到大排列。 即: N1 15, B15 , 030, C40。
  4. 重复步骤 2. 将 N1 与 B 作为一个新节点 N2 的两个子结点。 如下图右边所示。 N2 的权值 = 15 + 15 = 30。
    在这里插入图片描述
  5. 将 N2 替换 N1 与 B ,插入有序序列中, 保持从小到大排列。 即: N2 30 , D30, C40。
  6. 重复步骤 2。 将 N2 与 D 作为一个新节点 N3 的两个子结点。 如下图左边所示。 N3 的权值 = 30 + 30 = 60。
  7. 将 N3 替换 N2 与 D,插入有序序列中,保持从小到大排列。即 : C40 , N3 60。
  8. 重复步骤 2。将 C 与 N3 作为一个新节点 T 的两个子结点,如下图右图所示。 由于 T 即是根结点,完成赫夫曼树的构造。
    在这里插入图片描述
    此时的上图二叉树的带权路径长度 WPL= 40 * 1 + 30 * 2 + 15 * 3 + 10 * 4 + 5 * 4 = 205,与最上面图中的二叉树 b 的 WPL 值 220 相比,还少了 15。显然此时构造出来的二叉树才是最优的赫夫曼树。

不过现实总是比理想要复杂得多,上图虽然是赫夫曼树,但由于每次判断都要两次比较(如根结点就是 a < 80 && a >= 70,两次比较才能得到 y 或 n 的结果) ,所以总体性能上,反而不如最上图的二叉树性能高。当然这并不是我们要讨论的重点了。

通过刚才的步骤,我们可以得出构造赫夫曼树的赫夫曼算法描述。

  1. 根据给定的 n 个权值{ W1,W2,…,Wn }构成 n 棵二叉树的集合 F = { T1,T2,…Tn}, 其中每棵二叉树 Ti 中只有一个带权为 Wi根结点,其左右子树均为空。
  2. 在 F 中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
  3. 在 F 中删除这两棵树,同时将新得到的二叉树加入 F 中。
  4. 重复 2 和 3 步骤,直到 F 只含一棵树为止。这棵树便是赫夫曼树。

赫夫曼编码

当然,赫夫曼研究这种最优树的目的不是为了我们可以转化一下成绩。 他的更大目的是为了解决当年远距离通信(主要是电报)的数据传输的最优化问题。

比如我们有一段文字内容为 " BADCADFEED " 要网络传输给别人,显然用二进制的数字 ( 0 和 1) 来表示是很自然的想法。我们现在这段文字只有六个字母 ABCDEF,那么我们可以用相应的二进制数据表示,如下图所示。
在这里插入图片描述
这样真正传输的数据就是编码后的 “ 001000011010000011101100100011 ” , 对方接收时可以按照 3 位一分来译码。如果一篇文章很长,这样的二进制串也将非常的可怕。 而且事实上,不管是英文、中文或是其他语言,字母或汉字的出现频率是不相同的,比如英语中的几个元音字母 " a e i o u “,中文中的” 的 了 有 在 "等汉字都是频率极高。

假设六个字母的频率为 A 27 , B 8, C 15 , D 15 , E 30, F 5 ,合起来正好是 100%。 那就意味着,我们完全可以重新按照赫夫曼树来规划它们。

下图左图为构造赫夫曼树的过程的权值显示。 右图为将权值左分支改为 0, 右分支改为 1 后的赫天曼树。
在这里插入图片描述
此时,我们对这六个字母用其从树根到叶子所经过路径的 0 或 1 来编码,可以得到如下表所示这样的定义。
在这里插入图片描述
我们将文字内容为 “BADCADFEED” 再次编码,对比可以看到结果串变小了 。
• 原编码二进制串 : 001000011010000011101100100011 (共 30 个字符)
• 新编码二进制率: 1001010010101001000111100 (共 25 个字符)

也就是说,我们的数据被压缩了,节约了大约 17%的存储或传输成本。 随着字符的增加和多字符权重的不同,这种压缩会更加显出其优势。

当我们接收到 100101001010100100011110。 这样压缩过的新编码时,我们应该如何把它解码出来呢?
编码中非 0 即 1,长短不等的话其实是很容易混淆的,所以若要设计长短不等的编码,则必须是任一字符的编码都不是另一个字符的编码的前缀,这种编码称做前缀编码。

你仔细观察就会发现,上表中的编码就不存在容易与 1001、 1000 混滑的 “10” 和 “100” 编码。
可仅仅是这样不足以让我们去方便地解码的,因此在解码时,还是要用到赫夫曼树,即发送方和接收方必须要约定好同样的赫夫曼编码规则。
当我们接收到 1001010010101001000111100 时,由约定好的赫夫曼树可知, 1001 得到第一个字母是 B,接下来01意味着第二个字符是 A,如下图所示, 其余的也相应的可以得到,从而成功解码。
在这里插入图片描述
一般地,设需要编码的字符集为{d1,d2,…,dn },各个字符在电文中出现的次数或频率集合为{ W1,W2,…,Wn} ,以 d1,d2…,dn 作为叶子结点,以 W1,W2,…,Wn 作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表 0,右分支代表 1, 则从根结点到叶子结点所经过的路径分支组成的 0 和 1 的序列便为该结点对应字符的编码,这就是赫夫曼编码。

总结回顾

终于到了总结的时间,这一章与前面章节相比,显得过于庞大了些,原因也就在于树的复杂性和变化丰富度是前面的线性表所不可比拟的。即使在本章之后,我们还要讲解关于树这一数据结构的相关知识,可见它的重要性。

开头我们提到了树的定义,讲到了递归在树定义中的应用。提到了如子树、结点、度、叶子、分支结点、双亲、孩子、层次、深度、森林等诸多概念,这些都是需要在理解的基础上去记忆的。

我们谈到了树的存储结构时,讲了双亲表示法、孩子表示法、孩子兄弟表示法等不同的存储结构。

并由孩子兄弟表示法引出了我们这章中最重要一种树,二叉树.

二叉树每个结点最多两棵子树,有左右之分。提到了斜树,满二叉树、完全二叉树等特殊二叉树的概念。

我们接着谈到它的各种性质,这些性质给我们研究二叉树带来了方便。

二叉树的存储结构由于其特殊性使得既可以用顺序存储结构又可以用链式存储结构表示。

遍历是二叉树最重要的一门学问,前序、中序、 后序以及层序遍历都是需要熟练掌握的知识。要让自己要学会用计算机的运行思维去模拟递归的实现,可以加深我们对递归的理解。不过,并非二叉树遍历就一定要用到递归,只不过递归的实现比较优雅而已。这点需要明确。

最后,我们提到了关于二叉树的一个应用,赫夫曼树和赫夫曼编码,对于带权路径的二叉树做了详尽地讲述,让你初步理解数据压缩的原理,并明白其是如何做到无损编码和无错解码的。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值