6.5 Huffman

  Huffman 树又称最优树,可以用来构造最优编码,用于信息传输、数据压缩等方面,是一类有着广泛应用的二叉树。


6.5.1 二叉编码树

  在计算机系统中,符号数据在处理之前首先需要对符号进行二进制编码。例如,在计算机中使用的英文字符的 ASCII 编码就是 8 位二进制编码,由于 ASCII 码使用固定长度的二进制位表示字符,因此 ASCII 码是一种定长编码。为了缩短数据编码长度,可以采用不定长编码。其基本思想是:给使用频度较高的字符编较短的编码,这是数据压缩技术的最基本思想。如何给数据中的字符编以不定长编码,而使数据编码的平均长度最短呢?

  首先分析第一个问题:如何对字符集进行不定长编码。在一个编码系统中,任何一个编码都不是其他编码的前缀,则称该编码系统的编码是前缀码。例如: 01, 10, 110, 111, 101 就不是前缀编码,因为 10 是 101 的前缀,如果去掉 10 或101 就是前缀编码。当在一个编码系统中采用定长编码时,可以不需要分隔符;如果采用不是前缀编码,因为 10 是 101 的前缀,如果去掉 10 或101 就是前缀编码。当在一个编码系统中采用定长编码时,可以不需要分隔符;如果采用不定长编码时,必须使用前缀编码或分隔符,否则在解码时会产生歧义。所谓解码就是由二进制位串还原字符数据的过程。而使用分隔符会加大编码长度,因此一般采用前缀编码。例 6-1 说明了这个问题。

6-1 假设字符集为{A, B, C, D},原文为 ABACCDA。

一种等长编码方案为 A:00 B:01 C:10 D:11,此时编解码不会产生歧义,过程如下。
编码: ABACCDA → 00010010101100
解码: 00010010101100 → ABACCDA
一种不等长编码方案为: A:0 B:00 C:1 D:01,由于此编码不是前缀码,此时在编解码的过程中会产生歧义。对于同一编码可以有不同的解码,过程如下。
编码: ABACCDA → 000011010
解码: 000011010 → AAAACCDA
000011010 → BBCCDA 错误!出现歧义。

  为产生没有歧义的前缀编码,可以使用二叉编码树来实现。使用二叉树对字符集中的字符进行编码的方法是,将字符集中的所有字符作为二叉树的叶子结点;在二叉树中,每一个“父亲—左孩子”关系对应一位二进制位 0,每一个“父亲—右孩子”关系对应一位二进制位 1 ;于是从根结点通往每个叶子结点的路径,就对应于相应字符的二进制编码。每个字符编码的长度 L 等于对应路径的长度,也等于该叶子结点的层次数。例如对于例 6-1 中的每个字符可以按照图 6-15 所示的二叉编码树
进行编码。按照图 6-15 中的二叉编码树对 A、 B、 C、 D 四个字符进行编码,则 A 的编码是 0, B 的编码是 100, C 的编码是 11 , D的编码是 101。这个编码显然是一个前缀编码。

  由于在二叉树中任何一个叶子结点都不会出现在根到其他叶子结点的路径上,那么按照上述二叉编码树的编码方法,任何一个叶子结点表示的编码都不会是任何其他叶子表示编码的前缀,因此由二叉编码树得到的编码都是前缀码。反过来如果要进行解码,也可以由二叉编码树便捷的完成。解码的过程是从头开始扫描二进制编码位串,并从二叉编码树的根结点开始,根据比特位不断进入下一层结点,当碰到0 时向左深入,为 1 时向右深入;到达叶子结点后输出其对应的字符,然后重新回到根结点,并继续扫描二进制位串直到完毕。还是如图 6-15 所示,此时将 ABACCDA 进行编码得到: 0100011111010。解码过程是从左到右扫描二进制位串。在读出最前端的 0 后,相应的从根结点到达结点,于是输出 A,重新回到根结点;依次扫描后续二进制位 100,到达叶子结点 B,于是输出 B,重新回到根结点;读出下一个二进制位 0,输出 A;读出 11 ,输出 C;读出 11 ,输出 C;读出 101,输出 D;最后读出 0,输出 A;此时二进制位串扫描完毕,相应的解码工作也完成,最后得到字符数据 ABACCDA。

