【问题标题】:Does ArrayDeque have overhead of shifting elements on remove/add?ArrayDeque 在删除/添加时是否有移动元素的开销?
【发布时间】:2021-02-27 01:21:01
【问题描述】:

我遇到了this 问题,第一个(已接受)答案说这部分:

ArrayDeque 没有节点分配的开销 LinkedList 没有向左移动数组内容的开销 删除 ArrayList 所具有的。

我同意节点开销,但不同意关于移动元素的部分。我知道 StackOverflow 也可能有错误的信息,但是这个答案有很多票,所以一定是我的无知。谁能告诉我:

为什么ArrayDeque 没有移动元素的开销? ArrayDeque(正如其名称所述)仍然使用 ARRAY。这意味着它与任何其他 array 的工作方式相同。如果我有 10 个元素并且我删除了 head,那么这 9 个元素必须向左移动 1 个位置。这是LinkedList 没有的开销——它只是更改了对prevnext 的引用。我说的对吗?

总而言之,ArrayListArrayDeque 的工作方式不一样吗?如果结构发生变化,它们都会转移元素。唯一的区别是ArrayList 可以访问任何任意位置,而ArrayDeque 作为FIFO/LIFO 工作。如果我错了,有人可以纠正我吗?我不想在这里学错。

【问题讨论】:

  • 我会说,你是对的。但他们只查看了结构头部的队列操作,无论是在帖子中还是在基准测试中。刚刚看了一下,ArrayDequeue 也有例如delete(i) 进行所有轮班工作,其方式与 ArrayList (System.arrayCopy) 相同。
  • 仅仅看一个队列一侧的操作,怎么会有人发表这样的评论呢?我只是不明白这个答案在半真半假时得到了多少票。还是真的。
  • 好吧,那是 OP 的问题,很公平。但尤其是这种链表/数组列表的讨论会一直持续下去,你可以找到长达数小时的讨论,将它们的细节剖析到对硬件级别的影响等等。
  • 在删除第一个元素时ArrayDequeue 不必移动其余元素,因为ArrayDequeue 有一个字段用于第一个元素的位置 (head)。删除第一个元素基本上转换为head++(如果之后head 的值与内部数组的长度相同,则需要一些额外的逻辑)

标签: java arrays arraylist queue arraydeque


【解决方案1】:

很高兴有这样的直觉,即从数组的前面删除将需要将所有内容移回。但是,在 ArrayDeque 的特定情况下,实现的设计方式不需要这样做。

基于数组的双端队列通常使用称为循环缓冲区的数据结构来实现。这个想法是我们维护一个元素数组,但假设数组的末端粘在一起形成一个环。

ArrayDeque 内部维护了一个包含 16 个元素的数组,我们可以这样查看:

+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+

我们维护两个不同的指针,一个头指针和一个尾指针,跟踪双端队列第一个元素的位置和最后一个元素的位置双端队列的元素。最初,它们将指向数组的开头:

 head
  |
  v
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
  ^
  |
 tail

每当我们执行addFirst 时,我们都会将头指针备份一步,然后将元素写入我们找到的位置。由于我们假设数组的两端是连在一起的,所以这里倒退一步会将头指针移动到最后一个位置:

                                                             head
                                                              |
                                                              v
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |   |   |   |   |   |   |   |   | X |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
  ^
  |
 tail

要做一个addLast,我们写到尾部位置,然后向前推进:

                                                             head
                                                              |
                                                              v
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| X |   |   |   |   |   |   |   |   |   |   |   |   |   |   | X |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
      ^
      |
     tail

如果我们再做两个addFirsts,它会是什么样子:

                                                     head
                                                      |
                                                      v
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| X |   |   |   |   |   |   |   |   |   |   |   |   | X | X | X |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
      ^
      |
     tail

如果我们再做两个addLasts,这就是它的样子:

                                                     head
                                                      |
                                                      v
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| X | X | X |   |   |   |   |   |   |   |   |   |   | X | X | X |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
              ^
              |
             tail

我们从头指针开始读取双端队列的元素,然后继续向前直到到达尾指针。所以在这种情况下,我们从head指向的槽开始读取,而不是数组中的第一个位置。

以这种方式安排事情使得删除双端队列的第一个元素非常快(O(1))。我们只需清除head指向的入口,然后将head向前移动一步,如下所示:

                                                         head
                                                          |
                                                          v
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| X | X | X |   |   |   |   |   |   |   |   |   |   |   | X | X |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
              ^
              |
             tail

请注意,没有什么需要移动,因为我们只是假装我们从数组中较晚的位置开始。

但是,如果您要从 ArrayDeque 的中间删除,那么,就像从大多数其他基于数组的结构的中间删除一样,是的,您必须移动元素。但是,再一次,ArrayDeque 并未针对该用例进行优化;顾名思义,它专门设计为双端队列。

【讨论】:

    猜你喜欢
    • 2021-02-27
    • 2013-07-10
    • 1970-01-01
    • 2019-12-29
    • 2016-01-28
    • 2013-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多