Search code examples
javaarraysarraylistqueuearraydeque

Does ArrayDeque have overhead of shifting elements on remove/add?


I came across this question, where 1st (accepted) answer says this part:

ArrayDeque doesn't have the overhead of node allocations that LinkedList does nor the overhead of shifting the array contents left on remove that ArrayList has.

I agree with node overhead, but not with the part about shifting elements. I know StackOverflow can also have wrong information, but this answer has many votes, so it must be my ignorance. So can someone tell me this:

Howcome ArrayDeque doesn't have overhead of shifting elements? ArrayDeque (as its name states) still uses an ARRAY. Meaning it works same as any other array. If I had 10 elements and I remove head, those 9 will have to be shifted 1 place to the left. And that is overhead that LinkedList doesn't have - it just changes reference to prev and next. Am I correct?

To sum, don't ArrayList and ArrayDeque work the same way? They both shift elements if structure gets changed. The only difference is that ArrayList can access any arbitrary location, while ArrayDeque works as FIFO/LIFO. Can someone please correct me if I am wrong? I don't want to learn something wrong here.


Solution

  • It's great to have the intuition that removing from the front of an array will require everything to be shifted back. However, in the specific case of the ArrayDeque, the implementation is designed in a way that doesn't require this.

    An array-based deque is typically implemented using a data structure called a circular buffer. The idea is that we maintain an array of elements, but pretend that the ends of the array are glued together to form a ring.

    The ArrayDeque internally maintains an array of 16 elements, which we can view like this:

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

    We maintain two different pointers, a head pointer and a tail pointer, keeping track of the position of the first element of the deque and the position one past the last element of the deque. Initially, they'll point to the start of the array:

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

    Whenever we do an addFirst, we back up the head pointer by one step, then write the element to the location we find. Since we're pretending that the two ends of the array are linked together, backing up one step here moves the head pointer to the last position:

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

    To do an addLast, we write to the tail position, then advance it forward:

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

    Here's what it would look like if we did two more addFirsts:

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

    And here's what it would look like if we did two more addLasts:

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

    We read the elements of the deque by starting at the head pointer and proceeding forward until we reach the tail pointer. So in this case, we start reading from the slot pointed at by head, not the first position in the array.

    Arranging things this way makes it very fast (O(1)) to remove the first element of the deque. We just clear out the entry pointed at by head and move head forward a step, like this:

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

    Notice that nothing needs to get shifted around, since we're just pretending that we start at a later position in the array.

    However, were you to remove from the middle of the ArrayDeque, then, like removing from the middle of most other array-based structures, yes, you'd have to shift elements around. But, then again, ArrayDeque isn't optimized for that use case; it's specifically designed to be a double-ended queue, as the name suggests.