霍夫曼编码入门

基本思想

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

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

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

如何创建霍夫曼编码?

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

我们将会为每个字母创建一个树节点,并且从树的底部开始构建整个树。
图15.2所示以图形方式来展示这些节点,这里的每一个节点都会显示出它所代表的字母以及紧跟的频率。我们根据它们的出现频率进行升序排列。
在这里插入图片描述
霍夫曼开发的算法将当前最小的两个树合并成一个树来工作。在我们的例子里,第一步选择字母c和m,然后把它们合并成一个有根节点的树,根节点的总频率为3(两个单独频率的总和)。图15.3所示即这个结果。
在这里插入图片描述
接下来,因为l和p现在是频率最小的两个树,所以我们选择字母l和p,然后合并出一个新的树,结果如图15.4所示。
在这里插入图片描述

现在两个最小的频率是3和4,因此我们把它们合并在一起,结果如图15.5所示。
在这里插入图片描述

我们继续这个过程,并且把频率为4和6的两个树合并在一起,结果如图15.6所示。
在这里插入图片描述

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

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

要注意的是,在我们合并树的时候,如果有频率相等的情况的话,是可以形成多个树的。为了说明这一点,我们改变最后两步,让频率为7的树和包含字母a的频率为10的树先合并,而不像之前那样去合并包含字母n的树。这就会导致最终的树会像图15.8所示的这样。你甚至还可以在合并树的时候随意决定某一个树是左子树还是右子树,这些变化都会导致编码的略微不同。
在这里插入图片描述
表15.3所示为字母、它们的初始频率、图15.8里的树的比特编码,以及使用比特编码来存储这个字母所需要的总比特数。可以看到,表示这个字符串所需要的总比特数也是76个比特。你应该可以猜到这一点,因为我们用一个频率总和为10的子树替换掉了另一个频率总和为10的树。而且,在任何一种情况下,都需要相同数量的比特来表示这个子树。
在这里插入图片描述

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

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

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就经常出现在英语单词里。如果我们对这些多字母的序列也使用短比特编码的话,我们就能够比只对单个字母使用比特编码来压缩实现更高的压缩比。

总结

  • 霍夫曼编码为压缩算法
  • 变长编码的关键是要解决歧义,这种技术叫做无前缀码
  • 霍夫曼编码可以用二叉树结构实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值