0Pricing
DSA Interview Prep · Lekcja

Wzajemna symulacja stosu i kolejki

Zaimplementują Państwo kolejkę za pomocą dwóch stosów i stos za pomocą dwóch kolejek, wyjaśniając zamortyzowany koszt każdego podejścia.

Wzajemna symulacja stosu i kolejki to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej DSA Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Po co symulować jedną strukturę za pomocą drugiej

Implementacja kolejki za pomocą dwóch stosów oraz stosu za pomocą dwóch kolejek to klasyczne zadania projektowe pojawiające się podczas rozmów technicznych. Sprawdzają one zrozumienie niezmienników obu struktur danych oraz umiejętność zachowania gwarancji jednej struktury przy użyciu operacji podstawowych drugiej. Osoby przeprowadzające rozmowy techniczne wykorzystują te zadania również jako punkt wyjścia do omówienia złożoności zamortyzowanej.

Kluczowa obserwacja jest następująca: stosy działają w trybie LIFO, a kolejki w trybie FIFO. Aby przekształcić jedną strukturę w drugą, należy odwrócić kolejność — a odwrócenie stosu na drugi stos przywraca pierwotną kolejność dodawania, czyli kolejność FIFO.

Kolejka za pomocą dwóch stosów (podejście leniwe)

Podejście leniwe polega na użyciu stosu inbox do operacji dodawania oraz stosu outbox do operacji usuwania. Gdy wywoływana jest operacja dequeue, a outbox jest pusty, wszystkie elementy są przenoszone ze stosu inbox na stos outbox — to odwrócenie przywraca kolejność FIFO. Jeśli outbox nie jest pusty, element jest zdejmowany bezpośrednio z niego. Transfery są wykonywane leniwie, dzięki czemu koszt transferu O(n) rozkłada się zamortyzowanie na wiele operacji.

class MyQueue:
    def __init__(self):
        self.inbox  = []
        self.outbox = []

    def push(self, x):
        self.inbox.append(x)

    def _transfer(self):
        if not self.outbox:
            while self.inbox:
                self.outbox.append(self.inbox.pop())

    def pop(self):
        self._transfer()
        return self.outbox.pop()

    def peek(self):
        self._transfer()
        return self.outbox[-1]

    def empty(self):
        return not self.inbox and not self.outbox

q = MyQueue()
q.push(1); q.push(2); q.push(3)
print(q.peek())  # 1
print(q.pop())   # 1
print(q.pop())   # 2
q.push(4)
print(q.pop())   # 3

Zamortyzowana analiza O(1) dla kolejki ze stosów

Każdy element jest przenoszony ze stosu inbox na stos outbox najwyżej raz. Usuwanie elementu ze stosu outbox zajmuje O(1), a transfery odbywają się tylko wtedy, gdy outbox jest pusty. Zatem łączna praca dla n operacji dodawania i n operacji usuwania wynosi najwyżej 2n operacji na stosach — łącznie O(n), czyli zamortyzowane O(1) na operację. Oznacza to, że pojedyncze operacje mogą w najgorszym przypadku zajmować O(n), ale średnia złożoność wynosi O(1).

# Trace transfer costs for 10 push/pop interleaved
class TrackedQueue:
    def __init__(self):
        self.inbox = []; self.outbox = []; self.transfers = 0

    def push(self, x): self.inbox.append(x)

    def pop(self):
        if not self.outbox:
            while self.inbox:
                self.outbox.append(self.inbox.pop())
                self.transfers += 1
        return self.outbox.pop()

q = TrackedQueue()
for i in range(5):
    q.push(i)
for _ in range(5):
    q.pop()
q.push(10); q.push(20)
q.pop()
print('Total transfer operations:', q.transfers)  # at most n

Stos za pomocą dwóch kolejek (leniwe usuwanie)

Implementacja stosu za pomocą dwóch kolejek jest mniej naturalna, ponieważ kolejki działają w trybie FIFO. Podejście z leniwym usuwaniem polega na utrzymywaniu jednej głównej kolejki i jednej kolejki tymczasowej. Podczas operacji push dodajemy element do głównej kolejki (O(1)). Podczas operacji pop lub peek usuwamy wszystkie elementy oprócz ostatniego do kolejki tymczasowej, zapamiętujemy ostatni element, a następnie zamieniamy kolejki miejscami. Operacja ta zajmuje O(n), ale push zajmuje O(1).

from collections import deque

class MyStack:
    def __init__(self):
        self.main = deque()
        self.temp = deque()

    def push(self, x):
        self.main.append(x)   # O(1)

    def pop(self):
        # Move all but last element to temp
        while len(self.main) > 1:
            self.temp.append(self.main.popleft())
        val = self.main.popleft()   # the 'top'
        self.main, self.temp = self.temp, self.main  # swap
        return val

    def top(self):
        while len(self.main) > 1:
            self.temp.append(self.main.popleft())
        val = self.main[0]
        self.temp.append(self.main.popleft())
        self.main, self.temp = self.temp, self.main
        return val

    def empty(self):
        return len(self.main) == 0

