编码一系列随机可变长度二进制代码的最简洁方法?

时间:2010-01-29 19:01:04

标签: encoding binary integer variable-length space-efficiency

假设您有一个List<List<Boolean>>,并希望以最紧凑的方式将其编码为二进制形式。

我不关心读写性能。我只是想使用最小的空间。此外,该示例在Java中,但我们不限于Java系统。每个“列表”的长度为无界。因此,任何编码每个列表长度的解决方案本身都必须编码一个可变长度的数据类型。

与此问题相关的是可变长度整数的编码。您可以将每个List<Boolean>视为可变长度unsigned integer

请仔细阅读问题。我们不仅限于Java系统。

修改

我不明白为什么很多答案都谈论压缩。我本身并不是在尝试进行压缩,而只是将随机位的编码进行编码。除了每个比特序列具有不同的长度,并且需要保留顺序。

您可以用不同的方式来思考这个问题。假设您有一个随机无符号整数列表(无界)。如何在二进制文件中对此列表进行编码?

研究

我做了一些阅读,发现我真正想要的是Universal code

结果

我将使用论文Elias Omega Coding

中描述的A new recursive universal code of the positive integers变体

我现在明白,较小整数的表示越小,整数就越大。通过简单地选择具有第一个整数的“大”表示的通用代码,当您需要对任意大整数进行编码时,可以在长期运行中节省大量空间。

16 个答案:

答案 0 :(得分:9)

我正在考虑对这样的位序列进行编码:

head  | value
------+------------------
00001 | 0110100111000011

Head的长度可变。它的结束标记为第一次出现1.计算头部中的零数。 value字段的长度为2 ^ zeroes。由于值的长度是已知的,因此可以重复该编码。由于head的大小为log value,因此随着编码值的大小增加,开销会收敛到0%。

附录

如果要更多地调整值的长度,可以添加另一个存储确切长度值的字段。长度字段的长度可以由头部的长度确定。这是一个9位的例子。

head  | length | value
------+--------+-----------
00001 | 1001   | 011011001

答案 1 :(得分:7)

答案 2 :(得分:4)

我使用可变长度整数来编码要读取的位数。 MSB将指示下一个字节是否也是整数的一部分。例如:

11000101 10010110 00100000

实际上意味着:

   10001 01001011 00100000

由于整数持续2次。

这些可变长度的整数将告诉您要读取多少位。并且在所有开头都有另一个可变长度int来告诉有多少位集要读。

从那以后,假设您不想使用压缩,我可以看到在尺寸方面优化它的唯一方法是使其适应您的情况。如果您经常使用较大的位集,则可能需要使用短整数而不是字节来进行可变长度整数编码,这样可能会在编码本身中浪费更少的位。


编辑我认为没有一种完美的方法可以实现您想要的一切。你无法从无到有创造信息,如果你需要可变长度的整数,你显然也必须编码整数长度。 必然在空间和信息之间进行权衡,但也有一些最小的信息,你不能减少使用更少的空间。没有因素以不同的速度增长的系统将无法完美地扩展。这就像试图在对数曲线上拟合直线。你不能这样做。 (而且,这几乎就是你在这里要做的事情。)

你不能在整数之外编码可变长度整数的长度并同时获得无限大小的变量整数,因为这需要长度本身是可变长度的,无论你选择什么算法,它对我来说似乎是常识,只用一个可变长度的整数而不是两个或更多个整数你会更好。

所以这是我的另一个想法:在整数“header”中,为可变长度整数所需的每个字节写一个1。第一个0表示“标题”的结尾和整数本身的开头。

我正在尝试掌握确切的等式来确定为我给出的两种方式存储给定整数所需的位数,但是我的对数是生锈的,所以我会将其绘制下来并稍后编辑此消息包括结果。


编辑2 这是方程式:

  • 解决方案一,每个编码位7位(一次一个完整字节):
    y = 8 * ceil(log(x) / (7 * log(2)))
  • 解决方案一,每个编码位3位(一次一个半字节):
    y = 4 * ceil(log(x) / (3 * log(2)))
  • 解决方案二,每个编码位1个字节加分隔符:
    y = 9 * ceil(log(x) / (8 * log(2))) + 1
  • 解决方案二,每个编码位1个半字节加分隔符:
    y = 5 * ceil(log(x) / (4 * log(2))) + 1

