【问题标题】:compression algorithm for sorted integers排序整数的压缩算法
【发布时间】:2012-09-21 18:43:31
【问题描述】:

我有一个从最低到最高排序的随机整数序列。数字从 1 位开始,到 45 位附近结束。在列表的开头,我的数字非常接近:4、20、23、40、66。但是当数字开始变大时,它们之间的距离也有点高(实际上它们之间的距离是偶然的)。没有重复的数字。

我使用bit packing 来节省一些空间。尽管如此,这个文件可能会变得非常大。

我想知道在这种情况下可以使用哪种压缩算法,或者任何其他技术来尽可能节省空间。

谢谢。

【问题讨论】:

  • 我没有关注。你能画一张(可能是 ASCII 码)图片或序列吗
  • @Keyser 1, 23, 33, 34,39, 80, 122, 125, 169, 168, 203,这样下去
  • 您可以将列表表示为相邻数字之间的距离——22、10、1、5、41、42、3...(如果我的算术正确的话)。由于这些数字较短,您可以将它们更紧密地打包在一起。 (请注意,您应该在序列的开头添加一个虚拟零以生成初始 1,或者以其他方式记录起始编号。)您甚至可以玩第二次做差异的技巧以得出一个有符号的number -- -12, -9, 4, 36, 1, -39... 这可能会或可能不会更好地压缩(取决于数字分布)。
  • 请注意,您可以使用几种可变长度表示法之一。例如,以11b开头的数字是3字节(22位)的数字,以10b开头的数字是2字节(14位)的数字,以0b开头的数字是1字节(7位)的数字。您可以将此方案与您的钻头包装相结合,以得出最适合您需求的任何尺寸数字。
  • (这将有助于了解您的使用场景——如果您需要在不解包整个列表的情况下进行随机访问,某些方案会比其他方案更难访问,例如,第 27 个条目。)

标签: algorithm compression


【解决方案1】:

如果您知道数据的真实分布,您可以优化压缩。如果您可以提供每个整数的概率分布,您可以使用算术编码或其他熵编码技术来压缩到理论上的最小尺寸。

诀窍在于准确预测。

首先,您可能应该压缩数字之间的距离,因为这可以让您进行统计报表。如果您要直接压缩数字,您将很难对它们进行建模,因为它们只出现一次。

接下来,您可以尝试构建一个非常简单的模型来预测下一个距离。保留所有先前看到的距离的直方图,并根据频率计算概率。

您可能需要考虑缺失值(您显然不能为它们分配 0 概率,因为那是不可表达的)但您可以为此使用启发式方法,例如逐位编码下一个距离和 预测每个位单独。您几乎不需要为高位支付任何费用,因为它们几乎总是 0,而熵编码会将它们优化掉。

如果您了解分布,所有这一切都会简单得多。示例:您正在压缩所有素数的列表,您知道距离的理论分布,因为有公式。所以你已经有了一个完美的模型。

【讨论】:

  • 有一个概率。我从 1 位数到接近 43 位,但每个数字都存储在不同的文件中,这是偶然的。这就是为什么每个文件都有随机但排序的数字。有 256 个文件,因此下一个数字进入一个文件而不是另一个文件的概率是 1/256。
  • 所以这意味着距离遵循一个完全已知的分布?如果是,您在最后一段中得到了答案。
  • 您也可以有一个文件,并为每个数字存储一个 8 位整数来表示它属于哪个文件。这为您提供了最佳压缩(因为文件是均匀分布的)和每个数字 8 位。
  • 它们分布不均,抱歉。我正在使用哈希函数来确定每个数字应该去哪个文件,所以没有 1/256 的 100% 分布
  • 几乎是完美的发行版。如果值到文件的分布是由散列函数控制的,那么这个方案是你能做的最好的。对于每个数字,您实际上是在生成一个随机的 8 位值。
【解决方案2】:

有一种非常简单且相当有效的压缩技术,可用于已知范围内的排序整数。与大多数压缩方案一样,它针对串行访问进行了优化,但您可以根据需要构建索引来加速随机访问。

这是一种增量编码(即每个数字由与前一个数字的距离表示),由一个编码向量组成

  • 单个 1 位,表示 2k 的 delta,在以下代码中添加到 delta,或

  • 一个 0 位后跟一个 k 位增量,表示下一个数字是前一个数字的指定增量。

例如,如果 k 为 4,则序列:

00011 1 1 00000 1 00001

编码三个数字。第一个四位编码 (3) 是第一个 delta,取自初始值 0,因此第一个数字是 3。接下来的两个单独的 1 累加为 2·24 , 或 32,将其添加到以下 0000 的增量中,总共为 32。所以第二个数字是 3+32=35。最后最后一个delta是单个24加1,一共17个,第三个数是35+17=52。

1 位表示下一个 delta 应该增加 2k(或者,更一般地说,每个 delta 增加 2k 倍立即数前 1 位。)

另一种可能更好的想法是将每个增量编码为可变长度的位序列:1i0(1|0)k,表示 i·2k+[k 位后缀]的增量。但第一个演示文稿更符合最优性证明。

由于每个“1”码代表2k的增量,所以不能超过m/2k个,其中m是最大的数设置为压缩。其余代码均对应数字,总长度为 n·(k + 1),其中 n 为集合的大小。 k 的最佳值大致为 log2 m/n,在您的情况下为 7 或 8。

