【问题标题】:Looking for a data container with O(1) indexing and O(log(n)) insertion and deletion寻找具有 O(1) 索引和 O(log(n)) 插入和删除的数据容器
【发布时间】:2012-05-07 07:25:40
【问题描述】:

我不确定这是否可能,但对我来说似乎有点合理,我正在寻找一种允许我执行这些操作的数据结构:

  • 用 O(log n) 插入一个项目
  • 用 O(log n) 移除一个项目
  • 查找/编辑 O(1) 中的第 k 个最小元素,用于任意 k(O(1) 索引)

当然,编辑不会导致元素顺序发生任何变化。使之成为可能的原因是我将以递增的顺序一个接一个地插入元素。因此,例如,如果我尝试第五次插入,我确信在这之前的所有四个元素都比它小,而在这之后的所有元素都会更大。

【问题讨论】:

  • 我不确定这样的数据结构是否存在。通常,您需要权衡每种不同类型的数据结构,而您所描述的是一种功能异常强大的数据结构。我敢肯定,如果存在这样的东西,它会比堆/树/等更普遍。如果删除时间可以变得不那么重要,您可以使用一个简单的数组(它有 O(1) 附加和 O(1) 找到第 k 个元素)。我会继续考虑,但我不能保证任何事情。
  • 我可以想到O(logn) insert+remove & O(logk) find k - 但我没时间详细回答。这个想法是创建一个具有 3 个修改的 AVL 树:(1) 维护 min - 指向最小元素的指针。 (2)维护每个节点的father字段。 (3) 为每个节点维护numberOfSons字段。现在,当您需要第 k 个元素时,从 min 开始,一直向上,同时使用 numberOfSons 知道您已经通过了多少元素。您最多需要上O(logk) 个节点,然后再返回O(logk) 个节点,这导致O(logk) 找到第k 个元素
  • 你到底想做什么?如果您清楚地陈述所有假设,则可能有一种数据结构可以满足您的需求。例如,您真的需要按递增顺序插入元素吗?这可能会伤害(或帮助)。什么是绝对必要的?为什么性能会成为问题?
  • 如果您的数组只有 10000 个条目,可能 O(log N) 删除要求太强了?您可以使用tiered vector,它允许非常快速的索引,但 O(sqrt N) 删除。要在这个分层向量中找到要删除的元素,您需要并行维护其他数据结构。它可能是一个搜索树,它的每个节点都增加了后代的数量。总的来说,你有 O(1) 几乎即时索引、O(log N) 插入和 O(sqrt N) 删除。
  • 如果你真的想要这个数据结构,我建议发帖到cstheory.stackexchange.com,因为这个问题要求一个相当前沿的数据结构。当前现有的动态数组可能支持这一点,但仅适用于数组末尾的插入和删除。

标签: algorithm data-structures containers


【解决方案1】:

我不知道这样的数据容器是否可以实现所请求的时间复杂度。但这里有几种方法,几乎​​可以实现这些复杂性。

第一个是 tiered vector,插入和索引为 O(1),但删除为 O(sqrt N)。由于您预计此容器中只有大约 10000 个元素并且 sqrt(10000)/log(10000) = 7,因此您在这里几乎可以获得所需的性能。分层向量被实现为一个环形缓冲区数组,因此删除一个元素需要移动所有元素,在环形缓冲区中跟随它,并将一个元素从后面的每个环形缓冲区移动到它之前的一个;在此容器中进行索引意味着在环形缓冲区数组中进行索引,然后在环形缓冲区内进行索引。

可以创建一个不同的容器,与分层向量非常相似,具有完全相同的复杂性,但工作速度更快一些,因为它对缓存更友好。分配一个 N 元素数组来存储值。并分配一个 sqrt(N) 元素数组来存储索引更正(用零初始化)。我将在 100 元素容器的示例中展示它是如何工作的。要删除索引为 56 的元素,请将元素 57..60 移动到位置 56..59,然后在索引更正数组中将 1 添加到元素 6..9。要查找第 84 个元素,请在索引校正数组中查找第 8 个元素(其值为 1),然后将其值添加到索引 (84+1=85),然后从主数组中取出第 85 个元素。删除主数组中大约一半的元素后,需要压缩整个容器以实现连续存储。这仅获得 O(1) 累积复杂度。对于实时应用程序,此操作可以分几个较小的步骤执行。

这种方法可以扩展到深度为 M 的Trie,索引需要 O(M) 时间,删除需要 O(M*N1/M) 时间和 O(1)插入的时间。只需分配一个 N 元素数组来存储值,N(M-1)/M, N(M-2)/M, ..., N1/M-元素数组来存储索引更正。要删除元素 2345,移动主数组中的 4 个元素,在最大的“更正”数组中增加 5 个元素,在下一个中增加 6 个元素,在最后一个中增加 7 个元素。要从此容器中获取元素 5678,请将元素 5、56、567 中的所有更正添加到 5678,并使用结果来索引主数组。为“M”选择不同的值,可以平衡索引和删除操作之间的复杂性。例如,对于 N=65000,您可以选择 M=4;所以索引只需要 4 次内存访问和删除更新 4*16=64 个内存位置。

【讨论】:

  • 插入在分层向量中为 O(sqrt N),在您的变体中为 O(N),因为您使用单个数组。 O(1) 从何而来?编辑:我猜你指的是简单的推送。
【解决方案2】:

我想首先指出,如果 k 真的是一个随机数,那么可能值得考虑的是问题可能完全不同:求第 k 个最小的元素,其中 k在可用元素的范围内均匀随机基本上是......随机选择一个元素。而且它可以做很多不同的事情。

这里我假设您实际上需要选择一些特定的,如果是任意的,k


鉴于您的元素按顺序插入的 strong 前提条件,有一个简单的解决方案:

  • 由于您的元素是按顺序给出的,只需将它们一一添加到数组中即可;那就是你有一些(无限的)表T和一个光标c,最初是c := 1,当添加一个元素时,做T[c] := x 和 c := c+1
  • 当您想访问第 k 个最小元素时,只需查看 T[k]。

当然,问题在于,当您删除元素时,您会在表格中创建间隙,因此元素 T[k] 可能不是 k- th 最小的,但是 j 的 j 最小的,因为 k 之前的一些单元格是空的。

然后跟踪您已删除的元素就足够了,以了解 有多少 已删除小于 k。你如何在最多 O(log n) 的时间内做到这一点?通过使用range tree 或类似类型的数据结构。范围树是一种结构,可让您添加整数,然后查询 XY 之间的所有整数.因此,每当您删除一个项目时,只需将其添加到范围树即可;并且在查找第 k 个最小元素时,查询 0k 之间所有已删除的整数;假设 delta 已被删除,那么第 k 个元素将在 T[k+delta] 中。

有两个小问题需要修复:

  • 范围树以 O(log n) 的时间返回范围,但是要计算范围中的元素数量,您必须遍历范围中的每个元素,因此这会增加时间 O(D)其中 D 是范围内已删除项目的数量;为了摆脱这种情况,您必须修改范围树结构,以便在每个节点处跟踪子树中不同元素的数量。维持这个计数只会花费 O(log n),这不会影响整体复杂性,而且这是一个相当简单的修改。

  • 事实上,只进行一次查询是行不通的。实际上,如果您在 1 到 k 范围内获得 delta 删除元素,那么您需要确保在 k+1 到 k+delta 范围内没有删除任何元素,依此类推。完整的算法如下所示。


KthSmallest(T,k) := {
  a = 1;  b = k;  delta

  do {
    delta = deletedInRange(a, b)
    a = b + 1
    b = b + delta
  while( delta > 0 )

  return T[b]
}

此操作的确切复杂性取决于您删除的准确程度,但如果您的元素被均匀随机删除,那么迭代次数应该相当少。

【讨论】:

  • 假设deletedInRange(a,b) 在 O(log(b-a)) 中返回,我认为在最坏的情况下选择算法将采用类似于 O(n) 的方式;最坏的情况是当我想查看最小的元素时,我的数组中只有一个元素没有被删除,并且它在它的末尾。
  • 最坏情况分析不一定相关。我对您的使用模式没有清晰的了解,所以我不知道这对您的用例会有多好。如果您担心的情况更糟,那么您可以使用范围树进行二分搜索并在 O((log n)^2) 中找到第 k 个元素。但我认为平均而言,这将不如我建议的那么好。
【解决方案3】:

有一个Treelist(Java 的实现,带有源代码),对于所有三个操作(插入、删除、索引)都是 O(lg n)。

实际上,此数据结构的可接受名称似乎是“order statistic tree”。 (除了索引,它还被定义为支持 O(lg n) 中的 indexof(element)。)

顺便说一下,O(1) 并不被认为比 O(lg n) 有太多优势。这种差异往往被实践中的恒定因素所淹没。 (数据结构中是否有 1e18 项?如果我们将其设置为上限,则仅相当于 60 左右的常数因子。)

【讨论】:

    【解决方案4】:

    查看heaps。插入和删除应该是 O(log n),最小元素的窥视是 O(1)。但是,查看或检索第 K 个元素将再次花费 O(log n)。

    已编辑:正如阿米特所说,检索比偷看更昂贵

    【讨论】:

    • 它不允许访问O(1)中的任意k,也不允许删除O(logn)中的元素(不是头部)
    • 我怀疑你也可以得到 O(logn) 中的第 k 个元素。
    • 不,你不能,正如答案中所说。但是问题的标题要求最小的元素,所以答案是相关的
    • 它要求第 k 个最小的元素,但它仍然不允许在 O(logn) 中任意删除(请随意解释如何在堆中通过 O(logn) 中的键找到任意元素,如果你认为我错了,我会很高兴学习!)
    • @Miquel“第k个最小元素”不仅仅是“最小的”
    【解决方案5】:

    这可能是不可能的。 但是,您可以在平衡二叉树中进行某些更改以获得 O(log n) 中的第 k 个元素。

    在此处了解更多信息:Wikipedia.

    【讨论】:

      【解决方案6】:

      可索引的跳过列表可能能够执行(关闭)您想要的操作: http://en.wikipedia.org/wiki/Skip_lists#Indexable_skiplist

      但是,有一些注意事项:

      1. 这是一种概率数据结构。这意味着所有操作不一定都是 O(log N)
      2. 索引不会是 O(1),只需 O(log N)
      3. 根据您的 RNG 的速度以及遍历指针的速度,您可能会因此获得比仅使用数组并处理更高的移除成本更差的性能。

      很可能,与此类似的事情将是您为实现目标所能做的“最好的”。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-01-22
        • 2012-05-13
        • 2017-06-06
        相关资源
        最近更新 更多