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), push
pozostałyby takie same i pop
po prostu wymagałyby iteracji do ostatniego elementu i zwrócenia go. W obu przypadkach push
byłoby O(1)
i pop
był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.
Odpowiedzi:
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ą
for
pę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:
Dzięki temu możesz w
Queue
razie potrzeby użyć innych implementacji, a także ukryć 2 dostępne metody,LinkedList
które mogą ominąć duchaQueue
interfejsu. Obejmuje toget(int)
ipop()
(podczas kompilacji kodu występuje błąd logiczny, biorąc pod uwagę ograniczenia twojego przypisania. Zadeklarowanie zmiennych jakoQueue
zamiastLinkedList
ujawnia 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.Collections
lub 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.
źródło
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:
W rzeczywistości jest to świetne ćwiczenie. Powinienem to zrobić teraz :)
źródło
push
,peek
ipop
operacje są w O (1). To samo dotyczy stosu opartego na macierzy o zmiennym rozmiarze, z tą różnicą, żepush
jest 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.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.)
źródło
Ć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).
źródło
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
Queue
interfejs masize()
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 minimumadd()
iremove()
.Przy takiej implementacji musisz najpierw policzyć liczbę elementów, przenosząc je do drugiej kolejki, przenosząc
n-1
elementy 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.
źródło
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.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.
źródło