【问题标题】:A space efficient data structure to store and look-up through a large set of (uniformly distributed) Integers一种节省空间的数据结构,用于存储和查找大量(均匀分布的)整数
【发布时间】:2012-04-02 23:20:18
【问题描述】:

我需要在内存中保存并查找一百万个均匀分布的整数。 我的工作量非常密集。
我当前的实现使用 HashSet (Java)。我看到了良好的查找性能,但内存使用并不理想(几十 MB)。
您能想出更高效的(内存)数据结构吗?
编辑:该解决方案将需要支持对数据结构的少量添加。

背景:
上述整数问题是以下问题的简化:
我有一组一百万个字符串(我的“字典”),我想知道字典是否包含给定的字符串。
字典太大而无法放入内存,因此我愿意牺牲一点点准确性来减少内存占用。我将通过切换到包含每个字符串的哈希码值(整数)而不是实际字符的字典来做到这一点。我假设每个字符串发生冲突的可能性仅为1M/2^32

【问题讨论】:

  • 可能是Bloom filter 的工作吗?
  • 您是否需要insert 操作,或者字典是否只构建一次并且在查找期间不再修改?
  • @Gareth Rees:您为什么不将其发布为答案,以便对其进行投票?
  • @Andre - 感谢您的细心阅读。我确实犯了一个错误,没有明确说明解决方案需要支持对数据结构的少量添加。

标签: java memory-management data-structures hashcode


【解决方案1】:

虽然 Jon Skeet 的回答可以通过少量投资节省大量资金,但我认为您可以做得更好。由于您的数字分布相当均匀,因此您可以使用插值搜索来加快查找速度(大约 O(log log N) 而不是 O(log N))。对于 100 万件商品,您可能计划进行 4 次左右的比较,而不是 20 次左右。

如果您想多做一点工作以再次将内存(大致)减半,您可以将其构建为两级查找表,基本上是一种简单的 trie 版本。

你会将你的(大概)32 位整数分成两个 16 位的部分。您将使用前 16 位作为第一级查找表的索引。在这个级别上,您将拥有 65536 个指针,一个对应于整数的该部分的每个可能的 16 位值。这将带你到桌子的第二层。对于这一部分,我们将在所选指针和下一个指针之间进行二进制或插值搜索 - 即第二级中前 16 位中具有相同值的所有值。

然而,当我们查看第二张表时,我们已经知道了 16 位的值——因此我们不必存储所有 32 位的值,而只需要存储 other 16 位的价值。

这意味着第二级不再占用 4 兆字节,而是将其减少到 2 兆字节。除此之外,我们还需要一级表,但它只有 65536x4=256K 字节。

这几乎肯定会提高整个数据集的二进制搜索的速度。在最坏的情况下(对第二级使用二分搜索),我们可以进行多达 17 次比较(1 + log2 65536)。平均值会比这更好——因为我们只有一百万个项目,每个二级“分区”中平均只能有 1_000_000/65536 = ~15 个项目,大约为 1 + log2 (16) = 5 次比较。在第二级使用插值搜索可能会进一步减少这一点,但是当您仅从 5 次比较开始时,您没有太多空间进行真正显着的改进。鉴于第二级平均只有约 15 个项目,您使用的搜索类型不会有太大区别——即使是线性搜索也会非常快。

