Який сенс реалізації стека за допомогою двох черг?


34

У мене є таке домашнє завдання:

Реалізуйте методи стека push (x) та pop (), використовуючи дві черги.

Мені це здається дивним, оскільки:

  • Стек - це (LIFO) чергу
  • Я не бачу, для чого вам знадобиться дві черги, щоб здійснити це

Я шукав:

і знайшов пару рішень. Ось що я закінчив:

public class Stack<T> {
    LinkedList<T> q1 = new LinkedList<T>();
    LinkedList<T> q2 = new LinkedList<T>();

    public void push(T t) {
        q1.addFirst(t);
    }

    public T pop() {
        if (q1.isEmpty()) {
            throw new RuntimeException(
                "Can't pop from an empty stack!");
        }

        while(q1.size() > 1) {
            q2.addFirst( q1.removeLast() );
        }

        T popped = q1.pop();

        LinkedList<T> tempQ = q1;
        q1 = q2;
        q2 = tempQ;

        return popped;
    }
}

Але я не розумію, у чому перевага над використанням однієї черги; версія двох черг здається безглуздо складною.

Скажімо, ми обираємо, щоб натискання були більш ефективними з 2-х (як я це робив вище), pushзалишалися б однаковими і popпросто вимагали повторення останнього елемента та повернення його. В обох випадках pushбуло б O(1)і popбуло б O(n); але версія єдиної черги була б кардинально простішою. Він повинен вимагати лише одинарного циклу.

Я щось пропускаю? Будь-яке розуміння тут буде вдячне.


17
Черга зазвичай посилається на структуру FIFO, тоді як стек є структурою LIFO. Інтерфейс для LinkedList на Java - це деке (подвійне чергу), що дозволяє отримувати доступ як до FIFO, так і до LIFO. Спробуйте змінити програмування на інтерфейс черги, а не на LinkedList.

12
Більш звичною проблемою є реалізація черги за допомогою двох стеків. Книга Кріса Окасакі про суто функціональні структури даних може бути цікавою.
Ерік Ліпперт

2
Виконуючи те, що сказав Ерік, іноді ви можете опинитися на мові, що базується на стеках (наприклад, на постійному струмі або в автоматичному режимі, що працює з двома стеками (що еквівалентно машині Turing, тому що ви можете зробити більше)), де ви можете опинитися з кілька стеків, але черги немає.

1
@MichaelT: Або ви також можете опинитися на базі процесора на основі стека
slebetman

11
"Стек - це (LIFO) черга" ... uhm, черга - це лінія очікування. Як і лінія для використання громадського туалету. Лінії, яких ви чекаєте, коли-небудь поводяться LIFO? Перестаньте використовувати термін "черга LIFO", це безглуздо.
Мехрдад

Відповіді:


44

Немає переваги: ​​це суто академічна вправа.

Дуже давно , коли я був на першому курсі в коледжі , у мене була подібна вправа 1 . Мета полягала в тому, щоб навчити студентів використовувати об’єктно-орієнтоване програмування для реалізації алгоритмів замість того, щоб писати ітеративні рішення, використовуючи forпетлі з циклічними лічильниками. Натомість комбінуйте та використовуйте існуючі структури даних для досягнення своїх цілей.

Ви ніколи не будете використовувати цей код в World Real TM . Те, що вам потрібно відмовитися від цієї вправи, - це як "мислити поза коробкою" та повторно використовувати код.


Зверніть увагу, що ви повинні використовувати інтерфейс java.util.Queue у своєму коді, а не безпосередньо використовувати реалізацію:

Queue<T> q1 = new LinkedList<T>();
Queue<T> q2 = new LinkedList<T>();

Це дозволяє використовувати інші Queueреалізації за бажанням, а також приховувати 2 способи, LinkedListякі можуть охопити дух Queueінтерфейсу. Це включає в себе get(int)і pop()(під час компіляції вашого коду, там є логічна помилка, враховуючи обмеження вашого призначення. Декларування змінних Queueзамість того, LinkedListщоб розкрити це). Читання, пов’язане з цим: розуміння "програмування на інтерфейс" та чому корисні інтерфейси?

1 Я все ще пам’ятаю: вправа полягала в тому, щоб повернути стек, використовуючи лише методи інтерфейсу Stack і ніякі утилітні методи в java.util.Collectionsінших класах утиліти або тільки «статичні». Правильне рішення передбачає використання інших структур даних у якості тимчасових об'єктів, що містять: ви повинні знати різні структури даних, їх властивості та способи їх поєднання для цього. Наткнувся на більшість мого класу CS101, які ніколи раніше не програмували.

2 Методи все ще існують, але ви не можете отримати доступ до них без типів або роздумів. Тому не просто використовувати ці методи без черги.


1
Спасибі. Я думаю, це має сенс. Я також зрозумів, що я використовую "незаконні" операції у наведеному вище коді (натискаючи на фронт FIFO), але не думаю, що це щось змінює. Я перевернув усі операції, і вона все ще працює за призначенням. Я збираюся трохи зачекати, перш ніж прийму, оскільки не хочу перешкоджати іншим людям давати внесок. Дякую, хоча.
Carcigenicate

19

Переваги немає. Ви правильно зрозуміли, що використання черг для реалізації стека призводить до жахливої ​​часової складності. Жоден (компетентний) програміст ніколи не зробить такого подібного в реальному житті.

