【问题标题】:Compress array of numbers压缩数字数组
【发布时间】:2014-05-28 12:47:00
【问题描述】:

我有一个大数组(约 400.000.000 个条目),整数为 {0, 1, ..., 8}。 所以我每个条目需要 4 位。大约 200 MB。

目前我使用字节数组并在每个条目中保存 2 个数字。

我想知道,如果有一个好的方法来压缩这个数组。我做了一个快速的研究,发现了像 Huffmann 或 LZW 这样的算法。但是这些算法都是为了压缩数据,将压缩后的数据发送给某人并解压缩。

我只想拥有一个内存空间较小的表,以便将其加载到 RAM 中。 200MB 的桌子很容易安装,但我正在考虑更大的桌子。

重要的是,我仍然能够确定某些位置的值。

有什么建议吗?


更多信息: 我只是做了一点实验,结果表明,平均 2.14 个连续数字具有相同的值。 有 1 个零、154 个一、10373 个二、385990 个三、8146188 个四、85008968 个五、265638366 个六、70791576 个七和 80 个八。 所以超过一半的数字是6。

我只需要一个快速的 getValue(idx) 函数,setValue(idx, value) 并不重要。

【问题讨论】:

  • 压缩方案需要字节而不是整数。
  • ...不使用数组?听起来像是 SQL/sqlite 的工作
  • 需要对数据结构进行哪些操作(例如get(index)set(index, value)、...)?
  • 您只需要 [0-8] 范围内的值吗?然后我猜你可以使用一个字节数组每个字节存储两个数字(使用移位操作你可以做到这一点)。然后您可能需要将该逻辑包装在一个对象中,该对象通过虚拟索引(不是支持数组中的真实索引)为您提供 put/get 语义
  • 您可以在内存中压缩,但您可能必须在每次访问或更改数据时解压缩重新压缩。压缩方案通常使用整个文件进行压缩。如果您的数据具有随机分布且未使用的空间很少,那么无论如何您都不会比您正在做的更好。

标签: java arrays performance compression


【解决方案1】:

这取决于您的数据的外观。是否有重复条目,或者它们只是缓慢变化,还是什么?

如果是这样,您可以尝试压缩数据块并在需要时解压缩。块越大,可以节省的内存越多,速度就越差。恕我直言,没什么好交易的。您还可以将压缩和解压缩的数据保存在内存中。

否则,即在没有规律的情况下,每个条目至少需要 log(9) / log(2) = 3.17 位,并且没有什么可以改进它的。

您可以通过将 5 个数字打包到 short 中来非常接近这个值。作为9**5 = 59049 < 65536 = 2**16,它几乎完美契合。每个数字您需要3.2 位,没有大的胜利。通过这个公式给出五个数字的包装

a + 9 * (b + 9 * (c + 9 * (d + 9 * e)))

通过预先计算的表格,解包是微不足道的。

问题更新后更新

更多信息:我只是做了一些实验,结果表明,平均 2.14 个连续数字具有相同的值。有 1 个零、154 个一、10373 个二、385990 个三、8146188 个四、85008968 个五、265638366 个六、70791576 个七和 80 个八。所以超过一半的数字是6s。

平均有大约 2.14 个连续数字相同的事实可能会导致一些压缩,但在这种情况下,它什么也没说。几乎只有五和六,所以遇到两个相等的连续数字似乎是隐含的。

鉴于这个事实,你可以忘记我上面的优化。那里实际上只有 8 个值,因为您可以单独处理单个零。因此,每个值只需要 3 位,零需要一个索引。

您甚至可以为所有低于 4 或高于 7 的值创建一个 HashMap,在那里存储 1+154+10373+385990+80 个条目,并且每个值仅使用 2 位。但这仍然远非理想。

假设没有规律,每个值需要 1.44 位,因为这是 entropy。您可以遍历所有 5 个元组,计算它们的直方图,并使用 1 个字节来编码 255 个最频繁的元组。所有剩余的元组将映射到第 256 个值,告诉您必须在 HashMap 中查找稀有元组值。

一些评价

我很好奇它是否可以工作。将 5 个数字打包成一个字节需要 85996340 个字节。有近 500 万个不适合的元组,所以我的想法是为它们使用哈希映射。假设重新散列而不是链接,让它保持 50% 满是有意义的,所以我们需要 1000 万个条目。假设TIntShortHashMap(将索引映射到元组)每个条目占用 6 个字节,即 60 MB。太糟糕了。

仅将 4 个数字打包到一个字节中会消耗 107495425 个字节并留下 159531 个不适合的元组。这看起来更好,但是,我相信更密集的包装可以改进很多。

这个小program产生的结果:

*** Packing 5 numbers in a byte. ***
Normal packed size: 85996340.
Number of tuples in need of special handling: 4813535.

*** Packing 4 numbers in a byte. ***
Normal packed size: 107495425.
Number of tuples in need of special handling: 159531.