s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top())  # 3
print(s.pop())  # 3
print(s.pop())  # 2

Stos za pomocą jednej kolejki (obracanie przy dodawaniu)

Elegancka implementacja z jedną kolejką wygląda następująco: podczas operacji push dodajemy nowy element do kolejki, a następnie obracamy kolejkę tak, aby nowy element znalazł się na początku. Obracanie oznacza usunięcie z kolejki i ponowne dodanie wszystkich elementów, które znajdowały się w niej przed operacją push. Dzięki temu pop i peek zajmują O(1) (wystarczy usunąć lub podejrzeć element z początku). Operacja push zajmuje O(n) — jest to przeciwna zależność kosztów niż w wersji z dwiema kolejkami.

from collections import deque

class MyStackOneQueue:
    def __init__(self):
        self.q = deque()

    def push(self, x):
        self.q.append(x)
        # Rotate: move all preceding elements behind x
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())

    def pop(self):
        return self.q.popleft()

    def top(self):
        return self.q[0]

    def empty(self):
        return len(self.q) == 0

s = MyStackOneQueue()
s.push(1); s.push(2); s.push(3)
print(s.top())  # 3
print(s.pop())  # 3
print(s.top())  # 2

Podsumowanie kompromisów: który wariant wybrać

Dla kolejki z dwóch stosów: push O(1), pop/peek zamortyzowane O(1) — warto ją wybrać, gdy operacje pop występują często. Dla stosu z dwóch kolejek: push O(1), pop O(n) — warto go wybrać, gdy operacje push występują znacznie częściej niż pop. Dla stosu z jednej kolejki: push O(n), pop O(1) — warto go wybrać, gdy dominują operacje pop. Podczas rozmowy technicznej należy jasno przedstawić te kompromisy, aby pokazać, że myśli się nie tylko w kategoriach „to działa”.

print('Queue from 2 stacks: push O(1), pop O(1) amortised')
print('Stack from 2 queues: push O(1), pop O(n)')
print('Stack from 1 queue:  push O(n), pop O(1)')

Dlaczego odwrócenie przywraca FIFO

Gdy elementy 1, 2, 3 zostaną umieszczone na stosie (inbox), znajdują się od dołu do góry w kolejności 1, 2, 3. Zdjęcie wszystkich elementów i umieszczenie ich na drugim stosie (outbox) odwraca kolejność: na dole outbox znajduje się 3, a na górze 1. Zdejmowanie elementów z outbox daje kolejno 1, 2, 3 — dokładnie w kolejności ich dodania zgodnej z FIFO. Dlatego właśnie dokładnie dwa odwrócenia (dwa stosy) przywracają FIFO, podczas gdy pojedynczy stos zapewniałby LIFO.

# Demonstrate double-reversal = FIFO
inbox  = [1, 2, 3]   # pushed in this order
outbox = []
while inbox:
    outbox.append(inbox.pop())
print('outbox (one reversal):', outbox)  # [3, 2, 1] top-to-bottom

# Pop from outbox gives FIFO
result = []
while outbox:
    result.append(outbox.pop())
print('dequeued:', result)  # [1, 2, 3] — FIFO!

LeetCode 232: implementacja kolejki za pomocą stosów

LeetCode 232 to bezpośrednie zadanie „kolejka z dwóch stosów”. Oczekiwanym rozwiązaniem jest leniwy transfer do outbox. Podczas rozmowy technicznej należy zaznaczyć, że każdy element jest przenoszony z inbox do outbox najwyżej raz, dzięki czemu wszystkie operacje mają zamortyzowaną złożoność O(1). Warto również wspomnieć, że pojedyncze wywołanie pop może w najgorszym przypadku mieć złożoność O(n) (gdy outbox jest pusty), ale średnia złożoność dla n operacji wynosi O(1).

class MyQueue:
    def __init__(self):
        self.inbox  = []
        self.outbox = []

    def push(self, x):
        self.inbox.append(x)

    def pop(self):
        self.peek()             # ensure outbox is populated
        return self.outbox.pop()

    def peek(self):
        if not self.outbox:
            while self.inbox:   # transfer lazily
                self.outbox.append(self.inbox.pop())
        return self.outbox[-1]

    def empty(self):
        return not self.inbox and not self.outbox

# Simulation
q = MyQueue()
q.push(1); q.push(2)
print(q.peek())  # 1
print(q.pop())   # 1
print(q.empty()) # False

LeetCode 225: implementacja stosu za pomocą kolejek

