本文将通过 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"
若令每个字符分别对应于一匹叶子,则从根节点通往每匹叶子的路径,就对应于相应字符的二进制编码,这样的一棵树也称作二叉编码树。
二进制解码
反过来,根据这棵编码树也可以便捷地完成解码工作。
从头开始扫描接收到的二进制信息流;
从根节点开始,根据各比特位不断进入下一层节点;
到达叶子节点后,输出其对应的字符;
然后重新回到根节点,并继续扫描二进制流。
实际上,这一过程可以在接收过程中实时进行,而不必等到所有的比特位都到达之后才开始解码。
最优编码树
在实际的通讯系统中,信道的使用效率是个很重要的问题,这在很大程度上取决于编码算法本身的效率。很自然地,我们当然希望能够用尽可能少的比特位来表示字符串。那么,如何做到这一点呢?在什么情况下能够做到这一点呢?
先介绍一项编码效率的重要指标——平均编码长度
在二叉编码树中每个字符 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 树构造算法
具体方法是:
1,始终将森林中的所有树(根)组织为一个优先队列,比如基于二叉堆实现的优先队列。
2,这样,只要连续地调用 delMin()方法两次,就可以找出当前权重最小的两棵树。
3,在将这两棵树合并为一棵新树之后,可以调用 insert()方法将其重新插入优先队列。
4,这一过程将反复进行,每迭代一次,森林的规模就会减小 1。
因此,经过 n-1 次迭代,森林中将只包含一棵树,即 Huffman 编码树。