【问题标题】:Data structure that inserts in constant time at endpoints and before/after an element?在端点和元素之前/之后以恒定时间插入的数据结构?
【发布时间】:2018-12-29 01:28:58
【问题描述】:

我正在寻找一种数据结构:

  • 具有无限大小。
  • 维护其元素的插入顺序。
  • 在列表的开头和结尾高效插入(最好是在恒定时间内)。
  • 在现有元素之前或之后高效插入(最好是在恒定时间内)。

我排除了ArrayList,因为它在列表开头插入效率不高。

表面上 LinkedList 应该是完美匹配,但实际上 Java 实现在插入现有元素之前或之后效率不高(即它遍历整个列表以找到插入位置)。

(我个人不需要存储重复元素,但其他人可能)


动机:我正在构建一个允许偶尔作弊的事件队列(在现有事件之前或之后插入)。

【问题讨论】:

  • LinkedHashMap ?
  • @ACV 如何在现有元素之前或之后插入?
  • 取决于在给定要插入的元素之前/之后如何检测要插入的“现有元素”。
  • 是否允许重复?
  • @user7 是的,正如 khachik 所说,您需要先定义一个好的密钥才能使其正常工作

标签: java performance list


【解决方案1】:

老实说,我认为LinkedList 的自定义实现将是可行的方法:

  • 具有无限大小。
    • 是的。
  • 维护其元素的插入顺序。
    • 是的。
  • 在列表的开头和结尾处有效插入(最好是在恒定时间内)。
    • 是的,而且 O(1)。
  • 在现有元素之前或之后有效插入(最好是在恒定时间内)。
    • 如果您在插入/删除元素时保持Map<?, Node>,那么您可以在恒定时间内访问Node(及其上一个/下一个节点)并以这种方式插入/删除。

根据事件的总量(以及事件作弊的频率),也可以考虑线性时间,允许您使用 API 的 LinkedList 实现。

【讨论】:

  • 问题的症结在于Java API没有暴露链表的节点。这可以防止人们说“在此节点之前/之后插入”这将是有效的。 LinkedList.listIterator() 实际上确实允许在之前/之后进行有效插入,但迭代器被 addFirst()addLast() 无效。
  • 即使 API 确实公开了节点,您也无法随机访问它们。
  • 您可以为此使用 HashMap。它仍然是恒定的时间。
  • @JacobG.我没有要求随机访问,实际上我并不需要它。我只需要能够在恒定时间内阅读列表的前面。
  • @thatotherguy 我还没有看到一个面向地图的答案,它保证插入顺序并允许在两个现有节点之间插入无限数量的节点。
【解决方案2】:

AVL 树怎么样?由于树总是平衡的,它的高度为 O(log(N)),其中 N 是树中的节点数。这将是最坏的情况 插入也是 O(log(N))。

我相信,对于您要寻找的东西,这几乎是恒定的时间。抱歉,如果这不是答案,我还没有足够高的声誉来发表评论:/

【讨论】:

  • O(log(N)) 不是固定时间
  • @user7 OP 说“理想情况下在恒定时间内”
  • 这就是为什么我说“尽可能接近恒定时间来获得所需的内容”。如果可以的话,我会把它作为评论留下:/
  • 无论如何,我更喜欢 cmets 的答案。是否有针对 Java 的已建立的 AVL 树实现?
  • 这也不能维护元素的插入顺序
【解决方案3】:

我正在构建一个允许偶尔作弊的事件队列(在现有事件之前或之后插入)。

如果作弊很少见,请选择您自己的基于数组的结构。 java.util.ArrayDeque 在 O(1) 中处理两端,但不能在中间进行插入。它们可以很容易地添加,但复杂度是 O(n)。

请注意,LinkedList 对于大多数操作(由于其糟糕的数据局部性)比ArrayListArrayDeque 慢得多。


假设,您非常需要在中间插入恒定时间。类似的东西

insertAfter(existingElement, newElement);

java.util.LinkedList的问题是它没有这个操作。它只有ListIterator,它允许您在中间插入,但这只有在您首先定位迭代器时才有帮助(O(n))。