【讨论】:

  • 您可以将 323 放入 1024 位中,而浪费的空间很少。整件事 158.5MB
  • @wilsotc:对,每个数字可以节省近 0.03 位,但访问速度会慢很多(除非您预先计算大小为 2**1024 的表)。
【解决方案2】:

有很多选项 - 大部分取决于您的数据的外观。您可以使用以下任何一种,甚至是它们的组合。

LZW - or variants

在您的情况下,使用 4 位初始字典的变体可能是一个好的开始。

您可以将数据压缩成块,这样您就可以使用请求的索引来确定要即时解码哪个块。

如果您的数据中有重复模式,这将是一个很好的选择。

差分编码

您的编辑表明您的数据可能会受益于差异传递。本质上,您将每个值替换为它与其前任之间的差异。

同样,您需要以块为单位处理数据,并采用不同的固定运行长度。

您可能还会发现使用 LZW 的差分跟踪是一个很好的解决方案。

Fourier Transform

如果某些数据丢失是可以接受的,那么某些傅里叶变换压缩方案可能是有效的。

Lossless JPEG

如果您的数据具有二维方面,那么某些 JPEG 算法可能会很好地发挥作用。

底线

你需要记住:

  1. 您花费在压缩上的时间越长 - 达到一个限制 - 您可以实现的压缩率就越高
  2. 无损压缩可以走多远确实存在实际限制。
  3. 一旦你有损,你基本上不再受到限制。您可以使用 new int[]{6} 来近似您的全部数据,并获得相当多的正确结果。

【讨论】:

    【解决方案3】:

    由于超过 1/2 的条目是 6,因此只需将它们编码为单个位。使用 2 位表示第二个最常见的位,依此类推。然后你有这样的东西:

                            encoding               total   
               #entrie      bit pattern  #bits    # of bits
     zero            1      000000001      9              9
     ones          154      0000001        7           1078  
     twos        10373      000001         6          62238
     threes     385990      00001          5        1929950
     fours     8146188      0001           4       32584752
     fives    85008968      01             2      170017936
     sixes   265638366      1              1      265638366
     sevens   70791576      001            3      212374728
     eights         80      00000001       8            640
    --------------------------------------------------------
     Total                                        682609697 bits
    

    如果使用 682609697 位编码的 429981696 个条目,则平均每个条目需要 1.59 位

    编辑:

    为了实现快速查找,您可以在压缩数据中创建一个索引,以显示每 n 个条目的开始位置。然后,找到确切的值将涉及平均解压缩 n/2 个条目。根据它应该有多快,您可以调整索引中的条目数。要减小指向压缩数据的指针大小(以及索引大小),请使用估计值并仅存储该估计值的偏移量。

                                    Estimated pos   Offset from
    # entry no   Actual Position     (n * 1.59)      estimated
         0             0                  0               0      
       100           162                159               3      Use this 
       200           332                318              14  <-- column as   
       300           471                477              -6      the index
       400           642                636               6
       500           807                795              12
       600           943                954             -11
    

    这样一个索引的开销(每 100 个条目和 10 位偏移量)意味着每个条目额外 0.1 位

    【讨论】:

    • 好主意,但我需要一个快速的 getValue(index) 函数。使用您的方法,我将不得不将完整的数据集分成小块。我想它会占用更少的位,但肯定会超过 1.59。
    【解决方案4】:

    有 1 个零,154 个一,10373 个二,385990 个三,8146188 个四, 85008968 个五人制、265638366 个六人制、70791576 个七人制和 80 个八人制

    总计 = 429981696 个符号

    假设分布是随机的,熵定理表明你不能做得比 618297161.7 位 ~ 73.707 MB 或平均 1.438 位/符号更好。

    最小位数为 SUM(count[i] * LOG(429981696 / count[i], 2))。

    您可以使用范围编码器实现此大小。

    给定 Sqrt(N) = 20736

    再次,您可以通过在每个 SQRT( N) 解码符号。这允许对下一个 20736 个符号块进行快速解码。

    如果以线性方式访问内存流,访问元素的复杂度会降低到 O(1)。

    使用的额外内存:20736 * 4 = 81KB。

    【讨论】:

      【解决方案5】:

      考虑一些缓存解决方案怎么样,例如mapdbapache jcs。这将使您能够将集合保存到磁盘,从而使您能够处理非常大的列表。

      【讨论】:

        【解决方案6】:

        您应该查看BitSet 以最有效地存储它。与顾名思义相反,它并不完全是一个集合,它有顺序,您可以按索引访问它。

        在内部,它使用longs 的数组来存储位,因此可以使用位掩码进行自我更新。

        我不相信你可以在本地更有效地存储它,如果你想要更高的效率,那么你应该考虑打包/压缩算法。

        【讨论】:

          猜你喜欢
          • 2015-02-06
          • 2019-10-17
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-01-24
          • 1970-01-01
          相关资源
          最近更新 更多