Jaki jest sens implementowania stosu przy użyciu dwóch kolejek?

34

Mam następujące pytanie o pracę domową:

Zaimplementuj metody stosu push (x) i pop () przy użyciu dwóch kolejek.

Wydaje mi się to dziwne, ponieważ:

  • Stos to kolejka (LIFO)
  • Nie rozumiem, dlaczego potrzebujesz dwóch kolejek, aby go zaimplementować

Szukałem w okolicy:

i znalazłem kilka rozwiązań. Właśnie z tym skończyłem:

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;
    }
}

Ale nie rozumiem, na czym polega przewaga nad pojedynczą kolejką; wersja z dwiema kolejkami wydaje się bezcelowo skomplikowana.

Powiedzmy, że wybraliśmy, aby pushy były bardziej wydajne z 2 (jak to zrobiłem powyżej), pushpozostałyby takie same i poppo prostu wymagałyby iteracji do ostatniego elementu i zwrócenia go. W obu przypadkach pushbyłoby O(1)i popbyłoby O(n); ale wersja z pojedynczą kolejką byłaby znacznie prostsza. Powinien wymagać tylko pojedynczej pętli for.

Czy coś brakuje? Każdy wgląd tutaj będzie mile widziany.

Carcigenicate
źródło
17
Kolejka zazwyczaj odnosi się do struktury FIFO, podczas gdy stos jest strukturą LIFO. Interfejs LinkedList w Javie to deque (kolejka podwójnie zakończona), która umożliwia dostęp zarówno do FIFO, jak i LIFO. Spróbuj zmienić programowanie na interfejs kolejki zamiast implementacji LinkedList.
12
Częstszym problemem jest implementacja kolejki przy użyciu dwóch stosów. Być może książka Chrisa Okasakiego na temat czysto funkcjonalnych struktur danych jest interesująca.
Eric Lippert,
2
Opierając się na tym, co powiedział Eric, czasami możesz znaleźć się w języku opartym na stosach (takim jak dc lub automat do pchania z dwoma stosami (odpowiednik maszyny Turinga, ponieważ, cóż, możesz zrobić więcej)), gdzie możesz się znaleźć wiele stosów, ale nie ma kolejki.
1
@MichaelT: Możesz też znaleźć się na procesorze opartym na stosie
slebetman,
11
„Stos to kolejka (LIFO)” ... uhm, kolejka to linia oczekiwania. Jak linia do korzystania z publicznej toalety. Czy linie, na które czekasz, zachowują się jak LIFO? Przestań używać terminu „kolejka LIFO”, to nonsens.
Mehrdad

Odpowiedzi:

44

Nie ma żadnej korzyści: jest to ćwiczenie czysto akademickie.

Bardzo dawno temu, kiedy byłem studentem pierwszego roku na studiach miałem podobną ćwiczenia 1 . Celem było nauczenie studentów, jak korzystać z programowania obiektowego do implementacji algorytmów zamiast pisać rozwiązania iteracyjne za pomocą forpętli z licznikami pętli. Zamiast tego łącz i wykorzystuj istniejące struktury danych, aby osiągnąć swoje cele.

Nigdy nie użyjesz tego kodu w Real World TM . To, co musisz wziąć z tego ćwiczenia, to jak „myśleć nieszablonowo” i ponownie użyć kodu.


Pamiętaj, że powinieneś używać interfejsu java.util.Queue w kodzie zamiast bezpośredniego wdrożenia:

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

Dzięki temu możesz w Queuerazie potrzeby użyć innych implementacji, a także ukryć 2 dostępne metody, LinkedListktóre mogą ominąć ducha Queueinterfejsu. Obejmuje to get(int)i pop()(podczas kompilacji kodu występuje błąd logiczny, biorąc pod uwagę ograniczenia twojego przypisania. Zadeklarowanie zmiennych jako Queuezamiast LinkedListujawnia je). Literatura pokrewna: Zrozumienie „programowania do interfejsu” i dlaczego interfejsy są przydatne?

1 Nadal pamiętam: ćwiczenie polegało na odwróceniu stosu przy użyciu tylko metod interfejsu stosu i bez metod narzędziowych java.util.Collectionslub innych klas narzędzi „tylko statycznych”. Prawidłowe rozwiązanie polega na użyciu innych struktur danych jako tymczasowych obiektów przechowujących: musisz znać różne struktury danych, ich właściwości i sposób ich łączenia. Zakłopotał większość mojej klasy CS101, która nigdy wcześniej nie programowała.

