前缀代码
正如您所指出的,前缀代码是给定代码不是任何其他给定代码的前缀的代码。
这是一个非常笼统的定义。霍夫曼编码是前缀码的一种受限形式。
霍夫曼编码的一个常见用法是最小化(优化)编码“消息”所需的总位数。
“消息”通常是一个符号序列,它通过将每个符号出现映射到
一个特定的前缀代码并在其位置写出前缀代码。可以使用任何一组前缀代码
去做这个。但是,霍夫曼编码会根据比特数产生最短的消息。
例如,ASCII 字符集可以被视为符号到一组 8 位前缀代码的映射。
这甚至可以被认为是霍夫曼编码,前提是编码的消息包含完全
每个可能的符号的数量相同。
当要编码的消息包含不相等的符号频率时,有趣的事情就开始了。在这
第一点可以通过使用不同长度的前缀码来减少消息的总比特长度。使用短
更频繁的符号的前缀代码和不太频繁的符号的更长的前缀代码。
在您的示例中,有 8 个符号需要编码。映射到前缀代码“11”和“10”的符号将是最多的
消息中的常见符号。同样,映射到“0111”、“0110”、“1010”和“0100”的符号频率最低。
频率越高,前缀码越短。
创建霍夫曼编码的“诀窍”是构建前缀代码集,以便在映射后
消息中的每个符号与其关联的前缀代码 消息包含尽可能少的位。
我发现将前缀代码视为二叉树很有用,其中每个叶节点都映射到一个符号。
例如,与您问题中给出的前缀代码对应的二叉树 (01, 11, 000, 001,
0100, 0101, 0110, 0111) 将是:
+-- (11)
+--+
| +-- (10)
|
| +-- (0111)
--+ +--+
| | +-- (0110)
| +--+
| | | +-- (0101)
| | +--+
+--+ +-- (0100)
|
| +-- (001)
+--+
+-- (000)
要获取括号中的值,您只需在顶部边缘时分配“1”或在底部时分配“0”
边缘被跟随。
如何建造这样一棵树?
从表示二叉树和列表的数据结构开始。
二叉树将包含两种类型的节点。 1)一个叶子节点表示
符号及其频率和 2) 表示累积频率的内部节点
它下面的所有节点(它还需要两个指针,一个指向左分支,一个指向右分支)。
列表包含来自二叉树的有序节点集。列表中的节点是有序的
基于它们指向的节点的频率值。最低频率节点出现在列表的前面
并在列表末尾增加。指向树节点的指针链表可能很有用
实现 - 但任何有序列表结构都可以。
下面的算法使用两个列表:“参考”列表和“工作”列表。由于节点是
从“参考”列表中处理的新节点被创建并插入到“工作”列表中,使得
“工作”列表仍然按节点频率排序。
使用这些数据结构和以下算法来创建霍夫曼编码。
0. Initialize the "reference" list by creating a leaf node for each symbol
then add it into this list such that nodes with the lowest frequency
occur at the front of the list and those with the highest frequency
occur at the back (basically a priority queue).
1. Initialize the "working" list to empty.
2. Repeat until "reference" list contains 1 node
2.1 Set MaxFrequency to the sum of the first 2 node frequencies
2.1 Repeat until "reference" list is empty
If ("reference" list contains 1 node) OR
(sum of the next two nodes frequency > MaxFrequency)
Move remaining nodes to the "working" list
Set "reference" list to empty
Else
Create a new internal node
Connect the first "reference" node to the left child
Connect the second "reference" node to the right child
Set the new node frequency to the sum of the frequencies of the children
Insert the new node into the "working" list
Remove the first and second nodes from the "reference" list
2.2 Copy the "working" list to the "reference" list
2.3 Set the "working" list to empty
在此过程结束时,单个“引用”列表项将成为 Huffman 树的根。你可以列举
通过对树进行深度优先遍历来添加前缀代码。为每个左分支写一个“0”
为每个右分支取一个“1”。当遇到叶子时,代码就完成了。符号在
叶子是由刚刚生成的霍夫曼代码编码的。
什么是最佳编码
可以执行的一个有趣的计算是计算前缀编码的“位权重”。位重量
是表示前缀代码集所需的总位数。
看看上面的原始树。这棵树的重量是
(2 位 * 2) + (4 位 * 5) + (3 位 * 2) = 30 位。您使用 30 位来表示 8 个前缀值。什么
是最小的数
您可以使用多少位?想一想,当一棵树变得不平衡时,通往一些叶子的路径的长度变得
更长 - 这增加了重量。例如,4 值前缀树的最坏情况是:
+-- (1 bit)
--+
| +-- (2 bits)
+--+
| +-- (3 bits)
+--+
+-- (3 bits)
总权重为 (1 bit * 1) + (2 bits * 1) + (3 bits * 2) = 9 bits
平衡树:
+-- (2 bits)
+--+
| +-- (2 bits)
--+
| +-- (2 bits)
+--+
+-- (2 bits)
总权重为 (2 bits * 4) = 8 bits。请注意,对于平衡树,所有前缀代码都结束了
具有相同的位数。
树
位权重只是所有叶子的路径长度的总和。你最小化位重量
通过最小化总路径长度 - 这是通过平衡树来完成的。
如您所见,最小化任何给定的前缀树没有多大价值,您最终会得到一个固定的长度
符号编码。当您考虑生成的编码消息的位权重时,该值就出现了。最小化
这导致霍夫曼编码。
有多少种不同的编码?
可以通过遍历二叉树并为跟随的每个较低分支发出“0”来生成前缀代码
并且在遇到叶子之前,每个上部分支都有一个“1”。如:
+--+ (1)
|
--+
| +-- (01)
+--+
+-- (00)
或者,我们可以“翻转”该规则并为每个较低的分支分配一个“1”和一个“0”
对于上层分支:
+-- (0)
|
--+
| +-- (10)
+--+
+-- (11)
这些会生成两组不同的前缀代码。可以通过以下方式生成附加集
遍历所有可能的 1/0 分配给分支,然后遍历树。
这会给你 2^n 套。但是如果你这样做,你会发现可能会生成相同的前缀代码,但是
以不同的顺序。例如,前面的树会产生以下集合:{(0, 10, 11), (0, 11, 01),
(1, 01, 00), (1, 00, 01)}。然后将树翻转到:
+-- (??)
+--+
| +-- (??)
--+
|
+-- (?)
你会得到:{(11, 10, 0), (10, 11, 0), (01, 00, 1), (00, 01, 1)}。将它们放在一起 2^3 = 8 组。但是,如果您想要不考虑顺序的唯一集合,则只有 2 个集合:{(0, 10, 11), (1, 00, 01)}。
对平衡树进行相同的练习,并且只有一组。这些所有
让我相信唯一编码的数量与树的平衡结构有关
用于生成前缀代码。不幸的是,我没有一个精确的公式或计算出来。凭直觉,我猜这个数字是 2^(不同代码长度的数量 - 1)。对于平衡树: 2^(1 - 1) = 1;对于具有两个不同代码长度的树(如上例所示):2^(2 - 1) = 2;并以您为例:2^(3 - 1) = 4。