所以你需要你自己的链表。基本的实现相当简单,你还需要直接访问它的节点(包含nextprev 链接的东西,而不是你add 的东西)。然后维护一个Map<MyThing, MyListNode>,它可以让您找到existingElement 的节点。现在您只需为 newElement 创建一个新节点,正确链接它(这基本上是微不足道的,但有一些极端情况)并更新您的地图。


要消除边界情况(第一个/最后一个/唯一节点),请将两个虚拟节点(prefirstpostlast)添加到您的链接列表中。显然,这些是唯一不会存储在您的地图中的节点。它们使实现变得非常简单,例如,

void insertAfter(MyThing existingElement, MyThing newElement) {
   Node found = map.get(existingElement);
   if (found == null) throw ...
   Node newNode = new Node(newElement);
   map.put(newElement, newNode);

   // Link newNode where it belongs to.
   newNode.next = found.next;
   newNode.prev = found;

   // Fix links.
   newNode.prev.next = newNode;
   newNode.next.prev = newNode;   
}

void insertFirst(MyThing newElement) {
   Node found = prefirst;

   // All the remaining code is simply copied from above.
   // Factor it out.

   Node newNode = new Node(newElement);
   map.put(newElement, newNode);

   // Link newNode where it belongs to.
   newNode.next = found.next;
   newNode.prev = found;

   // Fix links.
   newNode.prev.next = newNode;
   newNode.next.prev = newNode;   
}

【讨论】:

  • 你是对的。我已经更新了这个问题。我认为您的答案中有错字。你写的Note that LinkedList is for most operations[...]LinkedList 是什么?另外,您写道 DequeO(n) 用于在中间插入,但据我所知,它不允许在中间插入。
  • @Gili 已修复 LinkedList。你说得对Deque,我的意思是ArrayDeque,并假设它也实现了List,但它没有。所以我建议MyOwnArrayDeque ;)。
  • @maaartinus 题外话,但计划让ArrayDeque 实现List 中的所有方法,但不实现接口本身:)
  • @JacobG。有趣的。你知道为什么吗?我的意思是……这些方法肯定有用,但缺少大量有用的方法(例如,ArrayList.removeLast),几乎没有添加任何东西。
  • @maaartinus 我尝试向 OpenJDK 提出大量方法,但出于某种原因,他们非常犹豫是否要添加方便的方法。
【解决方案4】:

使用LinkedHashMap

  • 保留广告顺序
  • 速度很快(因为基本上在O(1)中使用key来查找元素)。
  • 允许随时插入元素(但为此,您需要考虑一个好的键 - 设计一个对您的排序逻辑有意义的 hashCode()equals())。

Map接口的哈希表和链表实现,带有 可预测的迭代顺序。此实现不同于 HashMap 因为它维护一个双向链表,贯穿其所有 条目。这个链表定义了迭代顺序,即 通常是键插入地图的顺序 (插入顺序)。

我会反对自定义集合,因为您将花费大量时间重新发明轮子,然后您最终会得出结论,最好使用LinkedHashMap

请注意,此集合不是线程安全的。

【讨论】:

  • 正如在其他地方评论的那样,我看不出你如何同时控制 X 键在 Y 键之前向上,并保持在 X 和 Y 之间插入无限元素的能力。你只有这么多可以用这种技术。它可能适用于知道需要在 X 和 Y 之间插入的最大元素数的人。
【解决方案5】:

对于需要在任意位置进行最优插入的操作,我一般使用 c++11 的forward_list。它基于单链表。见forward_list

前向列表是序列容器,允许在序列中的任何位置进行恒定时间插入和擦除操作。参见forward_list 的第一句

如果您关心优化,为什么不切换到 c++。我不确定java中是否存在这样的功能。请注意,虽然 forward_list 提供恒定时间插入,但您不能直接按位置访问元素。

当您说在某个位置插入时,通常对于链表。 有两种情况:

  1. 您在该位置引用了节点。在这种情况下,时间复杂度是 O(1)

  2. 您只有索引。时间复杂度为 O(n)

【讨论】:

  • 同意。 LinkedList 将是我的问题的理想解决方案,除了 Java 不公开对节点的引用。因此,我们陷入了 O(n) 的复杂性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-12
  • 1970-01-01
  • 2013-03-29
  • 2011-11-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多