我对算法的概念进行了快速验证,而无需担心优化问题。它仍然很快;对随机样本进行排序比压缩/解压缩它需要更长的时间。我尝试了一些不同的种子和向量大小,从 16,400,000 到 31,000,000,值范围为 [0, 4,000,000,000)。每个数据值使用的位范围从 8.59 (n=31000000) 到 9.45 (n=16400000)。所有的测试都是用 7 位后缀完成的; log2 m/n 从 7.01 (n=31000000) 到 7.93 (n=16400000) 不等。我尝试使用 6 位和 8 位后缀;除了在 n=31000000 的情况下,6 位后缀略小,7 位后缀总是最好的。所以我猜想最优 k 不完全是 floor(log2 m/n) 但离它不远了。

压缩代码:

void Compress(std::ostream& os,
              const std::vector<unsigned long>& v,
              unsigned long k = 0) {
  BitOut out(os);
  out.put(v.size(), 64);
  if (v.size()) {
    unsigned long twok;
    if (k == 0) {
      unsigned long ratio = v.back() / v.size();
      for (twok = 1; twok <= ratio / 2; ++k, twok *= 2) { }
    } else {
      twok = 1 << k;
    }
    out.put(k, 32);

    unsigned long prev = 0;
    for (unsigned long val : v) {
      while (val - prev >= twok) { out.put(1); prev += twok; }
      out.put(0);
      out.put(val - prev, k);
      prev = val;
    }
  }
  out.flush(1);
}

解压:

std::vector<unsigned long> Decompress(std::istream& is) {
  BitIn in(is);
  unsigned long size = in.get(64);
  if (size) {
    unsigned long k = in.get(32);
    unsigned long twok = 1 << k;

    std::vector<unsigned long> v;
    v.reserve(size);
    unsigned long prev = 0;
    for (; size; --size) {
      while (in.get()) prev += twok;
      prev += in.get(k);
      v.push_back(prev);
    }
  }
  return v;
}

使用可变长度编码可能有点尴尬;另一种方法是将每个代码(1 或 0)的第一位存储在一个位向量中,并将 k 位后缀存储在一个单独的向量中。如果 k 为 8,这将特别方便。

导致文件稍长但更容易为其构建索引的变体是仅使用 1 位作为增量。那么对于某个 a,可能为 0,delta 始终为 a&centerdot;2k,其中 a 是后缀代码之前的连续 1 位的数量。然后,该索引由位向量中每个第 Nth 个 1 位的位置以及后缀向量中的相应索引(即与位向量中的下一个 0 对应的后缀的索引)组成)。


【讨论】:

  • 这行得通,但它不是最有效的,因为此代码对数据分布做出了某些假设。如果该假设是错误的,它将浪费比特,可能比未压缩的要大。这是一个有效的答案。
  • @usr,如果选择与原始数据向量大小对应的k,则不能大于未压缩。我对此添加了一些详细说明。它不对数据的分布做任何假设;仅关于数据量。
  • @usr, ... 数据的数量和范围(编辑速度不够快)
  • @rici 您的解决方案是否有更好的文档/手册/说明?很抱歉我的无知,但我仍然无法完全理解它
  • @JonathanAllan:是的,我认为你是对的。如果 k 为四,则二进制部分必须为五位(其中第一位为 0)。我想我修复了这个例子(以及几个挥之不去的格式问题)。谢谢。
【解决方案3】:

过去对我来说效果很好的一个选项是将 64 位整数列表存储为 8 个不同的 8 位值列表。您存储数字的高 8 位,然后存储接下来的 8 位,等等。例如,假设您有以下 32 位数字:

0x12345678
0x12349785
0x13111111
0x13444444

存储的数据将是(十六进制):

12,12,13,13
34,34,11,44
56,97,11,44
78,85,11,44

然后我通过放气压缩机运行它。

我不记得我能用这个实现什么压缩比,但它比压缩数字本身要好得多。

【讨论】:

    【解决方案4】:

    我想用最简单的解决方案添加另一个答案:

    1. 如前所述将数字转换为增量
    2. 通过 7-zip LZMA2 算法运行它。它甚至支持多核

    我认为这将在您的情况下给出几乎完美的结果,因为距离分布很简单。 7-zip 就能把它捡起来。

    【讨论】:

    • 我尝试了 LZMA 和 LZMA2 但 zpaq 得到了更好的结果,还是谢谢你。
    • Nanozip 也值得一试。它是顶级压缩机之一,并且仍然可以快速使用。
    【解决方案5】:

    如果您的序列由 -随机数组成,例如可能由典型的数字计算机生成,那么为了表示简洁,我认为任何压缩方案都不会胜过,只需存储生成器的代码以及定义其初始状态所需的任何参数。

    如果您的序列由以某种非确定性方式生成的真正随机数组成,那么已经发布的其他答案提供了各种好的建议。

    【讨论】:

      【解决方案6】:

      您可以简单地使用Delta EncodingProtocol Buffers

      就像你的例子:4、20、23、40、66。

      Delta 编码压缩:4、16、3、17、26。

      然后您将所有数字作为 varint 直接存储在 Protocol Buffers 中。 0-127 之间的数字只需要 1 个字节。 128-16384 之间的数字需要 2 个字节......这对于大多数场景来说已经足够了。

      您还可以使用熵编码 (huffman) 来实现比 varint 更有效的压缩率。每个数字甚至不到 8 位。

      将一个数字分成 2 部分。比如 17=...0001 0001(二进制)=(5)0001。第一部分 (5) 是有效位数。后缀部分(0001)没有前导1。

      如示例:4、16、3、17、26 = (3)00 (5)0000 (2)1 (5)0001 (5)1010

      即使有很多数字,第一部分也会在 0-45 之间。因此可以像霍夫曼那样通过熵编码有效地压缩它们。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-09-21
        • 1970-01-01
        • 1970-01-01
        • 2020-05-17
        • 2013-05-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多