当然,如果您愿意,您可以更进一步,改用 4 级表(整数中的每个字节一个表)。然而,这是否会为您节省足够多的麻烦以值得麻烦,这可能是值得商榷的。至少马上,我的直接猜测是你会做相当多的额外工作以获得相当少的节省(仅存储百万整数的最终字节显然占用 1 兆字节,而导致这一点的三级表显然会占用更多的空间,因此您可以将级别数加倍以节省大约半兆字节。如果您处于节省多一点会产生很大影响的情况,那就去吧-否则,我怀疑回报是否能证明额外投资的合理性。

【讨论】:

  • 确实是不错的优化。我想一旦我拥有多一/两个数量级的数据,它就会派上用场。
  • 有示例实现吗?
  • @Horcrux7:我当时写了一个概念证明,以确保它在发布之前可以工作。我今晚回家时可以找找(如果我记得的话),但六年后我很有可能把它扔掉了。
  • @JerryCoffin 我尝试在 PagedIntSet 中实现它并将其托管在LargeIntegerSet。如果您看到更高效版本的选项,请贡献它。
【解决方案2】:

听起来你可以只保留一个排序的int[],然后进行二分搜索。对于一百万个值,大约需要 20 次比较才能得出任何值 - 这是否足够快?

【讨论】:

  • 但这应该与 HashSet 具有相同的内存占用,不是吗?
  • @user949300:不,因为HashSet 必须为每个值分配一个Integer 对象(大小差别很大!),并且它不会像我建议的那样人口密集,并且它将必须保留每个值的哈希码并且它将为每个值都有一个Entry对象(这将当然,包含哈希码,所以这有点重复计算)。当然,所有这些都会更多地破坏缓存并占用更多内存。
  • @user949300 不,HashSet 由 HashMap 支持,并且有很多空单元格,具体取决于您存储的实际值。
  • 一些较小的整数已经被缓存(没有内存分配)但是是的,它会分配一堆大整数。所以你的想法节省了大约 4 倍的空间,甚至不包括我忘记的 Entry 对象。今天下午我显然太关注我的梦幻棒球队了。 :-)
  • @GiliNachum:不,这听起来像是您可能管理的最好的。正如你所说,保持一个大的int[] 和一个HashSet<Integer> - 只有当它开始变大时才将集合合并到数组中。
【解决方案3】:

如果您愿意接受少量误报以换取大量减少内存使用量,那么Bloom filter 可能正是您所需要的。

布隆过滤器由 k 个哈希函数和一个 n 位的表组成,最初为空。要将项目添加到表中,请将其提供给每个 k 散列函数(获取一个介于 0 和 n-1 之间的数字)并设置相应的位。要检查某个项目是否在表中,请将其提供给每个 k 散列函数,并查看是否设置了所有相应的 k 位。

误报率为 1% 的布隆过滤器每个项目需要大约 10 位;当您为每个项目添加更多位时,误报率会迅速降低。

Here's an open-source implementation in Java.

【讨论】:

    【解决方案4】:

    你可能想看看BitSet Lucene 中使用的那个甚至比标准的Java 实现更快,因为它忽略了一些标准的边界检查。

    【讨论】:

    • 我的第一个响应是一个 Bitset,但后来意识到由于 hashCodes 上升到 2^31,如果同时包含非常低的值(接近 0)和大的值(接近 MAXINT),大小可能会很大. Alsom,你需要两个 Bitset,一个用于正面,一个用于负面
    • @user949300 如果您需要两个位集,那么每个位集将保留一半位,并且存储 2^32 位需要 2^29 个字节 - 很大但并非不可能。我认为来自不同答案的布隆过滤器会是一个更好的解决方案,因为它可以实现更低的内存使用率和更高的误报拒绝率。
    【解决方案5】:

    Github 项目LargeIntegerSet 中有一些用于整数的 Java 实现,减少了内存消耗。

    【讨论】:

      【解决方案6】:

      有一些IntHashSet 可用的原语实现。

      快速谷歌搜索得到我this one。还有一个 IntHashSet 的 apache [开源] 实现。我更喜欢 apache 实现,虽然它有一些开销 [它被实现为 IntToIntMap]

      【讨论】:

        【解决方案7】:

        我认为您可能会重新考虑原始问题(具有有效的单词列表),而不是尝试优化“优化”。

        我建议研究 Radix 树/Trie。

        https://en.wikipedia.org/wiki/Radix_tree 要么 https://en.wikipedia.org/wiki/Trie

        您基本上是在存储某种带有字符串前缀的树,每次在字典中有选择时都会分支。它有一些有趣的副作用(允许非常有效地过滤前缀),可以为具有较长公共前缀的字符串节省一些内存并且相当快。

        一些示例实现:

        https://lucene.apache.org/core/4_0_0/analyzers-stempel/org/egothor/stemmer/Trie.html

        https://github.com/rkapsi/patricia-trie

        https://github.com/npgall/concurrent-trees

        这里有各种实现的有趣比较,更关注性能而不是内存使用,但它仍然很有帮助

        http://bhavin.directi.com/to-trie-or-not-to-trie-a-comparison-of-efficient-data-structures/

        【讨论】:

          猜你喜欢
          • 2010-09-26
          • 1970-01-01
          • 2023-03-18
          • 2016-02-14
          • 1970-01-01
          • 2020-01-17
          • 2015-06-25
          • 2016-05-11
          • 2013-03-24
          相关资源
          最近更新 更多