排序整数的压缩算法

时间:2012-09-30 19:40:15

标签: algorithm compression

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

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

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

谢谢。

6 个答案:

答案 0 :(得分:9)

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

诀窍在于准确预测。

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

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

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

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

答案 1 :(得分:7)

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

这是一种delta编码(即每个数字由前一个数字表示),由代码矢量组成

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

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

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

0011 1 1 0000 1 0001

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

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

另一种可能更好的思考方式是将每个delta编码为可变长度位序列:1 i 0(1 | 0) k ,表示i·2 k + [k位后缀]的增量。但是第一个演示文稿与最优性证明更好地对齐。

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

我快速证明了算法的概念,而不用担心优化问题。它仍然很快;对随机样本进行排序比压缩/解压缩需要更长的时间。我尝试了几种不同的种子和矢量大小从16,400,000到31,000,000,值范围为[0,4,000,000,000]。每个数据值使用的位数范围从8.59(n = 31000000)到9.45(n = 16400000)。所有的测试都是用7位后缀完成的; log 2 m / n从7.01(n = 31000000)到7.93(n = 16400000)不等。我尝试使用6位和8位后缀;除了n = 31000000的情况,其中6位后缀略小,7位后缀总是最好的。所以我猜最佳k并不是确切的楼层(log 2 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,增量总是* 2 k ,其中a是后缀代码之前的连续1位的数量。然后索引由位向量中每个N th 1位的位置组成,并且后缀向量中的相应索引(即位向量中与下一个0对应的后缀的索引) )。


答案 2 :(得分:5)

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

  1. 如前所述,将数字转换为增量
  2. 通过7-zip LZMA2算法运行它。甚至是多核准备
  3. 我认为这会给你几乎完美的结果,因为距离的分布很简单。 7-zip可以拿起它。

答案 3 :(得分: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 :(得分:2)

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

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

答案 5 :(得分:2)

您可以简单地使用Delta EncodingProtocol Buffers

像您的示例:4、20、23、40、66。

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

然后,您将所有数字直接存储为协议缓冲区中的varint。 0至127之间的数字仅需要1个字节。还有2个字节,表示128-16384之间的数字...对于大多数场景而言,就足够了。

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

将数字分为2部分。就像17 = ... 0001 0001(binary)=(5)0001。第一部分(5)是有效位数。后缀部分(0001)不带前导1。

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

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