霍夫曼编码入门

霍夫曼编码入门基本思想霍夫曼编码的基本思想是 对于需要压缩的文本里出现的频率更高的字母使用更少的比特 不常出现的字母使用更多的比特 对于在我们的文本里经常出现的字母 比如说 a e 和 s 我们最终可能只会用两比特或者 3 比特来表示它 对于不常出现的字母 比如像 z 或者 q 会用超过 8 比特来表示它 如何创建霍夫曼编码 霍夫曼编码用来创建压缩文件的技术会生成被称为前置码 prefixcode 或者是前缀码 的编码 或更准确地来说 被称为 prefix freecode 或者是无前缀码 这两个术语可以进行互换

基本思想

霍夫曼编码的基本思想是:

对于需要压缩的文本里出现的频率更高的字母使用更少的比特, 不常出现的字母使用更多的比特。 

对于在我们的文本里经常出现的字母,比如说a、e和s,我们最终可能只会用两比特或者3比特来表示它。对于不常出现的字母,比如像z或者q,会用超过8比特来表示它。

如何创建霍夫曼编码?

  • 霍夫曼编码用来创建压缩文件的技术会生成被称为前置码(prefix code,或者是前缀码)的编码;或更准确地来说,被称为(prefix-free code,或者是无前缀码)。
  • 这两个术语可以进行互换使用。
  • 对于被认为是无前缀的代码,没有任何编码可以是另一个编码的前置部分。
  • 编码10、011、010和110可以组成一个无前缀的代码集。
  • 如果我们把这些编码分配给字母a、b、c、d,并且有00这样的一个比特序列,那么我们就可以通过对每个比特进行处理直到找到匹配的字母,从而轻松地解码整个序列。
00 bdaca 

由于没有任何的编码是其他编码的前缀,因此这个过程是对信息进行解码的唯一解,我们可以在比特序列匹配到其中一个字母的时候,立即停止处理后面的比特,并且输出相应的字母。

可视化并且仔细理解这个算法的简单方法是使用一个这些前置码生成的树。

比特0对应于在树里向左移动,而比特1则对应于在树里向右移动。

图15.1所示为这个例子里的前置码所生成的树。这个树能够让你从树的根节点开始,在处理每个比特的时候都向下移动。当到达带有字母的叶节点的时候,就输出这个字母,并且在树的根节点处再次启动整个过程。通过树来表示前置码可以让你更容易来理解它,因此可以看到所有的字母都始终在叶节点。如果一个字母不在叶节点的话,那么这个编码就不是无首码的代码,并且当我们尝试处理它的时候会出现歧义。考虑编码0、11和110,它们并不是对应于字母a、b和c的无前缀码。如果我们尝试对110进行解码,那么结果是字母c还是双字母序列ba?而如果使用无前缀码的话,就不会出现这种有歧义的问题。

在这里插入图片描述

很明显,现在的问题是应该如何来组成这个树,从而有最大的压缩比。就像我们前面说过的,我们希望使用得更频繁的字母有更短的编码,并且使用得更少的字母有更长的编码。这就意味着常用的字母需要靠近树的根节点,而不常用的字母需要在树的根节点的下方非常远的地方。因此,我们的第一步是处理输入的文件,来确定每个字母的频率,并且按照频率对字母进行排序。为了演示这个算法,我们将会使用一个常见的回文“a man a plan a canal panama”作为例子,因为这个回文只用了一小部分字母,从而能够让我们的树维持在比较小的规模。表15.1所示为这个回文的字母的频率。
在这里插入图片描述
回忆一下我们的两个要求:




1 所有的字母必须在叶节点, 2我们希望不太频繁出现的字母被存储在树的底部附近。 

下一步,把两个频率为7和10的树合并在一起。最后,只剩下了两个树,我们把它们合并起来,就得到了最终结果,如图15.7所示。存储字符串所需比特数如表15.2所示。

