【问题标题】:Sorted data structure with O(logN) insertion that gives insertion point index具有 O(logN) 插入的排序数据结构,提供插入点索引
【发布时间】:2015-03-03 01:08:07
【问题描述】:

我的目标是一个可以完成两件事的排序数据结构:

  1. 快速插入(在位置根据排序顺序)
  2. 我可以快速地将我的数据分割成所有大于或小于或等于元素的集合。 我需要知道每个分区的大小,并且我需要能够“获取”这些分区。

目前,我正在使用 ArrayList 在 java 中实现这一点,它很容易提供 #2,因为我可以执行二进制搜索 (Collections.binarySearch) 并获得一个插入索引,告诉我在什么时候插入一个元素。然后基于索引范围从 0 到数组大小的事实,我立即知道有多少元素大于我的元素或小于我的元素,并且我可以轻松获取这些元素(作为子列表)。但是,这没有属性 #1,并导致过多的数组复制。

这让我想使用像 SkipList 或 RedBlackTree 这样可以更快地执行插入的东西,但是我不知道如何在不花费 O(N) 时间的情况下满足属性 #2。

任何建议将不胜感激。谢谢

编辑:感谢以下参考数据结构的答案,这些数据结构在 O(logN) 时间内执行插入并且也可以快速分区,但我想强调 size() 要求 - 我需要知道这些分区而不必遍历整个分区(其中,according to this 是 TreeSet 所做的。这背后的原因是,在我的用例中,我使用多个不同的数据结构副本来维护我的数据,每个副本都使用不同的比较器,然后需要问“根据什么比较器是大于特定元素最小的所有事物的集合”。在ArrayList 的情况下,这实际上很容易,并且只需要 O(YlogN) 其中 Y 是比较器的数量,因为我只需对每个 Y 数组进行二进制搜索并返回具有最高插入索引的数组列表。我不清楚如何在不采用 O(YN) 的情况下使用 TreeSet。

我还应该补充一点,即使无法准确求解,插入索引的近似答案仍然很有价值。

【问题讨论】:

  • 我可能有误解,但是将堆实现为数组有什么问题?

标签: java arrays data-structures


【解决方案1】:

使用普通的Java TreeSet。插入需要 O(logN),因此您的第 1 个要求已完成。这是文档中的引用:

此实现为基本操作(添加、删除和包含)提供有保证的 log(n) 时间成本。

当它实现NavigableSet interface 时,您有#2 或您对以下方法的要求:

这些操作被包含/排除所提供边界的版本重载。

TreeSet 非常灵活:它允许您定义Comparator 以自定义方式对Set 进行排序,或者您也可以依赖元素的自然排序

编辑:

根据返回子集size()操作O(n)的要求,恐怕Java API中没有adhoc实现。

确实,TreeSet范围操作返回的集合视图,通过在O(log n)时间'跳转'到视图的第一个元素,实现size(),然后遍历后续元素,在每次迭代,直到到达子集的末尾。

我必须说这很不幸,因为它并不总是需要遍历返回的子集视图,但有时,提前知道子集的大小可能非常有用(因为它是你的用途案例)。

因此,为了满足您的要求,您需要另一个结构,或者至少是一个辅助结构。经过一番研究,我建议你使用Fenwick tree。 Fenwick 树也称为二进制索引树 (BIT),可以是不可变的或可变的。不可变版本用数组实现,而可变版本可以用平衡二叉树实现,即红黑树(Java TreeSet实际上是用红黑树实现的)。 Fenwick 树主要用于存储频率并计算在O(log n) 时间内直到给定元素的所有频率的总和。

请参阅this question here on Stack Overflow 以获得对这个完全未知但非常有用的结构的完整介绍。 (由于Stack Overflow里有解释,这里就不复制了)。

Here's another Stack Overflow question 询问如何正确初始化 Fenwick 树,here's actual Java code showing how to implement Fenwick tree's operations。最后,here's a very good theoretic explanation 关于结构和使用的底层算法。

网络中所有示例的问题在于它们使用结构的不可变版本,这不适合您,因为您需要在向结构中添加元素的过程中交错查询。但是,它们对于充分理解所使用的结构和算法都非常有用。

我的建议是你研究 Java TreeMap 的实现,看看如何修改/扩展它,这样你就可以把它变成一个 Fenwick 树,保持 1 的值每把钥匙。这个1 将是每个键的频率。因此,Fenwick 树的基本操作getSum(someElement) 实际上会在O(log n) 时间内返回从第一个元素到someElement 的子集的大小。

因此,挑战在于实现一个平衡树(实际上是 Java 的 Red-Black TreeMap 的后代),它实现了您需要的所有 Fenwick 树的操作。我相信你已经完成了getSum(somElement),但也许你也可以扩展返回的子树范围视图,以便在为范围视图实现size() 操作时它们都引用getSum(someElelment)

希望这会有所帮助,至少我希望这是一个很好的起点。如果您需要说明和示例,请告诉我。

【讨论】:

  • 谢谢你,但我担心这些子集上的 size() 运算符的复杂性,我认为它是 O(K)(其中 K 是子集的大小)。请参阅this answer,它显示的源代码似乎表明是这种情况。不幸的是,我不仅需要快速访问分区集,还需要知道它们的大小。
  • @newmanne 这很难。您对size() 不是O(n) 的要求是完全具有挑战性的!问题:你需要结构是可变的吗?即,您是否需要在 获取给定元素最接近最后一个元素的比较器之后添加元素?
  • 我不需要大于给定元素的元素视图是可变的。我确实需要整体数据结构是可变的,因为我将通过添加新数据点来交叉查询。这能回答你的问题吗?
  • @newmanne 是的,它回答了我的问题。但恐怕这也会使问题变得更加困难。 :)
  • @newmanne 我已经编辑了我的答案,并提供了有关解决方案的可能见解。请让我知道您是否需要澄清或讨论某些问题。
