Операции ArrayDeque

#java #data-structures #deque #circular-buffer #arraydeque

#java #структуры данных #deque #циклический буфер #arraydeque

Вопрос:

У меня есть немного свободного времени, и я пытаюсь понять, как ArrayDeque работает внутри. Я прочитал пару статей и вопросов / ответов здесь, и я думаю, что я довольно близок. Я использовал отладку, чтобы следить за рабочим процессом, и есть кое-что, что меня беспокоит. Я создал пустой deque, в результате чего получился массив с 16 элементами, которые являются нулевыми. Когда я использовал addFirst, он добавил элемент в позицию 16 в массиве, а addLast был добавлен в начале в позицию 0. Чего мне не хватает, не могли бы вы поделиться некоторыми знаниями или указать мне правильное направление, чтобы я мог полностью понять, что происходит за кулисами. Заранее спасибо!

Ответ №1:

Deque на основе массива обычно реализуется с использованием структуры данных, называемой циклическим буфером. Идея заключается в том, что мы поддерживаем массив элементов, но делаем вид, что концы массива склеены вместе, образуя кольцо.

Из вашей отладки кажется ArrayDeque , что внутри поддерживается массив из 16 элементов, который мы можем просмотреть следующим образом:

  --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
|   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |   |
 --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- 
  

Мы поддерживаем два разных указателя, указатель head и указатель tail, отслеживая положение первого элемента deque и положение после последнего элемента deque. Первоначально они будут указывать на начало массива:

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

Всякий раз, когда мы выполняем an addFirst , мы создаем резервную копию головного указателя на один шаг, а затем записываем элемент в найденное местоположение. Поскольку мы притворяемся, что два конца массива связаны друг с другом, резервное копирование на один шаг здесь перемещает указатель заголовка в последнюю позицию:

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

Чтобы выполнить addLast , мы записываем в конечную позицию, затем продвигаем ее вперед:

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

Вот как это выглядело бы, если бы мы сделали еще два addFirst s:

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

И вот как это выглядело бы, если бы мы сделали еще два addLast s:

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

Мы считываем элементы deque, начиная с указателя head и продвигаясь вперед, пока не дойдем до указателя tail. Итак, в этом случае мы начинаем чтение из слота, на который указывает by head , а не с первой позиции в массиве.

В конце концов, два указателя встретятся посередине. Когда это происходит, мы создаем совершенно новый массив, который больше исходного (обычно на 150% больше), затем копируем элементы в новый массив, чтобы освободить немного места.

Комментарии:

1. как вам удалось ввести этот ответ за 2 минуты? Нет 5 минут.

2. Это было очень полезно, и я не могу вас отблагодарить. Это немного сбивает с толку, поскольку похоже, что обе операции работают в обратном порядке по отношению к их именам, потому что addFirst добавляет в начало, которое находится в конце (по индексу) массива, а addLast добавляет в хвост, который находится в начале массива. Еще раз спасибо за полный ответ!

3. Я отредактировал свой ответ, чтобы прояснить одну важную деталь — при чтении из массива мы начинаем не с позиции 0, как обычно, а с позиции головного указателя. Таким образом, даже если мы создаем резервную копию и записываем в конце массива, мы притворяемся, что массив начинается с позиции head .

4. Да, я вроде как понял это. Еще раз большое вам спасибо!