【问题标题】:Data structure to build and lookup set of integer ranges用于构建和查找整数范围集的数据结构
【发布时间】:2024-01-17 09:02:01
【问题描述】:

我有一组uint32 整数,其中可能有数百万个项目。其中 50-70% 是连续的,但在输入流中它们以不可预知的顺序出现。

我需要:

  1. 将此集合压缩为范围以实现节省空间的表示。已经使用普通算法实现了这一点,因为只计算一次的范围速度在这里并不重要。经过这种转换后,结果范围的数量通常在 5 000-10 000 之间,当然,其中许多是单项。

  2. 测试某个整数的成员资格,不需要有关集合中特定范围的信息。这个必须非常快——O(1)。正在考虑minimal perfect hash functions,但他们在范围方面表现不佳。 Bitsets 空间效率非常低。其他结构,如二叉树,具有 O(log n) 的复杂度,最糟糕的是,实现会产生许多条件跳转,并且处理器无法很好地预测它们,从而导致性能不佳。

有没有专门针对整数范围的数据结构或算法来解决这个任务?

【问题讨论】:

  • 您能具体说明一下您需要哪些操作吗?根据我的阅读,您有一组预先存在的范围,并且您希望从中支持“哪个范围(如果有)包含给定整数?”的操作。这是正确的吗?
  • @templatetypedef:我只需要“是/否”回答“这个数字在集合中吗?”对于预先存在的集合。主要问题是如何在具有实际空间要求的 O(1) 中做到这一点。
  • 另一个想法 - 您是否考虑过使用二元决策图之类的东西?我记得 Don Knuth 曾经谈到使用零抑制二进制决策图来编码大部分为零的函数(在你的情况下,你有一个从 32 位到数字是否存在的函数,而大多数时候它不是)。这将为您提供 O(1) 查找时间(因为每次查找最多需要 32 个步骤),但我不确定它的空间效率如何。
  • 一个 bitset 为 512mb。这真的是空间太大了吗?不会有另一个实用的 O(1) 数据结构。
  • @Markus Kull:512MB 太大了,因为需要同时处理多个集合。

标签: algorithm data-structures integer set range


【解决方案1】:

关于第二个问题:

您可以在Bloom Filters 上查找。布隆过滤器专门设计用于回答 O(1) 中的成员资格问题,尽管响应是 nomaybe(不像是/否那样明确:p)。

maybe 的情况下,当然,您需要进一步处理才能真正回答问题(除非概率答案在您的情况下就足够了),但即便如此,布隆过滤器也可能充当看门人,并拒绝大多数的查询。

此外,您可能希望在不同的结构中保留实际范围和退化范围(单个元素)。

  • 单个元素最好存储在哈希表中
  • 实际范围可以存储在排序数组中

这减少了存储在排序数组中的元素数量,从而减少了在那里执行的二进制搜索的复杂性。既然你说许多范围是退化的,我认为你只有大约 500-1000 个范围(即少一个数量级),并且 log(1000) ~ 10

因此,我建议采取以下步骤:

  • 布隆过滤器:如果没有,则停止
  • 实际范围的排序数组:如果是,则停止
  • 单个元素的哈希表

首先执行排序数组测试,因为从你给出的数字(数百万个数字合并在几千个范围内)如果包含一个数字,它很可能会在一个范围内而不是单个 :)

最后一点:当心 O(1),虽然它看起来很吸引人,但你不是在渐近情况下。几乎没有 5000-10000 范围,因为 log(10000) 大约是 13。所以不要通过获得具有如此高常数因子的 O(1) 解决方案来悲观你的实现,以至于它实际上运行得比 O(log N ) 解决方案:)

【讨论】:

  • 看起来非常实用和有前途。从*的文章中,Bloom Filter 每个项目需要 4.8 位,所以我们可以用大约 25% 的空间开销来获得它。我的阅读正确吗?
  • 我认为,仅以不同的结构存储范围和数字可能是一项重要的突破。
  • @9dan:它是一个参数。根据您希望达到的误报百分比,您可以对其进行调整。然而,挑战通常不是提出mk,而是实际定义散列函数:)
  • @9dan:关于范围/数字的分离,我肯定会在实现布隆过滤器之前从这个开始,因为它是一个复杂的野兽。
【解决方案2】:

如果您事先知道范围是什么,那么您可以使用下面概述的策略检查给定整数是否存在于 O(lg n) 的范围之一中。这不是 O(1),但在实践中仍然相当快。