LeetCode 225 to problem „stos z kolejek”. Najbardziej przejrzyste jest rozwiązanie z jedną kolejką i obracaniem przy operacji push. Po dodaniu elementu x obracamy kolejkę, przenosząc wszystkie elementy, które już się w niej znajdowały, za element x. Koszt wynosi O(n) dla każdej operacji push, ale operacje top i pop mają złożoność O(1). Należy przedstawić ten kompromis i potwierdzić, że odpowiada on ograniczeniom (na przykład obciążeniu z niewielką liczbą operacji push lub dużą liczbą operacji pop).

from collections import deque

class MyStack:
    def __init__(self):
        self.q = deque()

    def push(self, x):       # O(n)
        self.q.append(x)
        for _ in range(len(self.q) - 1):
            self.q.append(self.q.popleft())

    def pop(self):           # O(1)
        return self.q.popleft()

    def top(self):           # O(1)
        return self.q[0]

    def empty(self):
        return len(self.q) == 0

s = MyStack()
s.push(1); s.push(2); s.push(3)
print(s.top())  # 3
print(s.pop())  # 3
print(s.top())  # 2
print(s.empty()) # False

Rozszerzenie do trzech stosów w jednej tablicy

Powiązane wyzwanie projektowe: zaimplementowanie trzech stosów przy użyciu jednej tablicy. Jedno podejście dzieli tablicę na trzy równe, stałe sekcje. Bardziej elastyczne podejście wykorzystuje przeplatane przechowywanie z użyciem wskaźników: każdy stos rośnie w swoim obszarze, a gdy granice się zderzą, dane są kopiowane. Sprawdza to umiejętność zarządzania dynamiczną tablicą i pojawia się na rozmowach rekrutacyjnych na stanowiska seniorskie. Podejście ze stałymi sekcjami jest prostsze, ale marnuje miejsce, jeśli stosy rosną nierównomiernie.

class ThreeStacks:
    def __init__(self, size):
        self.data = [0] * (3 * size)
        self.tops = [-1, -1, -1]  # relative top of each stack
        self.size = size

    def push(self, stack_num, val):
        self.tops[stack_num] += 1
        if self.tops[stack_num] >= self.size:
            raise OverflowError('stack full')
        self.data[stack_num * self.size + self.tops[stack_num]] = val

    def pop(self, stack_num):
        if self.tops[stack_num] < 0:
            raise IndexError('stack empty')
        val = self.data[stack_num * self.size + self.tops[stack_num]]
        self.tops[stack_num] -= 1
        return val

ts = ThreeStacks(5)
ts.push(0, 10); ts.push(1, 20); ts.push(2, 30)
print(ts.pop(0), ts.pop(1), ts.pop(2))  # 10 20 30

Najważniejsze wnioski: wzorce symulacji

Problemy wzajemnej symulacji uczą szerszej zasady: każdą strukturę danych można zbudować na bazie innej, jeśli zapewni się wystarczające buforowanie pośrednie i odwracanie kolejności. Koszt symulacji zależy od tego, które operacje są optymalizowane — zawsze można zapewnić push O(1) lub pop O(1), ale zapewnienie obu operacji w O(1) wymaga amortyzacji albo wielu struktur pomocniczych.

Podczas rozmowy rekrutacyjnej należy zawsze zapytać: „Które operacje wykonuje się częściej?”. Pomaga to wybrać odpowiedni wariant implementacji i pokazuje dojrzałe podejście do wymagań operacyjnych.

Szybki test

Proszę sprawdzić swoją wiedzę na temat zagadnień z kursu Data Structures & Algorithms — Coding Interview Prep omówionych w tej lekcji.

Podsumowanie lekcji

W tej lekcji omówiono: kolejka z dwóch stosów zapewnia zamortyzowane pop O(1) dzięki leniwemu przenoszeniu elementów ze stosu wejściowego do wyjściowego, stos z jednej kolejki zapewnia pop O(1) dzięki obracaniu kolejki przy każdym push (push O(n)) oraz wybór operacji, która ma działać w O(1), zależy od sposobu użycia. W dalszej części poznają Państwo szczegóły implementacji map haszujących i obsługę kolizji.

Często zadawane pytania

Czy lekcja „Wzajemna symulacja stosu i kolejki” jest bezpłatna?

Tak — pełny tekst „Wzajemna symulacja stosu i kolejki” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu DSA Interview Prep, przejdź na CoddyKit PRO. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Wzajemna symulacja stosu i kolejki”?

Zaimplementują Państwo kolejkę za pomocą dwóch stosów i stos za pomocą dwóch kolejek, wyjaśniając zamortyzowany koszt każdego podejścia. Ćwiczysz DSA Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć DSA Interview Prep?

Nie wymagamy żadnego doświadczenia. DSA Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Wzajemna symulacja stosu i kolejki”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji DSA Interview Prep?

Tak. Każda lekcja DSA Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Implementacja stosu i zastosowania
  2. Implementacja kolejki i deque
  3. Wzorzec stosu monotonicznego
  4. Wzajemna symulacja stosu i kolejki
← Powrót do DSA Interview Prep