2 Metody są nadal dostępne, ale nie można uzyskać do nich dostępu bez rzutów typu i refleksji. Dlatego nie jest łatwo korzystać z tych metod niekolejkowych.

Społeczność
źródło
1
Dzięki. To chyba ma sens. Zdałem sobie również sprawę, że w powyższym kodzie używam „nielegalnych” operacji (przesuwanie do przodu FIFO), ale nie sądzę, żeby to coś zmieniło. Odwróciłem wszystkie operacje i nadal działa zgodnie z przeznaczeniem. Poczekam chwilę, zanim zaakceptuję, ponieważ nie chcę zniechęcać innych osób do wyrażania opinii. Ale dziękuję.
Carcigenicate
19

Nie ma przewagi. Prawidłowo zdałeś sobie sprawę, że użycie kolejek do wdrożenia stosu prowadzi do okropnej złożoności czasu. Żaden (kompetentny) programista nigdy nie zrobiłby czegoś takiego w „prawdziwym życiu”.

Ale to możliwe. Możesz użyć jednej abstrakcji, aby zaimplementować inną, i odwrotnie. Stos można zaimplementować w postaci dwóch kolejek, a także można zaimplementować kolejkę w postaci dwóch stosów. Zaletą tego ćwiczenia jest:

  • zbierasz stosy
  • podsumowujesz kolejki
  • przyzwyczajasz się do myślenia algorytmicznego
  • uczysz się algorytmów związanych ze stosem
  • możesz pomyśleć o kompromisach w algorytmach
  • realizując równoważność kolejek i stosów, łączysz różne tematy swojego kursu
  • zdobywasz praktyczne doświadczenie programistyczne

W rzeczywistości jest to świetne ćwiczenie. Powinienem to zrobić teraz :)

amon
źródło
3
@JohnKugelman Dzięki za edycję, ale naprawdę miałem na myśli „okropną złożoność czasu”. Do połączonych ze opartym na liście w stos push, peeki popoperacje są w O (1). To samo dotyczy stosu opartego na macierzy o zmiennym rozmiarze, z tą różnicą, że pushjest zamortyzowana w O (1), z najgorszym przypadkiem O (n). W porównaniu z tym stos oparty na kolejce jest znacznie gorszy w przypadku O (n) push, O (1) pop i peek lub alternatywnie O (1) push, O (n) pop i peek.
amon
1
„okropna złożoność czasu” i „znacznie gorszy” nie są do końca właściwe. Zamortyzowana złożoność to wciąż O (1) dla push i pop. W TAOCP (vol1?) Jest zabawne pytanie (w zasadzie musisz pokazać, że liczba przełączeń elementu z jednego stosu na drugi jest stała). Wydajność najgorszego przypadku dla jednej operacji jest inna, ale wtedy rzadko słyszę, że ktoś mówi o wydajności O (N) dla wypychania list ArrayLists - zwykle nie jest to interesująca liczba.
Voo
5

Zdecydowanie prawdziwym celem jest utworzenie kolejki z dwóch stosów. Jeśli używasz niezmiennych struktur danych z języka funkcjonalnego, możesz wcisnąć do stosu elementów, które można przesuwać, i wyciągnąć z listy elementów, które można wstawiać. Przedmioty z możliwością otwierania są tworzone, gdy wszystkie przedmioty zostały wysunięte, a nowy stos z możliwością wstawiania jest odwrotnością stosu, w którym nowy stos może być pusty. Jest wydajny.

Co do stosu złożonego z dwóch kolejek? Może to mieć sens w kontekście, w którym dostępnych jest kilka dużych i szybkich kolejek. Jest to zdecydowanie bezużyteczne jako tego rodzaju ćwiczenie Java. Ale może to mieć sens, jeśli są to kanały lub kolejki wiadomości. (tj .: N wiadomości w kolejce, z operacją O (1), aby przenieść (N-1) elementy z przodu do nowej kolejki.)

