霍夫曼编码算法

2025年3月17日 | 阅读13分钟

可以使用霍夫曼编码技术来压缩数据,使其变小而不丢失任何信息。它是大卫·霍夫曼在最初创造它之后。包含频繁重复字符的数据通常使用霍夫曼编码进行压缩。

霍夫曼编码是一种著名的贪心算法。分配给字符的代码大小取决于字符的频率,这就是它被称为贪心算法的原因。为频率最高的字符分配短长度的变长代码,反之亦然。它使用变长编码,这意味着它为提供的比特流中的每个字符分配不同的变长代码。

前缀规则

本质上,这条规则规定,分配给字符的代码不能是另一个代码的前缀。如果违反了这条规则,在解码创建的霍夫曼树时可能会出现各种歧义。

让我们通过一个例子来更好地理解这条规则:为每个字符分配一个代码,例如

假设生成的比特流是001,代码可以如下解码

什么是霍夫曼编码过程?

霍夫曼码是在主要两个步骤中为数据流中的每个不同字符获得的

  • 首先,使用数据流中仅有的不同字符创建一个霍夫曼树。
  • 其次,我们必须遍历创建的霍夫曼树,为字符分配代码,然后使用这些代码来解码给定的文本。

霍夫曼编码的步骤

用于使用提供的字符构建霍夫曼树的步骤

如果在此情况下使用霍夫曼编码进行数据压缩,则必须确定以下信息才能进行解码

  • 每个字符的霍夫曼码
  • 霍夫曼编码消息的长度(以比特为单位),平均代码长度
  • 利用下面介绍的公式,可以发现后两者。

如何从输入字符构建霍夫曼树?

首先必须确定数据流中每个字符的频率。

Character频率
a4
b7
c3
d2
e4
  1. 按频率升序对字符进行排序。这些保存在 Q/min-heap 优先级队列中。
  2. 为数据流中的每个不同字符及其频率创建一个叶节点。
  3. 从节点中移除两个频率最低的节点,并通过将这两个频率相加来创建一个新的根节点。
    • 在移除最小频率的节点时,将第一个移除的节点作为其左子节点,第二个移除的节点作为其右子节点。
    • 将此节点添加到最小堆中。
    • 因为根的左侧应该总是包含最小频率。
  4. 重复步骤 3 和 4,直到堆中只剩一个节点,或者所有字符都由树中的节点表示。当只剩下根节点时,树就完成了。

霍夫曼编码示例

让我们用一个例子来解释算法

Huffman Coding Algorithm
Huffman Coding Algorithm

霍夫曼编码算法

步骤 1:构建一个最小堆,其中每个节点代表一个具有单个节点的树的根,并保存 5(来自给定数据流的不同字符数)。

Huffman Coding Algorithm

步骤 2:在步骤二中,从最小堆中获取两个最小频率的节点。添加一个第三个内部节点,频率为 2 + 3 = 5,该节点是通过连接两个提取的节点创建的。

Huffman Coding Algorithm
  • 现在,最小堆中有 4 个节点,其中 3 个是具有单个元素的树的根,1 个是具有两个元素的树的根。

步骤 3:在步骤三中,以类似的方式从堆中获取两个最小频率的节点。此外,添加一个由连接这两个提取节点形成的新内部节点;其在树中的频率应为 4 + 4 = 8。

Huffman Coding Algorithm
  • 现在最小堆中有三个节点,一个节点是单个元素的树的根,两个堆节点是具有多个元素的树的根。

步骤 4:在步骤四中,获取两个最小频率的节点。此外,添加一个由连接这两个提取节点形成的新内部节点;其在树中的频率应为 5 + 7 = 12。

  • 在创建霍夫曼树时,我们必须确保最小值总是在左侧,第二个值总是在右侧。目前,下图显示了形成的树
Huffman Coding Algorithm

步骤 5:在步骤 5 中,获取接下来的两个最小频率的节点。此外,添加一个由连接这两个提取节点形成的新内部节点;其在树中的频率应为 12 + 8 = 20。

继续,直到所有不同字符都已添加到树中。上图显示了为指定角色创建的霍夫曼树。