在这里插入图片描述
在这里插入图片描述
表15.2展示了字母、它们的初始频率、它们的比特编码,以及使用比特编码来存储这个字母所需要的总比特数(频率乘以比特编码的长度)。对于我们的例子字符串“a man a plan a canal panama”来说,总共需要76比特来存储。前面提到过,我们还必须要存储字母和它所对应的编码,从而能够让我们对压缩后的编码进行解码。在这个例子里,由于字符串的长度太短了,而存储每个字母的编码也需要一定的开销,因此可能会导致压缩之后的文件变大




如何用代码实现霍夫曼编码

第一步是读取我们需要压缩的文件,并且计算出每个字母的频率总和。

Python里,我们可以把字母作为键映射到它们的频率上。

C++里,除非你已经有了一个散列表,不然的话,你也可以通过一个数组来储存字母的频率。因为当我们读取文件的每个字节的时候,我们会知道最多有256个可能的值(如果文件是ASCII编码的,那么只有128个可能的值)。因此,我们可以用一个长度为256的数组,并且把每个值都初始化为零,然后每次从文件里读取一个字节的时候,在相应的数组的位置上加1。这个算法的复杂度是Θ
(n),其中n对应于文件的总字节数。

下一步是对频率进行排序。

由于最多需要对256个元素进行排序,因此可以把这一步看作Θ (1)。

下一步,我们要为每个频率创建一个树节点,并且按顺序来存储它们。

为了能够很好地实现前面图里所展示的算法,二叉堆或者优先队列是我们需要使用的数据结构。我们从堆里移除两个最低频率的元素,然后插入这两个被移除的元素合并之后所形成的树。与上一步一样,这里的工作量也是不变的,因为我们总是向堆里插入以及从堆里删除固定数量的元素。然而,即使我们不考虑这些数字常量的算法,排序以及在堆里插入和删除n个元素都需要O(nlgn)的时间,其中n是文件里的字母数量。

我们现在可以通过树来确定每个字母在树里的位置的比特编码了。

我们可以通过一个修改版本的后序遍历来确定每个字母的编码,这个修改版本的后序遍历会添加一个额外的参数,这个参数被用来传递一个和比特序列所对应的列表。每次移动到左路径的时候,我们会在列表的最后添加一个0;每次跟踪右路径的时候,我们会在列表的最后添加一个1。每次递归调用返回的时候,我们会删除列表里的最后一个元素。根据树里的节点数量,这一步的运行时间是线性的。而且,这一步也可以被认为是常量时间的操作,因为树里最多会有512个节点。下一步是把这个字母和它的编码存储在一个新文件里,然后按照它们在原始文件里出现的顺序存储每个字母的编码。可以看出来,对于大型文件来说,运行时间主要是被用在读取和写入字母上,并且这个时间和文件的字节总数是线性相关的。

要解压缩一个文件的话,我们就需要读取这个包含文件里的每一个字母的比特编码的头信息。然后,你就可以构建一个树或者是创建一个散列表,来把每个编码都和它所对应的字母进行匹配。然后当文件开始读取的时候,你会一次读取1比特,你可以像我们在这一节开头说的那样使用这个树来对它进行解码,或者如果使用散列表的话,继续读取编码直到获得的代码是散列表里的键为止。整个运行时间还是和文件的字节总数是线性相关的。

要实际压缩一个文件的话,我们通常会需要进行位操作。大多数编程语言(但并不是全部)会为执行位操作提供运算符。在Python和C++里,< <和>
>运算符可以被用来移位,二进制运算符&和|可以被用来执行位级别的and/or操作。下面与Python解释器的交互式操作展示了一个关于这些运算符的例子:

在这里插入图片描述
语句x << 1会把所有的位都向左移动一个位置,如果最初的值是1的话,那么x现在的值就是2了。语句x << 1后面使用的|运算符会按位进行“或”运算,因此x现在的值是3。再向左对3进行左移,在基数为2的情况下这个数字被表示为11,向左移动一个位置就会得到110。和0进行位的“或”运算并不会改变这个数字,所以x现在的值是6。霍夫曼压缩算法里,这种类型的位操作可以被用来构建编码序列的比特编码。每当达到一定数量(比如8个或者是32个)的比特的时候,你都可以把这个值写到文件里去,然后再启动这个过程。




使用这种贪婪策略的压缩有多好?

答案是它对于前置码的实现来说是最优解,因为我们使用了最短的位数来表示最常用的字母。

但是,如果你实现这个算法之后把它和gzip、bzip2或者zip这样的压缩程序进行比较的话,你可能会发现它们压缩文件的比例会超过你的Huffman压缩程序。这些程序工作得更好的原因是它们会把多个字母组合在一起,并且为这些字母构建编码。比如,双字母序列sh、th以及ch就经常出现在英语单词里。如果我们对这些多字母的序列也使用短比特编码的话,我们就能够比只对单个字母使用比特编码来压缩实现更高的压缩比。

总结

  • 霍夫曼编码为压缩算法
  • 变长编码的关键是要解决歧义,这种技术叫做无前缀码
  • 霍夫曼编码可以用二叉树结构实现
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/222840.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月17日 下午2:53
下一篇 2026年3月17日 下午2:54


相关推荐

  • 在phpstorm中如何对比文件呢?「建议收藏」

    在phpstorm中如何对比文件呢?

    2022年2月9日
    469
  • Leetcode: Shortest Word Distance II

    Leetcode: Shortest Word Distance II

    2021年9月11日
    54
  • Javascript中indexOf的用法和分析

    Javascript中indexOf的用法和分析前言相信说到 indexOf 大家并不陌生 判断字符串是否包涵子字符串时特别常用 正则不熟练同学的利器 这篇文章就最近遇到的一个问题 用实例再说说说 indexOf 方法 本文是小知识点积累 不作为深入讨论的话题 因此这里没有解释 indexOf 的第二个参数 相信大家都知道第二个参数的作用 String 类型的使用温习一下大家熟知的字符串用法 举个 12345letstr orange nbsp

    2025年8月30日
    4
  • 地理加权回归模型步骤_地理加权回归中的拟合度

    地理加权回归模型步骤_地理加权回归中的拟合度目录数据准备加载需要的R包导入空间数据空间自相关分析空间邻域面数据空间邻域点数据空间邻域全局空间自相关局部空间自相关空间回归分析线性回归分析地理加权回归经典的线性回归模型是建立在最小二乘法(OLS模型)基础上对参数进行“平均”或“全局”估计。如果自变量为空间数据,且自变量间存在空间自相关性,传统回归模型(OLS模型)残差项独立的假设将无法满足。地理加权回归(GWR)模型能够反映参数在不同空间的空间非平稳性,使变量间的关系可以随空间位置的变化而变化,其结果更符合客观实际,能反映局部情况。杨晴青,刘倩

    2022年10月7日
    4
  • c++—-随机数算法

    c++—-随机数算法    本文转载:http://blog.csdn.net/luotuo44/article/details/33690179     相对于C++11之前的随机数生成器来说,C++11的随机数生成器是复杂了很多。这是因为相对于之前的只需srand、rand这两函数即可获取随机数来说,C++11提供了太多的选择和东西。 随机数生成算法:   &.

    2022年7月26日
    7
  • VS2019配置SFML

    VS2019配置SFMLVS2019 配置 SFML1 下载安装 SFMLSDK 网址 https www sfml dev org download php 解压并放在文件夹里 记住这个路径 在我的电脑中这个路径是 F C Projects library SFML 2 5 12 VS 新建一个 C 控制台项目我命名为 SfmlTest 并放在常用的项目文件夹中 3 在解决方案中右键项目名称 点击属性 4 4 1 在 C C 中点击常规 在第一行附加包含目录中复制粘贴 SFML 2 5 1 include

    2025年10月8日
    4

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号