Obrabować
źródło
Hmm ... to sprawiło, że pomyślałem o wykorzystaniu rejestrów przesuwnych jako podstawy obliczeń oraz o architekturze pasów / młynów
Slebetman,
wow, procesor Mill jest naprawdę interesujący. Na pewno „kolejka”.
Rob
2

Ćwiczenie jest niepotrzebnie zaprojektowane z praktycznego punktu widzenia. Chodzi o to, aby zmusić cię do użycia interfejsu kolejki w sprytny sposób do wdrożenia stosu. Na przykład rozwiązanie „Jedna kolejka” wymaga iteracji w kolejce, aby uzyskać ostatnią wartość wejściową dla operacji „pop” stosu. Jednak struktura danych w kolejce nie pozwala na iterację po wartościach, masz ograniczony dostęp do nich w trybie „pierwsze weszło, pierwsze wyszło” (FIFO).

Destruktor
źródło
2

Jak już zauważyli inni: nie ma przewagi w świecie rzeczywistym.

W każdym razie jedna odpowiedź na drugą część pytania, dlaczego po prostu nie użyć jednej kolejki, jest poza Javą.

W Javie nawet Queueinterfejs ma size()metodę, a wszystkie standardowe implementacje tej metody to O (1).

Niekoniecznie jest to prawdą w przypadku naiwnej / kanonicznej listy połączonej, którą zaimplementowałby programista C / C ++, który utrzymywałby wskaźniki do pierwszego i ostatniego elementu, a każdy element był wskaźnikiem do następnego elementu.

W takim przypadku size()jest to O (n) i należy tego unikać w pętlach. Lub implementacja jest nieprzejrzysta i zapewnia jedynie absolutne minimum add()i remove().

Przy takiej implementacji musisz najpierw policzyć liczbę elementów, przenosząc je do drugiej kolejki, przenosząc n-1elementy z powrotem do pierwszej kolejki i zwracając pozostały element.

To powiedziawszy, prawdopodobnie nie stworzyłoby czegoś takiego, jeśli mieszkasz w krainie Java.

Thraidh
źródło
Dobra uwaga na temat size()metody. Jednak przy braku metody O (1) size()stos jest trywialny, aby stos mógł śledzić swój bieżący rozmiar. Nic nie powstrzyma implementacji w jednej kolejce.
cmaster
Wszystko zależy od implementacji. Jeśli masz kolejkę, zaimplementowaną tylko ze wskaźnikami do przodu i wskaźnikami do pierwszego i ostatniego elementu, nadal możesz pisać i algorytm, który usuwa element, zapisuje go do zmiennej lokalnej, dodaje element poprzednio w tej zmiennej do tej samej kolejki, dopóki pierwszy element jest ponownie widoczny. Działa to tylko wtedy, gdy można jednoznacznie zidentyfikować element (np. Za pomocą wskaźnika), a nie tylko coś o tej samej wartości. O (n) i używa tylko add () i remove (). W każdym razie łatwiej jest zoptymalizować, aby znaleźć powód, aby to zrobić, z wyjątkiem myślenia o algorytmach.
Thraidh
0

To prawda, że ​​trudno wyobrazić sobie zastosowanie takiej implementacji. Ale najważniejsze jest udowodnienie, że da się to zrobić .

Pod względem faktycznego wykorzystania tych rzeczy mogę jednak pomyśleć o dwóch. Jednym z zastosowań jest wdrażanie systemów w ograniczonych środowiskach, które nie zostały do ​​tego zaprojektowane : na przykład bloki z czerwonego kamienia Minecraft okazują się reprezentować kompletny system Turinga, którego ludzie używali do wdrażania obwodów logicznych, a nawet całych procesorów. W pierwszych dniach gier opartych na skryptach zaimplementowano również wiele pierwszych botów do gier.

Ale możesz również zastosować tę zasadę w odwrotnej kolejności, upewniając się, że coś nie jest możliwe w systemie, gdy nie chcesz . Może się to zdarzyć w kontekście bezpieczeństwa: na przykład potężne systemy konfiguracyjne mogą być atutem, ale nadal istnieją poziomy mocy, których raczej nie możesz dać użytkownikom. Ogranicza to to, na co możesz pozwolić językowi konfiguracyjnemu, aby nie został on zniszczony przez atakującego, ale w tym przypadku właśnie tego chcesz.

The Spooniest
źródło