6.5.2 Huffman 树及 Huffman 编码

  在上一小节中介绍了如何对字符集进行不定长编码的方法,但是同时我们看到对于同一个字符集进行编码的二叉编码树可以有很多,只要叶子结点个数与字符个数对应即可。例如
对例 6-1 中字符即进行编码的二叉树就可以有,但不限于图 6-16 所示的二叉树。在这些不同的编码中哪个才是使得编码长度最小的呢?例如在例 6-1 中,选择图 6-15 中的编码方案比选择图 6-16 中的两种编码方案好。由于
字符 A、 B、 C、 D 分别出现了 3 次、 1 次、2 次、 1 次。使用图 6-15 的编码方案,编码的长度为 3×1+1×3+2×2+1×3=13;使用图 6-16( a)的编码方案,编码的长度为 3×3+1×2+2×3+1×1
=18;使用图 6-16( b)的编码方案,编码的长度为 3×3+1×2+2×1+1×3=16。

字符集中各种字符出现的概率是不同的,字符的出现概率决定了编码方案的选择。

  当引入以上概念以后,求最佳编码方案实际上就抽象为求在叶子结点个数与权确定时带权路径长度最小的二叉树。那么什么样的树带权路径长度最小呢?
对于给定n个权值w1, w2, … wn( n≥2),求一棵具有n个叶子结点的二叉树,使其带权路径长度∑ WiLi最小。由于Huffman给出了构造具有这种树的方法,因此这种树称为Huffman树。

Huffman 树: 它是由 n 个带权叶子结点构成的所有二叉树中带权路径长度最小的二叉树, Huffman 树又称最优二叉树。

构造 Huffman 树的算法步骤如下:
① 根据给定的 n 个权值,构造 n 棵只有一个根结点的二叉树, n 个权值分别是这些二叉树根结点的权, F 是由这 n 棵二叉树构成的集合;
② 在 F 中选取两棵根结点树值最小的树作为左、右子树,构造一颗新的二叉树,置新二叉树根的权值=左子树根结点权值+右子树根结点权值;
③ 从 F 中删除这两颗树,并将新树加F:

④ 重复②、③,直到 F 中只含一棵树为止。

  

  直观地看,先选择权值小的,所以权值小的结点被放置在树的较深层,而权值较大的离根较近,这样自然在 Huffman 树中权越大的叶子离根越近,这样一来,在计算树的带权路径长度时,自然会具有最小的带权路径长度,这种生成算法就是一种典型的贪心算法。用上述算法可以验证在例 6-1 中,图 6-15 所示的二叉树就是 Huffman 树,即例 6-1 中原文 ABACCDA 的最短编码长度为 13。

6-2 假设有一组权值{7, 4, 2, 9, 15, 5},试构造以这些权值为叶子的 Huffman 树。构造 Huffman 树的过程如图 6-17 所示

  使用二叉编码树进行编码,以字符出现的概率作为相应叶子的权值,当这棵二叉编码树是 Huffman 树时,所得到的编码称之为 Huffman 编码。例 6-1 中 A、 B、 C、 D 四个字符的Huffman 编码分别是 0、 100、 11、 101。

  下面讨论构造 Huffman 树的具体实现。
  Huffman 树的实现可以使用顺序的存储结构也可以使用链式的存储结构。前面给出了二叉树的链式存储实现,这里我们也给出 Huffman 树的链式存储结构的实现。
Huffman 树也是一棵二叉树,其结点可以继承二叉树的结点来实现,但是需要两个新的属性,即权值和编码。代码 6-2 定义了 Huffman 树的节点结构。


代码 6-2 Huffman树结点定义

package test;

public class HuffmanTreeNode extends BinTreeNode {
private int weight; // 权值
private String coding = ""; // 编码
// 构造方法 public HuffmanTreeNode(int weight) {
this(weight, null);
} public HuffmanTreeNode(int weight, Object e) {
super(e);
this.weight = weight;
} // 改写父类方法
public HuffmanTreeNode getParent() {
return (HuffmanTreeNode) super.getParent();
} public HuffmanTreeNode getLChild() {
return (HuffmanTreeNode) super.getLChild();
} public HuffmanTreeNode getRChild() {
return (HuffmanTreeNode) super.getRChild();
} // get&set 方法
public int getWeight() {
return weight;
} public String getCoding() {
return coding;
} public void setCoding(String coding) {
this.coding = coding;
}
}

