Статьи

Реализация очереди с использованием двух стеков

В углу кода мы берем реальную задачу программирования интервью и решаем ее в коде. Хотя эти вопросы могут быть заданы как упражнение для доски, вы, как разработчик, узнаете гораздо больше, фактически написав код решения и научившись использовать API, коллекции и алгоритмы в обсуждении.

Вы можете скачать это упражнение с CJIQ Github по адресу https://github.com/corejavainterviewquestions/queuefromtwostacks . Каждый этап является коммитом; Откат к первоначальной проверке и код на ветке. Затем вы можете сравнить свой ответ с моим. Результаты разработаны с использованием TDD.

Создание очереди из двух стеков — один из тех вопросов, который, кажется, задерживался годами и до сих пор задается. Это полезно знать на случай, если его спросят, но, что более важно, это хорошая возможность больше узнать о стеках, очередях и расчете Big O.

Задача проста. Реплицируйте функциональность очереди, используя два стека.

Очередь — это структура данных «первым пришел — первым обслужен» (FIFO). Если вы добавите элементы A, B и C в таком порядке, а затем трижды выскочит, вы получите A, B и C. И наоборот, стек — это структура «последний пришел первым — вышел» (LIFO); В этом же примере вы получите C, B, а затем A.

Вы можете прочитать больше об этих структурах данных в  предыдущем посте об интервью и структурах данных.

В очереди есть 2 метода, которые нас интересуют: добавить и удалить. Стек имеет толчок и поп.
Давайте начнем с нашего первого провального теста.

 @Test
 public void addingOneItemAndRemovingFromQueueWillReturnThatItem() throws Exception {
     Queue queue = new Queue();
     queue.add(“Item 1”);
 
     assertThat(queue.remove(), is(“Item 1”));
 }

Чтобы написать этот тест, я создал метод add — я решил использовать Strings. Мы могли бы очень легко это исправить, но давайте будем простыми. Мы добавляем метод удаления, который мы ожидаем вернуть объект, который мы добавили.
Реализовать очень просто; функциональность была бы одинаковой для стека, поэтому мы фактически просто делегируем.

 Stack stackOne = new Stack();
 public void add(String item) {
     stackOne.push(item);
 }
 public String remove() {
     return stackOne.pop();
 }

Первый тестовый проход! Почувствуй серотонин.

совершить

Хорошо, настало время для более реалистичного теста, добавив несколько элементов.

 @Test
 public void
 addingMultipleItemsThenRemovingOneWillReturnFirstItem() throws Exception {
     Queue queue = new Queue();
     queue.add(“Item 1”);
     queue.add(“Item 2”);
     queue.add(“Item 3”);
     assertThat(queue.remove(), is(“Item 1”));
 }

«Это дает хороший провал теста; Мы возвращаем пункт 3 неправильно. Теперь нам нужно подумать о реализации.
Представьте наш стек как ведро.

стеки
Нам нужно добраться до дна ведра, чтобы убрать предмет, который был первым. Единственный способ сделать это — вытолкнуть все предметы на пути.
Этот тест очень прост, поэтому мы не заботимся о других предметах. Давайте просто раскроем все как есть. Это грубо, но в TDD мы стремимся сделать минимальную работу, чтобы пройти тест.

 public String remove() {
     String result = null;
     while(!stackOne.isEmpty())
         result = stackOne.pop();
     return result;
 }

Довольно простой. Давайте напишем более сложный тест.

совершить

Я переименовал последний тест и расширил его, добавив дополнительный всплывающий текст.

@Test
 public void
 addingThreeItemsThenRemovingTwoWillReturnItemOneThenTwo() throws Exception {
     Queue queue = new Queue();
     queue.add(“Item 1”);
     queue.add(“Item 2”);
     queue.add(“Item 3”);
     assertThat(queue.remove(), is(“Item 1”));
     assertThat(queue.remove(), is(“Item 2”));
 }

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

стеки (1)

Затем мы должны вернуть все элементы обратно в первый стек, чтобы мы могли добавить больше элементов в стек.

public String remove() {
     while(!stackOne.isEmpty()) {
         stackTwo.push(stackOne.pop());
     }
     String result = stackTwo.pop();
     while(!stackTwo.isEmpty()) {
         stackOne.push(stackTwo.pop());
     }
     return result;
 }

Это делает испытание успешным и является точным, хотя и грубым решением проблемы.

совершить