现在,对于每个非叶节点,为左边缘分配 0,为右边缘分配 1,以创建每个字母的代码。

确定边权重的规则

  • 如果你给左边缘赋予权重 0,那么我们也应该给右边缘赋予权重 1。
  • 如果左边缘赋予权重 1,则右边缘必须赋予权重 0。
  • 上述两种约定中的任何一种都可以使用。
  • 然而,在解码树时也应遵循相同的协议。

遵循权重分配后,修改后的树显示如下

Huffman Coding Algorithm

理解代码

  • 为了从生成的霍夫曼树中解码每个字符的霍夫曼码,我们必须遍历霍夫曼树直到到达叶节点,那里存在该字符。
  • 必须在遍历期间记录节点之间的权重,并将它们分配给位于特定叶节点中的项。
  • 下面的例子将有助于进一步说明我们的意思
  • 要获得上图中每个字符的代码,我们必须遍历整个树(直到覆盖所有叶节点)。
  • 因此,创建的树用于解码每个节点的代码。下表是每个字符的代码列表
Character频率/计数代码
a401
b711
c3101
d2100
e400

下面是 C 语言的实现

输出

f: 0
c: 100
d: 101
a: 1100
b: 1101
e: 111
……………
Process executed in 1.11 seconds
Press any key to continue.

以上代码的 Java 实现

输出

f: 0
c: 100
d: 101
a: 1100
b: 1101
e: 111
……………….
Process executed in 1.11 seconds
Press any key to continue.

说明

通过遍历,霍夫曼树被创建和解码。然后在位于叶节点中的字符上应用遍历期间收集的值。使用霍夫曼码可以这样识别给定数据流中的每个唯一字符。O (nlogn),其中 n 是字符总数,是时间复杂度。如果 n 个节点,则调用 ExtractMin() 2*(n - 1) 次。由于 extractMin() 调用 minHeapify(),其执行时间为 O (logn)。因此,总复杂度为 O (nlogn)。如果输入数组已排序,则存在线性时间算法。我们将在下一篇文章中更详细地介绍这一点。

霍夫曼编码的缺点

在本节中,我们将讨论霍夫曼编码的缺点以及为什么它并不总是最佳选择

  • 如果并非所有字符的概率或频率都是 2 的负幂,则它不被认为是理想的。
  • 虽然可以通过分组符号和扩展字母表来接近理想情况,但阻塞方法需要处理更大的字母表。因此,霍夫曼编码可能并不总是非常有效。
  • 虽然有许多有效的方法来计算每个符号或字符的频率,但为每个符号或字符重建整个树可能非常耗时。当字母表很大且概率分布随每个符号快速变化时,通常是这种情况。

贪心霍夫曼码构造算法

  • 霍夫曼开发了一种贪心技术,为输入数据流中的每个不同字符生成霍夫曼码,一种理想的前缀码。
  • 该方法每次使用最少的节点自下而上地创建霍夫曼树。
  • 由于每个字符的代码长度取决于它在给定数据流中出现的频率,因此此方法被称为贪心方法。如果检索到的代码大小较小,那么它在数据中是一种常见元素。

霍夫曼编码的用途

  • 在这里,我们将讨论霍夫曼编码的一些实际用途
  • 传统的压缩格式,如 PKZIP、GZIP 等,通常采用霍夫曼编码。
  • 霍夫曼编码用于传真和文本数据传输,因为它减小了文件大小并加快了传输速度。
  • 霍夫曼编码(特别是前缀码)被一些多媒体存储格式(如 JPEG、PNG 和 MP3)用于压缩文件。
  • 霍夫曼编码主要用于图像压缩。
  • 当需要传输包含频繁重复字符的字符串时,它可能更有帮助。

结论

  • 总的来说,霍夫曼编码对于压缩包含频繁出现字符的数据很有帮助。
  • 我们可以看到,出现频率最高的字符具有最短的代码,而出现频率最低的字符具有最长的代码。
  • 霍夫曼编码压缩技术用于创建变长编码,它为每个字母或符号使用不同数量的比特。这种方法优于定长编码,因为它占用的内存更少,传输数据速度更快。
  • 阅读本文,以更好地了解贪心算法。

下一主题Kadane's Algorithm