【问题标题】:Algorithm to Generate 'n' Binary Prefix Codes生成“n”个二进制前缀码的算法
【发布时间】:2011-11-11 11:16:33
【问题描述】:

Prefix Code 是一组代码,因此没有代码是另一个代码的前缀。比如下面的集合就是前缀码:

10
11
000
001
0100
0101
0110
0111

n = 8 成员。我认为这些通常是用某种类型的霍夫曼树创建的。

我的问题是:你能帮我创建一个函数来生成一个包含“n”个成员的二进制前缀代码吗?

类似这样的:

list<int> GenerateBinaryPrefixCodes(int n);

此外,要求它是“最优的”,即比特的总和最小化。

我更喜欢 C/C++/C#/类似的答案。这不是真正的家庭作业,但我这样标记它是因为听起来这将是一个很好的硬件问题。

谢谢!

【问题讨论】:

  • 关于霍夫曼编码的维基百科文章有这个链接:huffman.sourceforge.net
  • 任何n 或只是两个的幂?在后一种情况下有一个更简单的方法。

标签: algorithm language-agnostic huffman-code


【解决方案1】:

前缀代码

正如您所指出的,前缀代码是给定代码不是任何其他给定代码的前缀的代码。 这是一个非常笼统的定义。霍夫曼编码是前缀码的一种受限形式。

霍夫曼编码的一个常见用法是最小化(优化)编码“消息”所需的总位数。 “消息”通常是一个符号序列,它通过将每个符号出现映射到 一个特定的前缀代码并在其位置写出前缀代码。可以使用任何一组前缀代码 去做这个。但是,霍夫曼编码会根据比特数产生最短的消息。

例如,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。

【讨论】:

  • @user807566。修正算法。请注意,这可能不是最有效的算法,但它很简单。
【解决方案2】:

比特数总和最小化的要求等价于要求代码是每个符号出现一次的字符串的最佳霍夫曼码。因此,只需创建一个具有 n 个唯一字符的字符串并为其生成 Huffman 树。 The algorithm is outlined on Wikipedia.

【讨论】:

  • 如果所有字符频率都相同,则霍夫曼树将是平衡的二叉树,并且所有字符编码将具有相同的长度,至少其中 N 是 2 的幂。它们将在 1 以内只是因为无法构建完美平衡的树,所以 N 的其他值位。
【解决方案3】:

您的 n=8 示例似乎并不代表最佳解决方案。

10 11 000 001 0100 0101 0110 0111 总位数:26

000 001 010 011 100 101 110 111 总位数:24

当频率恒定时,最佳前缀编码将是固定长度。每个前缀代码的长度为 log(n),并且是从 0..n-1 开始的字母表的二进制表示。

EDIT 适用于 n 不是 2 的幂的情况。

// generate tree
function PCode(n) {
 var a = [];
 for(var x=1; x<=n; x++) {
  a.push({"v":x});
 }
 for(var x=0; x<n-1; x++) {
  var node = {"v": null, "l": a.shift(), "r": a.shift()};
  a.push(node);  
 }
 return a.pop();
}

//print
function Print(node, s) {
 if(node["v"] != null) {
  console.log(s);
 }
 if(node["l"] != null) Print(node["l"], s + "0");
 if(node["r"] != null) Print(node["r"], s + "1");
 return;
}

//test
Print(PCode(3), "");

【讨论】:

    【解决方案4】:

    请查看this C++ tutorial site。它将为您提供有用的 C++ 结构。我在右侧的“相关”部分看到其他类似的 SO 问题可能会有所帮助。

    我以前用 C 语言用递归算法做过这个,是的,这将是一个很好的家庭作业问题。

    【讨论】:

      【解决方案5】:

      生成问题(解码的唯一性)可以通过构建n个叶子节点的二叉树来保证,并枚举每个这样的节点在树中的位置(0是左分支,1是右分支)。你是对的,霍夫曼树有这个属性。请注意,对于霍夫曼树,每个节点的权重等于其代表字符的频率,并且树是使用递归属性构建的,即节点连接的左右决策基于到该点的子节点的总和.这种累积和属性也是为什么斐波那契分布会为霍夫曼树提供最坏情况的压缩。

      注意,霍夫曼编码最适合固定字母的可变编码。非固定字母表的一个示例是决定将“ the ”视为要压缩的集合中的单个元素(而不是两个空格和一个字母)。

      您的问题似乎与替换无关。您只需要 n 个元素的前缀代码,其中所有前缀代码的长度总和最小化。这与构建每个元素频率为 1 的 Huffman 树相同(因为它保证了总编码字符串的最小编码,对您而言,它等于每个编码元素的比特之和恰好一次,即最小化总数位)。注意:这保证了最小的编码,它不保证最快的实现。您可能不需要为每个方法调用构建树。不幸的是,我不知道有什么实现。

      【讨论】:

        【解决方案6】:

        让我们用二进制表示为 1x 的数字对二进制字符串 x 进行编码。否则,0 和 00 将映射到同一个 int。

        std::vector<int> GenerateBinaryPrefixCodes(int n) {
            std::vector<int> list;
            for (int i = n; i != 2 * n; ++i) list.push_back(i);
            return list;
        }
        

        【讨论】:

          猜你喜欢
          • 2020-03-19
          • 2014-02-05
          • 1970-01-01
          • 1970-01-01
          • 2018-03-25
          • 2013-03-20
          • 1970-01-01
          • 2021-07-01
          • 2021-12-14
          相关资源
          最近更新 更多