В приведенном выше коде есть некоторое дублирование. TDD Мантра Красный, Зеленый, Рефакторинг. Итак, давайте немного очистим код.

 public String remove() {
     swapStacks(stackOne, stackTwo);
     String result = stackTwo.pop();
     swapStacks(stackTwo, stackOne);
     return result;
 }
 private void swapStacks(Stack stackOne, Stack stackTwo) {
     while(!stackOne.isEmpty())
         stackTwo.push(stackOne.pop());
 }

Это не только более чистый код, но и гораздо проще понять, что происходит.

совершить

Как насчет пропущенных тестов? В этих сценариях важно учитывать крайние случаи. Как насчет пустого дела? Особенно в интервью важно, чтобы вы решили, как правильно себя вести. Должна ли очередь вернуть ноль или выдать исключение? Либо можно утверждать вполне разумно. Я выбрал исключение, так как передача значения null может привести к хрупкому и подверженному ошибкам коду.

@Test(expected = NoSuchElementException.class)
 public void throwsErrorIfRemoveCalledOnEmptyQueue(){
     Queue queue = new Queue();
     queue.remove();
 }

Этот тест не пройден; мы получаем  EmptyStackException. Это должно быть легко исправить.

public String remove() {
     if(stackOne.isEmpty())
         throw new NoSuchElementException(“The Queue Is Empty”);
     swapStacks(stackOne, stackTwo);
     String result = stackTwo.pop();
     swapStacks(stackTwo, stackOne);
     return result;
 }

Легко!

совершить

Просто для безопасности, давайте добавим более сложный тест, из нескольких добавлений и удалений.

 @Test
 public void multipleAddsAndRemovesInCorrectOrder() throws Exception {
     Queue queue = new Queue();
     queue.add(“Item 1”);
     queue.add(“Item 2”);
     assertThat(queue.remove(), is(“Item 1”));
     queue.add(“Item 3”);
     assertThat(queue.remove(), is(“Item 2”));
     queue.add(“Item 4”);
     queue.add(“Item 5”);
     queue.add(“Item 6”);
     assertThat(queue.remove(), is(“Item 3”));
     assertThat(queue.remove(), is(“Item 4”));
 }

Как и ожидалось, этот тест проходит. Теперь у нас есть полное решение очереди.
Однако это очень медленное решение. Давайте проанализируем Big O. Во-первых, мы должны просмотреть каждый элемент в стеке и вытолкнуть его; это п. Затем мы должны подтолкнуть каждый
элемент, который снова n. Затем мы выталкиваем все элементы, еще одну полную итерацию n. Затем мы добавляем все столбцы на один элемент назад, n – 1. Это O (4n). Довольно бедно! Можем ли мы найти лучшее решение?
Давайте рассмотрим первый раз, когда мы выполняем операцию удаления. Мы помещаем каждый элемент и помещаем его во второй стек. На этом этапе вторичный стек находится в правильном порядке очереди. Если мы не вернемся к первому стеку, мы можем просто выскочить из вторичной очереди, пока она не станет пустой. Если происходит запрос на удаление и стек 2 пуст, тогда мы просто меняем стеки.

public String remove() {
     if(stackOne.isEmpty() && stackTwo.isEmpty())
         throw new NoSuchElementException(“The Queue Is Empty”);
     if(stackTwo.isEmpty())
     while(!stackOne.isEmpty())
         stackTwo.push(stackOne.pop());
     return stackTwo.pop();
 }

Каждый поп из стека два. Мы проверяем, есть ли элементы (теперь мы должны проверить оба стека). Если они есть, мы проверяем, нужно ли нам делать обмен стека (т. Е. Второй стек пуст). Затем мы можем извлечь из верхней части стека 2.
Поскольку у нас теперь есть набор тестов, который мы можем запустить, чтобы проверить решение, мы знаем, что ничего не сломали.
Но какова сложность этого алгоритма? Когда второй стек становится пустым, мы должны сделать полный щелчок из первого стека и перенести на второй; который является O (2n), упрощенным до O (n). Однако это не должно происходить часто (зависит от использования стека). Большинство вызовов, которые нужно удалить, будут просто выполнять stackTwo.pop (), что является операцией с постоянным временем.
Поэтому мы можем сказать, что, хотя в худшем случае это O (n), оно будет давать постоянное амортизированное время, например, если вы выполните миллион операций через очередь, большая часть из них будет иметь постоянное время.