【解决方案2】:

如果您不需要重复元素(或者如果您可以使元素看起来不同),我会使用java.util.TreeSet。它符合您规定的要求。

  1. 由于二叉树结构的 O(log n) 排序插入
  2. O(log n) 分段时间使用in-place subsets

不幸的是,由于answer you linked 中的原因,您需要始终知道段的大小,因此 O(log n) 分段时间有效地减慢到 O(n)。就地子集在您询问它们之前不知道它们的大小,然后它们会计数。计数的大小被存储,但如果支持集以任何方式更改,则子集必须再次计数。

【讨论】:

  • 谢谢,但正如我对上面的回复所评论的那样,虽然 O(N) 分段时间很棒,但我正在努力解决的问题是,就我而言,我需要知道这些分段的大小在亚线性时间内,我目前的理解是 TreeSet 将计算子集大小的线性大小。
  • 你说的是真的;抱歉,我忽略了您对细分市场的尺寸要求。我自己追踪了源代码,得出了同样的结论。我更新了我的答案以反映这些信息。
【解决方案3】:

我认为解决这个问题的最佳数据结构是B-Treedense index。这样的 B-Tree 是由以下内容构建的: - 内部节点只包含指向子节点的指针 - 包含指向分页数组的指针的叶子 - 多个大小相等的数组(页)

不幸的是,Java 中 B-Tree 的通用实现很少,可能是因为存在太多变体。

插入成本为

  • O(log(n)) 找到位置
  • O(p) 将新元素插入页面(其中 p 是恒定页面大小)

也许这个数据结构也涵盖了你的分割问题。如果不是:提取成本将是

  • O(log(n)) 找到边界
  • O(e) 复制提取(其中 e 是提取的大小)

【讨论】:

    【解决方案4】:

    获得所需内容的一种简单方法是使用每个节点的左右子树大小来增加您最喜欢的二叉搜索树数据结构(红黑树、AVL 树等) --- 称它们为 L 尺寸R 尺寸

    假设可以有效地更新树数据结构中的这些字段(比如恒定时间)。那么这就是你得到的:

    • 插入、删除和所有常规二叉搜索树操作与您选择的数据结构一样高效 --- 对于红背树,O(log n)。
    • 给定一个键 x,您可以在 O(log n) 时间内获得树中键小于 x 的元素的数量,方法是沿着树向下找到 x 的适当位置,总结 L-每次“向右”时,大小(为您正在遍历的实际节点加一)。 “大于”的情况是对称的。
    • 给定一个键 x,您可以通过再次沿着树向下找到 x 的适当位置,获得在 O(log n + |x_L|) 时间内小于 x 的元素的排序列表 x_L,并且每次你向右走时,你标记你刚刚遍历的节点,将它附加到列表 h_L。然后按顺序遍历 h_L 中的每个节点(按添加到 h_L 的顺序)将为您提供 x_L(已排序)。 “大于”的情况是对称的。

    最后,对于我的回答,我需要向您保证,我们可以有效地维护这些 L 和 R 大小,以供您选择特定的树数据结构。我将考虑红黑树的情况。

    请注意,对于普通二叉搜索树,维护 L-sizes 和 R-sizes 是在恒定时间内完成的(当您从根开始添加一个节点时,如果该节点应该进入左子树,只需在 L-sizes 上加一,或者如果它在右子树中,则为 R 大小)。现在红黑树的附加平衡程序仅通过节点的局部旋转来改变树结构---参见Wikipedia's depiction of rotations in red-black trees。很容易看出,P 和 Q 的旋转后 L-size 和 R-size 可以从 A、B、C 的 L-size 和 R-size 重新计算。这只会给红黑树的操作增加固定的工作量。

    【讨论】:

      猜你喜欢
      • 2013-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-10-08
      • 1970-01-01
      相关资源
      最近更新 更多