我建议您花时间绘制它们(最好用对数线性坐标系查看)以获得适合您情况的理想解决方案,因为没有完美解决方案。在我看来,第一种解决方案具有最稳定的结果。

答案 3 :(得分:3)

我认为对于“可能最紧凑的方式”,你需要一些压缩,但是霍夫曼编码可能不是那样,因为我认为它最适合使用具有静态每符号频率的字母。

签出Arithmetic Coding - 它对位进行操作,可以适应动态输入概率。我还看到有一个BSD许可Java library,它会为你做这个,似乎期望单个位作为输入。

我认为对于最大压缩,您可以连接每个内部列表(前缀为其长度)并在整个批次中再次运行编码算法。

答案 4 :(得分:3)

我没有看到编码任意位组与压缩/编码任何其他形式的数据有何不同。请注意,您只对要编码的位施加松散限制:即,它们是位列表的列表。有了这个小限制,这个位列表就变成了数据,任意数据,这就是“正常”压缩算法所压缩的。

当然,大多数压缩算法都假设输入在未来(或过去)以某种方式重复,如LZxx系列压缩器,或者具有符号的给定频率分布。

鉴于您的先决条件以及压缩算法的工作原理,我建议您执行以下操作:

  1. 使用较少的字节数包装每个列表的位,使用字节作为位域,编码长度等。
  2. 在生成的字节流上尝试huffman,arithmetic,LZxx等。
  3. 有人可以争辩说,这是非常明显且最简单的方法,并且这不会起作用,因为您的位序列没有已知模式。但事实是,这是你在任何情况下都能做到的最好的事情。

    除非您知道来自数据的某些,或者对这些列表进行某些转换,这些转换会使它们产生某种模式。以JPEG编码中的DCT系数编码为例。列出这些系数(对角线和锯齿形)的方式使得有利于变换的不同系数的输出中的模式。这样,传统的压缩可以应用于结果数据。如果您知道某些位列表允许您以更可压缩的方式重新排列它们(一种显示更多结构的方式),那么您将获得压缩。

答案 5 :(得分:3)

我有一种潜在的怀疑,即在最坏的情况下,你根本无法将真正随机的位组编码为更紧凑的形式。任何类型的RLE都会在错误的输入上对集合进行膨胀,即使它在平均和最佳情况下表现良好。任何类型的周期性或内容特定的近似都会丢失数据。

正如其他海报中所述,您必须知道有关数据集的SOMETHING以更紧凑的形式表示它和/或您必须接受一些损失才能将其变为可预测的形式更紧凑的表达。

在我看来,这是一个信息理论问题,具有无限信息和零损失的约束。您不能以不同的方式表示信息,也不能将其近似为更容易表示的内容。因此,你需要的空间至少与你掌握的信息一样多。

http://en.wikipedia.org/wiki/Information_theory

我想你总是可以作弊,并操纵硬件来对媒体上的一系列离散值进行编码,以梳理出更多的“每比特位”(想想多路复用)。你会花更多时间编码并阅读它。

实际上,您总是可以尝试“抖动”效果,您可以通过多种方式多次编码数据(尝试解释为音频,视频,3D,周期性,顺序,基于键,差异等...)多个页面大小并选择最佳。你几乎可以保证拥有最好的REASONABLE压缩,你最糟糕的情况就不会比你的原始数据集差。

Dunno,如果能让你获得理论上的最佳效果。

答案 6 :(得分:2)

理论限制

如果不了解您想要压缩的数据,这是一个难以回答的问题;对于不同的域名,您的问题的答案可能会有所不同。

例如,来自the Limitations section of the Wikipedia article on Lossless Compression

  

无损数据压缩算法无法保证所有输入数据集的压缩。换句话说,对于任何(无损)数据压缩算法,将存在当由算法处理时不会变小的输入数据集。这可以通过使用计数参数的初等数学来证明。 ...

基本上,由于理论上不可能无损地压缩所有可能的输入数据,因此甚至无法有效地回答您的问题。

实际妥协