构造 Huffman 树的过程可以通过算法 6-7 实现。

算法 6-7 buildHuffmanTree
输入: 结点数组 nodes
输出: Huffman 树的根结点
代码:

package test;

public class Test {

    // 通过结点数组生成 Huffman 树
private static HuffmanTreeNode buildHuffmanTree(HuffmanTreeNode[] nodes) {
int n = nodes.length;
if (n < 2)
return nodes[0];
List l = new ListArray(); // 根结点线性表,按 weight 从大到小有序
for (int i = 0; i < n; i++) // 将结点逐一插入线性表
insertToList(l, nodes[i]);
for (int i = 1; i < n; i++) { // 选择 weight 最小的两棵树合并,循环 n-1 次
HuffmanTreeNode min1 = (HuffmanTreeNode) l.remove(l.getSize() - 1);
HuffmanTreeNode min2 = (HuffmanTreeNode) l.remove(l.getSize() - 1);
HuffmanTreeNode newRoot = new HuffmanTreeNode(min1.getWeight() + min2.getWeight());
newRoot.setLChild(min1);
newRoot.setRChild(min2); // 合并
insertToList(l, newRoot);// 新树插入线性表
}
return (HuffmanTreeNode) l.get(0);// 返回 Huffman 树的根
} // 将结点按照 weight 从大到小的顺序插入线性表
private static void insertToList(List l, HuffmanTreeNode node) {
for (int j = 0; j < l.getSize(); j++)
if (node.getWeight() > ((HuffmanTreeNode) l.get(j)).getWeight()) {
l.insert(j, node);
return;
}
l.insert(l.getSize(), node);
} }

  算法 6-7 说明:算法使用一个线性表l保存在生成Huffman树过程中森林F的所有树的根结点,并保持在线性表中这些根结点的权值从大到小有序。不难知道当线性表采用数组实现时方法insertToList的运行时间为Ο(n)。因此初始化将n个叶子结点插入线性表的时间为Ο(n2)。在有线性表l之后,取得最小权值的 2 个根结点,只需要Ο(1)的时间,合并 2 棵树需要Ο(1)时间,将新树插入线性表l需要Ο(n)时间,循环执行n-1 次,因此构造Huffman树的时间为Ο(n2)。综上所述,算法buildHuffmanTree的时间复杂度T(n)= Ο(n2)。
Huffman 编码可以在 Huffman 树中递归生成,算法 6-8 实现了这个操作。

算法 6-8 generateHuffmanCode
输入: Huffman 树根结点
输出: 生成 Huffman 编码

package test;

public class Test {
// 递归生成 Huffman 编码
private static void generateHuffmanCode(HuffmanTreeNode root) {
if (root == null)
return;
if (root.hasParent()) {
if (root.isLChild())
root.setCoding(root.getParent().getCoding() + "0"); // 向左为 0
else
root.setCoding(root.getParent().getCoding() + "1"); // 向右为 1
}
generateHuffmanCode(root.getLChild());
generateHuffmanCode(root.getRChild());
}
}

