数据结构(十四) -- Huffman树

本文将通过 Huffman 编码树的构造问题,介绍优先队列结构的具体应用。

二进制编码

通讯系统可以帮助人们将一段信息从发送端传送给接收端。最常见的信息形式是字符串,即由来自某一有限字符集Σ 的若干个字符组成的一个序列M = (x1, …, xn),xi∈Σ,1≤i≤n。在将M 加载至信道上并发送之前,首先需要对M 进行编码(Encoding)。通常采用的都是二进制编码,以英文字母集Σ = {A, B, C, …, Z}为例,若需要传送字符串“MAIN”,一种可行的编码方式就是:

e('N') = "00"
e('A') = "010"
e('I') = "011"
e('M') = "1"

数据结构(十四) -- Huffman树_第1张图片

若令每个字符分别对应于一匹叶子,则从根节点通往每匹叶子的路径,就对应于相应字符的二进制编码,这样的一棵树也称作二叉编码树。

二进制解码

反过来,根据这棵编码树也可以便捷地完成解码工作。

  • 从头开始扫描接收到的二进制信息流;

  • 从根节点开始,根据各比特位不断进入下一层节点;

  • 到达叶子节点后,输出其对应的字符;

  • 然后重新回到根节点,并继续扫描二进制流。

实际上,这一过程可以在接收过程中实时进行,而不必等到所有的比特位都到达之后才开始解码。

最优编码树

在实际的通讯系统中,信道的使用效率是个很重要的问题,这在很大程度上取决于编码算法本身的效率。很自然地,我们当然希望能够用尽可能少的比特位来表示字符串。那么,如何做到这一点呢?在什么情况下能够做到这一点呢?

先介绍一项编码效率的重要指标——平均编码长度

在二叉编码树中每个字符 c 的编码长度为对应的叶子的深度 depth(c)。

Σ 中各字符的编码长度总和除以字符集 Σ 就是单个字符的平均编码长度。

对于任一字符集Σ,若在所有的编码方式中,某一编码方式 e() 使得平均编码长度最短,则称 e() 为 Σ 的一种最优编码,与之对应的编码树称作 Σ 的一棵最优编码树。

我们注意到,对于同一字符集Σ,所有深度不超过|Σ|的编码树只有有限棵,因此其中的总编码长度最小者必然存在(尽管不见得唯一)。

如何找到最优编码树

首先需要更加深入地了解最优编码树的性质,在最优二叉编码树中:

  • (1) 每个内部节点的度数均为 2

  • (2) 各叶子之间的深度差不超过 1

推论:基于由 2 | Σ | -1 个节点构成的完全二叉树 T,将 Σ 中的字符任意分配给 T 的 | Σ | 匹叶子,即可得到 Σ 的一棵最优编码树。

这一推论也直接给出了一个构造最优编码树的算法。

带权编码

上面所介绍的最优编码树,在实际应用中的利用价值并不大。不难看出,只有当 Σ 中各个字符在信息串中出现的概率相等时,其最优性才有意义,遗憾的是,这一条件很难满足。

在实际应用中,Σ 中各字符的出现频率不仅很少相等或相近,而且往往相差悬殊。以英文信息串为例,'e'、't'等字符的出现频率通常都是'z'、'j'等字符的数百倍。在这种情况下,就应该从另一角度衡量每个字符的编码长度。

若假设字符 c 出现的概率为 p(c) ≥ 0, 其在二叉编码树中对应的叶子的深度记作depth(c),则:

  • 每个字符 c∈Σ 的带权编码长度为|e(c)| = depth(c) × p(c)
  • 对于任一字符集 Σ 的任一编码方式 e(),Σ 中各字符的平均带权编码长度总和|e(c)|称作 e()(或其对应的二叉编码树)的平均带权编码长度。

例如:信息串" M A M A N I "

各字符出现的概率分别为p('M') = 2/6、p('A') = 2/6、p('I') = 1/6 和p('N') = 1/6

则各字符的带权编码长度分别为:
|e('M')| = 3×(2/6) = 1
|e('A')| = 3×(2/6) = 1
|e('I')| = 2×(1/6) = 1/3
|e('N')| = 1×(1/6) = 1/6

相应地,这一编码方式对应的平均带权编码长度就是:
|e('M')| + |e('A')| + |e('I')| + |e('N')| = 2.5

如何找到最优带权编码树(不唯一)

首先

** 完全二叉编码树 ≠ 平均带权编码最短 **

最优带权编码树有以下性质:

  • 在最优带权编码树中,内部节点的度数均为 2

  • 对于字符出现概率为 p()的任一字符集 Σ ,若字符 x 和 y 在所有字符中的出现概率最低,则必然存在某棵最优带权编码树,使得 x 和 y 在其中同处于最底层,而且互为兄弟。

对于字符出现概率满足一定分布的任一字符集 Σ ,我们都可以按照如下算法来构造其对应的 Huffman 编码树:

首先,对应于 Σ 中的每一字符,分别建立一棵由单个节点组成树,其权重就是该字符出现的频率,这|Σ |棵树组成一个森林 F。

接下来,从 F 中选出权重最小的两棵树,创建一个新节点,并分别以这两棵树作为其左、右子树,从而将它们合成为一棵更高的树,其权重等于其左、右子树权重之和。

这一选取、合并的过程将反复进行,直到最后 F 中只剩下一棵树⎯⎯它就是我们所需要的 Huffman 编码树。

举个例子:

字符 出现频率
A 623
B 99
C 239
D 290
E 906
F 224
数据结构(十四) -- Huffman树_第2张图片
基于优先队列的 Huffman 树构造算法

具体方法是:

  • 1,始终将森林中的所有树(根)组织为一个优先队列,比如基于二叉堆实现的优先队列。

  • 2,这样,只要连续地调用 delMin()方法两次,就可以找出当前权重最小的两棵树。

  • 3,在将这两棵树合并为一棵新树之后,可以调用 insert()方法将其重新插入优先队列。

  • 4,这一过程将反复进行,每迭代一次,森林的规模就会减小 1。

因此,经过 n-1 次迭代,森林中将只包含一棵树,即 Huffman 编码树。

你可能感兴趣的:(数据结构(十四) -- Huffman树)