只需使用Huffman,DEFLATE,7Z或类似ZIP的现成压缩算法,并将这些位作为可变长度字节数组(或列表,或向量,或者用Java或任何语言调用它们)喜欢)。当然,要读取这些位可能需要一些解压缩,但这可以在幕后完成。您可以创建一个隐藏内部实现方法的类,以在某些索引范围内返回布尔列表或数组,尽管数据存储在内部包字节数组中。在给定索引或索引处更新布尔值可能是个问题,但绝不是不可能的。

答案 7 :(得分:1)

Ints-Encits-of-Ints-Encoding:

  • 当你到达列表的开头时,记下ASCII'['的位。然后进入列表。

  • 当您找到任意二进制数时,请记下与ASCII中数字的十进制表示相对应的位。例如数字100,写入0x31 0x30 0x30。然后写入对应于ASCII','。

  • 的位
  • 当你到达列表的末尾时,记下']'的位。然后写ASCII','。

此编码将编码任意长度的无界整数列表的任意深度嵌套。如果此编码不够紧凑,请使用gzip进行后续处理,以消除ASCII位编码中的冗余。

答案 8 :(得分:0)

您可以将每个List转换为BitSet,然后序列化BitSet-s。

答案 9 :(得分:0)

好吧,首先你会想要将这些布尔值打包在一起,这样你就可以将其中的八个加到一个字节。 C ++的标准bitset就是为此目的而设计的。如果可以的话,您应该原生使用它而不是矢量。

之后,理论上你可以在保存时压缩它以使尺寸更小。我建议不要这样做,除非你背对着墙。

我在理论上说,因为它很大程度上依赖于你的数据。在不了解您的数据的情况下,我真的不能再对此进行说明了,因为有些算法在certian类型的数据上比其他算法更好。事实上,简单的信息理论告诉我们,在某些情况下,任何压缩算法都会产生比你开始时占用更多空间的输出。

如果你的bitset相当稀疏(不是很多0,或者不是很多1),或者是条纹(长时间运行相同的值),那么你可以通过压缩得到很大的收益。在几乎所有其他情况下,它都不值得麻烦。即使在那种情况下也可能不是。请记住,您添加的任何代码都需要进行调试和维护。

答案 10 :(得分:0)

正如您所指出的,没有理由使用比单个位更多的空间来存储布尔值。如果将它与一些基本构造组合在一起,例如每行以编码该行中位数的整数开头,则您将能够存储任何大小的2D表,其中该行中的每个条目都是单个位。

然而,这还不够。任意1和0的字符串将看起来相当随机,并且随着数据随机性的增加,任何压缩算法都会崩溃 - 因此我建议像Burrows-Wheeler Block排序这样的过程大大增加重复“单词”或“在您的数据中阻止“。一旦完成,一个简单的霍夫曼代码或Lempel-Ziv算法应该能够很好地压缩你的文件。

要允许上述方法用于无符号整数,可以使用Delta代码压缩整数,然后执行块排序和压缩(信息检索过帐列表中的标准做法)。

答案 11 :(得分:0)

@zneak的答案(打败我),但使用霍夫曼编码的整数,特别是如果更有可能的话。

只是为了自包含:将列表的数量编码为霍夫曼编码的整数,然后对于每个列表,将其位长度编码为霍夫曼编码的整数。每个列表的位跟随没有中间浪费的位。

如果列表的顺序无关紧要,按长度排序会减少所需的空间,只需要编码每个后续列表的增量长度增加。

答案 12 :(得分:0)

如果我正确理解了这个问题,那么这些位是随机的,我们有一个随机长度的独立随机长度列表列表。由于没有什么可以处理字节,我将讨论这个比特流。由于文件实际上包含字节,因此您需要为每个字节放置8位,并保留最后一个字节的0..7位。

存储布尔值的最有效方法是原样。只需将它们作为一个简单的数组转储到比特流中。

在比特流的开头,您需要对数组长度进行编码。有很多方法可以做到这一点,你可以通过选择最适合你的阵列来节省一些比特。为此,您可能希望使用具有固定码本的霍夫曼编码,以便常用和小值获得最短序列。如果列表很长,你可能不会太在意它以更长的形式编码的大小。

如果没有关于预期列表长度的更多信息,就不能给出关于码本(以及霍夫曼码)将会是什么的精确答案。

如果所有内部列表的大小相同(即你有一个2D数组),当然只需要两个维度。