这种方法背后的想法是,如果您将所有范围合并在一起,那么您在数轴上就会有一组不相交的范围。从那里,您可以通过说区间 [a, b] ≤ [c, d] iff b ≤ c 来定义这些区间的排序。这是一个总排序,因为所有范围都是不相交的。因此,您可以将所有间隔放在一个静态数组中,然后按此顺序对它们进行排序。这意味着最左边的区间在数组的第一个槽中,最右边的区间在最右边的槽中。这种构造需要 O(n lg n) 时间。

要检查某个区间是否包含给定的整数,您可以对该数组进行二进制搜索。从中间间隔开始,检查整数是否包含在该间隔中。如果是这样,你就完成了。否则,如果该值小于该范围内的最小值,则继续向左搜索,如果该值大于该范围内的最大值,则继续向右搜索。这本质上是一个标准的二分搜索,它应该在 O(lg n) 时间内运行。

希望这会有所帮助!

【讨论】:

  • 是的,这是我当前的实现 :) 在我的测试用例中,它比哈希表慢 6-7 倍,但哈希表的空间效率非常低。无论如何,+1 发帖 ;)
  • 您可以通过首先检查最大范围来稍微优化这一点,以获得更好的早期命中率。也许制作一个包含超过一定数量元素的所有范围的单独列表,并对其进行二进制搜索。
  • @actual: implementation detail -> 使用二叉树实际构建范围很好,但是一旦范围稳定,您可以将信息压缩成对的排序数组。二进制搜索具有相同的复杂性,但是它大大增加了内存局部性。
  • @Matthieu M.,是的,我刚刚将类似算法和数据结构的类命名为二叉树。事实上,我对数组使用二进制搜索。
【解决方案3】:

AFAIK 没有在 O(1) 中搜索整数列表的算法。

使用大量内存只能进行 O(1) 搜索。

因此,尝试在整数范围列表中找到 O(1) 搜索算法并不是很有希望。

另一方面,您可以通过仔细检查您的数据集(最终构建一种哈希表)来尝试时间/内存权衡方法。

【讨论】:

  • 感谢您的想法。我想我会尝试在大范围和某种散列上进行二进制搜索,也许是最小的,在单项和小范围上。
  • 好吧,bucket sort 可以在 O(1) 时间内搜索到一个列表。最好说没有使用 comparisons 的算法可以在 O(1) 中搜索整数列表。
【解决方案4】:

可以使用y-fast树或者van Emde Boas树来实现O(lg_w)时间查询,其中w是一个单词的位数,可以使用融合树来实现O(lg_w n)时间查询。就 n 而言,最佳权衡是 O(sqrt(lg(n)))。

其中最容易实现的可能是 y-fast 树。它们可能比二分查找更快,尽管它们大约需要 O(lg w) = O(lg 32) = O(5) 哈希表查询,而二分查找大约需要 O(lg n) = O(lg 10000) = O(13) 比较,所以二分查找可能更快。

【讨论】:

    【解决方案5】:

    而不是基于“比较”的存储/检索(总是 O(log(n)) ), 您需要处理基于“基数”的存储/检索。

    换句话说 .. 从 uint32 中提取 nibbles,然后进行尝试 ..

    【讨论】:

    • 谢谢,我试试看。
    【解决方案6】:

    将您的范围保存在排序数组中并使用二进制搜索进行查找。

    与任何其他基于树的方法相比,它很容易实现,O(log N),并且使用更少的内存和需要更少的内存访问,因此它可能也会快得多。

    【讨论】:

      【解决方案7】:

      根据您对问题的描述,听起来以下可能是一个不错的折衷方案。我已经使用面向对象的语言对其进行了描述,但可以使用联合类型或具有类型成员和指针的结构轻松将其转换为 C。

      使用前 16 位来索引对象数组(大小为 65536)。在该数组中有 5 个可能的对象

      • NONE 对象表示集合中没有以这 16 位开头的元素
      • ALL 对象表示所有以 16 位开头的元素都在集合中
      • RANGE 对象表示最后 16 位在上限和下限之间的所有项目都在集合中
      • SINGLE 对象意味着数组中只有一个以 16 位开头的元素
      • BITSET 对象使用 65536 位位集处理所有剩余情况

      当然,你不需要在 16 位上拆分,你可以调整以反映你的集合的统计信息。事实上,您不需要使用连续位,但它会加快位旋转,并且如果您的许多元素是连续的,如您所说,将提供良好的属性。

      希望这是有道理的,如果我需要更全面地解释,请发表评论。实际上,您已经将深度 2 二叉树与范围和位集相结合,以实现时间/速度的权衡。如果您需要节省内存,请使树更深,并相应地稍微增加查找时间。

      【讨论】:

      • 谢谢,这将是我的 B 计划。
      最近更新 更多