数据结构与算法(周鹏-未出版)-第六章 树-6.5 Huffman 树的更多相关文章

  1. 链表的实现 -- 数据结构与算法的javascript描述 第六章

    链表 链表是由一组节点组成的集合.每个节点都使用一个对象的引用指向它的后继.指向另一个节点的引用叫做链 结构示意图 : 链表头需要我们标识 head { element:head,next:obj1 ...

  2. 【知识强化】第六章 查找 6.3 B树和B+树

    本节课我们来学习本章的第一个难点,就是B树.那么B树它其实是一种数据结构,我们设计出这种数据结构就是为了提高我们的查找效率的,提高我们在磁盘上的查找效率.那么什么是B树呢?了解B树之前,我们先来回忆一 ...

  3. 检索算法 -- 数据结构与算法的javascript描述 第13章

    检索算法-如何在列表中查找特定的值. 顺序查找 从列表的第一个元素开始对列表元素逐个进行判断,直到找到了想要的结果,它属于暴力查找技巧的一种,在执行查找时可能会访问到数据结构里的所有元素. 代码: / ...

  4. 排序算法 -- 数据结构与算法的javascript描述 第12章

    排序是常见的功能,给定一组数据,对其进行排序. 在此之前,我们需要准备个基础工作--自动生成数组,并可以对该组数据做任何处理. /** * 测试类 ,数组 * @param numElements * ...

  5. 字典 -- 数据结构与算法的javascript描述 第七章

    字典 字典是一种以键-值对形式存储数据的数据结构 最基本功能规划 add 添加数据到字典 remove 从字典中移除数据 get 从字典中取出数据 count 统计字典数据量 find 查找数据在字典 ...

  6. 队列的实现 -- 数据结构与算法的javascript描述 第五章

    队列也是列表的一种,有不同于列表的规则. 先进先出 入队方法 出队方法 可以找到队首 可以找到队尾 可以查看队列有多长 可以查看队列是否为空 这是一个基本的需求,围绕他来实现,当然我们可以自己扩展列表 ...

  7. 栈的实现 -- 数据结构与算法的javascript描述 第四章

    栈 :last-in-first-out 栈有自己特殊的规则,只能 后进入的元素 ,最先被推出来,我们只需要模拟这个规则,实现这个规则就好. peek是返回栈顶元素(最后一个进入的). /** * 栈 ...

  8. 学习JavaScript数据结构与算法 (二)

    学习JavaScript数据结构与算法 的笔记 包含第四章队列, 第五章链表 本人所有文章首发在博客园: http://www.cnblogs.com/zhangrunhao/ 04队列 实现基本队列 ...

  9. python数据结构与算法

    最近忙着准备各种笔试的东西,主要看什么数据结构啊,算法啦,balahbalah啊,以前一直就没看过这些,就挑了本简单的<啊哈算法>入门,不过里面的数据结构和算法都是用C语言写的,而自己对p ...

随机推荐

  1. redis简单使用

    主要参考资料:http://wiki.jikexueyuan.com/project/redis-guide/data-type.html一.redis 安装1.在官网下载安装包2.解压安装包 tar ...

  2. POJ1468 Sorting Slides

    Sorting Slides Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 4442   Accepted: 1757 De ...

  3. java中逗号分隔的字符串和List相互转换

    1.将逗号分隔的字符串转换为List String str = "a,b,c"; List<String> result = Arrays.asList(str.spl ...

  4. URL中文编码

    /// <summary>        /// GB2312编码        /// </summary>        /// <param name=" ...

  5. cxgrid强大用法

    cxgrid强大用法 (2012-07-25 14:09:42) 转载▼ 标签: delphi cxgrid 用法 强大 杂谈 分类: Delphi cxGrid功能强大,适合做企业级的复杂查询.非常 ...

  6. ASP.NET微信公众号用于给指定OpenId用户发送红包

    ASP.NET微信公众号用于给指定OpenId用户发送红包 微信公众号要实现对指定用户发送红包,必须指定一个存放兵发放金额的商户号,在微信商户平台里面申请商户号并获取相关参数例如发送红包所要用到的安全 ...

  7. WPF MeasureOverride和 ArrangeOverride做个 页面导航

    public class NavigationPanel:Panel { protected override Size MeasureOverride(Size availableSize) { S ...

  8. .NET MVC 学习笔记(四)— 基于Bootstarp自定义弹出框

    .NET MVC 学习笔记(四)—— 基于Bootstarp自定义弹出框 转载自:https://www.cnblogs.com/nele/p/5327380.html (function ($) { ...

  9. Linux系统文件权限管理(6)

    Linux操作系统是多任务(Multi-tasks)多用户(Multi-users)分时操作系统,linux操作系统的用户就是让我们登录到linux的权限,每当我们使用用户名登录操作系统时,linux ...

  10. 深度学习:浅谈RNN、LSTM+Kreas实现与应用

    主要针对RNN与LSTM的结构及其原理进行详细的介绍,了解什么是RNN,RNN的1对N.N对1的结构,什么是LSTM,以及LSTM中的三门(input.ouput.forget),后续将利用深度学习框 ...