反序列化:解码长度并分配结构,然后逐个读取这些位,按顺序将它们分配给结构。

答案 13 :(得分:0)

列表-的一览的-整型二进制:

Start traversing the input list
For each sublist:
    Output 0xFF 0xFE
    For each item in the sublist:
        Output the item as a stream of bits, LSB first.
          If the pattern 0xFF appears anywhere in the stream,
          replace it with 0xFF 0xFD in the output.
        Output 0xFF 0xFC

解码:

If the stream has ended then end any previous list and end reading.
Read bits from input stream. If pattern 0xFF is encountered, read the next 8 bits.
   If they are 0xFE, end any previous list and begin a new one.
   If they are 0xFD, assume that the value 0xFF has been read (discard the 0xFD)
   If they are 0xFC, end any current integer at the bit before the pattern, and begin reading a new one at the bit after the 0xFC.
   Otherwise indicate error. 

答案 14 :(得分:0)

这个问题有一定的归纳感。你想要一个功能:(布尔列表) - &gt; (bool list)这样的反函数(bool列表) - &gt; (bool列表列表)生成相同的原始结构,并且编码的bool列表的长度是最小的,而不对输入结构施加限制。由于这个问题是如此抽象,我认为这些列表可能令人难以置信 - 10 ^ 50,或10 ^ 2000,或者它们可能非常小,如10 ^ 0。此外,可能存在大量列表,再次为10 ^ 50或仅为1.因此算法需要适应这些广泛不同的输入。

我认为我们可以将每个列表的长度编码为(bool列表),并添加一个额外的bool来指示下一个序列是另一个(现在更大)长度还是实际比特流。

let encode2d(list1d::Bs) = encode1d(length(list1d), true) @ list1d @ encode2d(Bs)
    encode2d(nil)       = nil

let encode1d(1, nextIsValue) = true :: nextIsValue :: []
    encode1d(len, nextIsValue) = 
               let bitList = toBoolList(len) @ [nextIsValue] in
               encode1d(length(bitList), false) @ bitList

let decode2d(bits) = 
               let (list1d, rest) = decode1d(bits, 1) in
               list1d :: decode2d(rest)

let decode1d(bits, n) = 
               let length = fromBoolList(take(n, bits)) in
               let nextIsValue :: bits' = skip(n, bits) in
               if nextIsValue then bits' else decode1d(bits', length)
assumed library functions
-------------------------

toBoolList : int -> bool list
   this function takes an integer and produces the boolean list representation
   of the bits.  All leading zeroes are removed, except for input '0' 

fromBoolList : bool list -> int
   the inverse of toBoolList

take : int * a' list -> a' list
   returns the first count elements of the list

skip : int * a' list -> a' list
   returns the remainder of the list after removing the first count elements

开销是每个bool列表。对于空列表,开销是2个额外的列表元素。对于10 ^ 2000个bool,开销将是6645 + 14 + 5 + 4 + 3 + 2 = 6673额外的列表元素。

答案 15 :(得分:0)

如果我理解正确,我们的数据结构是(1 2(33483 7)373404 9(337652222 37333788))

格式如下:

byte 255 - escape code
byte 254 - begin block
byte 253 - list separator
byte 252 - end block

所以我们有:

 struct {
    int nmem; /* Won't overflow -- out of memory first */
    int kind; /* 0 = number, 1 = recurse */
    void *data; /* points to array of bytes for kind 0, array of bigdat for kind 1 */
 } bigdat;

 int serialize(FILE *f, struct bigdat *op) {
   int i;
   if (op->kind) {
      unsigned char *num = (char *)op->data;
      for (i = 0; i < op->nmem; i++) {
         if (num[i] >= 252)
            fputs(255, f);
         fputs(num[i], f);
      }
   } else {
      struct bigdat *blocks = (struct bigdat *)op->data
      fputs(254, f);
      for (i = 0; i < op->nmem; i++) {
          if (i) fputs(253, f);
          serialize(f, blocks[i]);
      }
      fputs(252, f);
 }

关于数字数字分布的法律规定,对于任意无符号整数集的集合,字节值越高,发生的越少,因此在末尾放置特殊代码。

每个前面的编码长度不会占用更少的空间,但会使反序列化变得困难。