Але це можливо. Ви можете використовувати одну абстракцію для реалізації іншої, і навпаки. Стек може бути реалізований у двох чергах, і ви також можете реалізувати Чергу з точки зору двох стеків. Перевагою цієї вправи є:

  • ви резюмуєте стеки
  • ви резюмуєте черги
  • ви звикаєте до алгоритмічного мислення
  • ви вивчаєте алгоритми, пов'язані зі стеком
  • ви думаєте про компроміси в алгоритмах
  • усвідомлюючи еквівалентність Черг та стеків, ви підключаєте різні теми вашого курсу
  • Ви отримуєте практичний досвід програмування

Власне, це чудова вправа. Я повинен це зробити сам зараз :)


3
@JohnKugelman Дякую за вашу редакцію, але я справді мав на увазі "жахливу складність у часі". Для пов'язаного списку на основі стека push, peekі popоперації в O (1). Те саме для стека на основі масиву, що змінюється, за винятком pushамортизованого O (1), в гіршому випадку O (n). У порівнянні з цим, стек на основі черги значно поступається O (n) натисканням, O (1) поп-пік, або альтернативно O (1) push, O (n) pop і peek.
амон

1
"жахлива часова складність" і "значно неповноцінна" не зовсім правильно. Амортизована складність все ще O (1) для push і pop. У TAOCP (vol1?) Є цікаве запитання з цього приводу (в основному ви повинні показати, що кількість разів елемент може переходити з однієї стеки в іншу постійну). Найгірша ефективність для однієї операції відрізняється, але тоді я рідко чую, щоб хтось говорив про продуктивність O (N) для push в ArrayLists - не зазвичай цікаве число.
Ву

5

Однозначно є реальна мета створення черги з двох стеків. Якщо ви використовуєте незмінні структури даних з функціональної мови, ви можете натиснути на групу елементів, що висуваються, і витягнути зі списку спливаючих елементів. Елементи, що спливаються, створюються, коли всі елементи були вискочені, а новий спливаючий стек - це зворотний бік висувного стека, де новий стек, що виштовхується, тепер порожній. Це ефективно.

Що стосується стека з двох черг? Це може мати сенс у контексті, коли у вас є купа великих та швидких черг. Це, безумовно, марно, як цей вид вправ на Java. Але це може мати сенс, якщо це канали чи черги повідомлень. (тобто: N повідомлень увімкнено, операцією O (1) для переміщення (N-1) елементів спереду в нову чергу.)


Хм .. це змусило мене задуматися про використання регістрів зрушень як основи обчислень та про архітектуру пояса / млина
slebetman

Ух, Mill CPU справді цікавий. "Машина черги" точно.
Роб

2

Здійснення є невиправдано ухитрився з практичної точки зору. Сенс у тому, щоб змусити вас інтелектуально використовувати інтерфейс черги, щоб реалізувати стек. Наприклад, ваше рішення "Одна черга" вимагає, щоб ви перейшли до черги, щоб отримати останнє вхідне значення для операції стека "pop". Однак структура даних черги не дозволяє повторювати значення, ви маєте доступ до них у першому-першому-вихідному (FIFO).


2

Як уже зазначали інші: переваги в реальному світі немає.

У будь-якому випадку, одна відповідь на другу частину вашого запитання, чому б просто не використовувати одну чергу, виходить за межі Java.

У Java навіть Queueінтерфейс має size()метод, і всі стандартні реалізації цього методу є O (1).

Це не обов'язково справедливо для наївного / канонічного пов'язаного списку, як це застосував програміст C / C ++, який би просто утримував покажчики на перший і останній елемент, а кожен елемент - на вказівник на наступний елемент.

У цьому випадку size()є O (n), і його слід уникати в петлях. Або реалізація непрозора і забезпечує лише мінімум add()і remove().

З такою реалізацією вам доведеться спочатку підрахувати кількість елементів, перенісши їх у другу чергу, перенести n-1елементи назад у першу чергу та повернути залишився елемент.

Однак це, мабуть, не складе щось подібне, якщо ви живете на Java-землі.


Хороший момент щодо size()методу. Однак, за відсутності методу O (1) size(), стек тривіальний, щоб стежити за його поточним розміром. Ніщо не зупинить реалізацію однієї черги.
cmaster

Все залежить від реалізації. Якщо у вас є черга, реалізована лише з вказівниками вперед і вказівниками на перший і останній елемент, ви все одно можете записати і алгоритм, який видаляє елемент, зберігає його в локальну змінну, додає попередньо елемент у цій змінній до тієї ж черги до тих пір, поки перший елемент бачимо знову. Це працює лише в тому випадку, якщо ви можете однозначно ідентифікувати елемент (наприклад, за допомогою вказівника), а не просто щось із тим самим значенням. O (n) і використовує лише add () та delete (). У будь-якому випадку, простіше оптимізувати, щоб знайти причину насправді зробити це, окрім роздумів про алгоритми.
Thraidh

0

Важко уявити собі використання для такої реалізації, це правда. Але більшість сенсу - довести, що це можна зробити .

Щодо фактичного використання цих речей, я можу подумати про два. Одним із цілей цього є реалізація систем у обмежених середовищах, які не були розроблені для цього : наприклад, блоки червоного каменю Minecraft, як виявляється , являють собою повну систему Тьюрінга, яку люди використовували для реалізації логічних схем і навіть цілих процесорів. У перші дні ігор, керованих сценаріями, багато перших ботів ігор також були реалізовані таким чином.

Але ви також можете застосовувати цей принцип у зворотному порядку, гарантуючи, що щось не можливо в системі, коли ви не хочете, щоб це було . Це може виникнути в контексті безпеки: наприклад, потужні системи конфігурації можуть бути перевагою, але все ще є такі потужності, які ви, швидше за все, не надаєте користувачам. Це обмежує те, що ви можете дозволити робити мові конфігурації, щоб не зламати її зловмисником, але в цьому випадку це саме ви хочете.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.