0Pricing
Coding Interview Prep · Lekcja

Implementacja stosu i zastosowania

Zaimplementują Państwo stos z operacjami push/pop/peek, a następnie rozwiążą zadania valid-parentheses, min-stack i evaluate reverse-polish notation.

Implementacja stosu i zastosowania to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 1 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 Coding Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Struktura danych stosu

Stos to struktura danych działająca zgodnie z zasadą last-in, first-out (LIFO). Ostatni element odłożony na stos jest pierwszym elementem z niego zdejmowanym. Można wyobrazić go sobie jako stos talerzy: elementy można dodawać i usuwać wyłącznie z góry. Podstawowe operacje to push (dodanie na górze), pop (usunięcie z góry) i peek (odczytanie elementu z góry bez usuwania). Wszystkie trzy operacje mają złożoność O(1) w poprawnie zaimplementowanym stosie.

W Pythonie lista doskonale sprawdza się jako stos: append odpowiada operacji push, pop() operacji pop, a [-1] operacji peek.

stack = []

# Push
stack.append(10)
stack.append(20)
stack.append(30)
print('After pushes:', stack)  # [10, 20, 30]

# Peek
print('Top:', stack[-1])       # 30

# Pop
print('Popped:', stack.pop())  # 30
print('After pop:', stack)     # [10, 20]

Klasa stosu z operacjami Push, Pop, Peek i isEmpty

Umieszczenie listy w klasie zapewnia przejrzystszy interfejs i zapobiega przypadkowemu użyciu operacji, które nie należą do stosu, takich jak insert lub indeksowanie pozycji innych niż górny element. Jest to implementacja, jakiej oczekują rekruterzy, gdy proszą o „zaimplementowanie stosu od podstaw”.

class Stack:
    def __init__(self):
        self._data = []

    def push(self, val):
        self._data.append(val)

    def pop(self):
        if self.is_empty():
            raise IndexError('pop from empty stack')
        return self._data.pop()

    def peek(self):
        if self.is_empty():
            raise IndexError('peek at empty stack')
        return self._data[-1]

    def is_empty(self):
        return len(self._data) == 0

    def __len__(self):
        return len(self._data)

s = Stack()
s.push(1); s.push(2); s.push(3)
print(s.peek())  # 3
print(s.pop())   # 3
print(len(s))    # 2

Poprawne nawiasy (LeetCode 20)

LeetCode 20 „Valid Parentheses”: określenie, czy ciąg nawiasów jest poprawnie zbilansowany. Dla każdego nawiasu otwierającego należy wykonać push. Dla każdego nawiasu zamykającego należy sprawdzić, czy element na szczycie stosu jest odpowiadającym mu nawiasem otwierającym; jeśli nie albo jeśli stos jest pusty, należy zwrócić False. Jeśli na końcu stos jest pusty, ciąg jest poprawny. To klasyczne pierwsze zastosowanie stosu podczas rozmów kwalifikacyjnych dotyczących programowania.

def isValid(s):
    stack = []
    matching = {')': '(', '}': '{', ']': '['}
    for ch in s:
        if ch in '([{':
            stack.append(ch)
        else:
            if not stack or stack[-1] != matching[ch]:
                return False
            stack.pop()
    return len(stack) == 0

print(isValid('()[]{}'))    # True
print(isValid('([)]'))      # False
print(isValid('{[]}'))      # True
print(isValid(']'))         # False

Stos minimum (LeetCode 155)

LeetCode 155 „Min Stack”: zaprojektowanie stosu obsługującego operacje push, pop, peek i getMin, każdą w czasie O(1). Sztuczka polega na utrzymywaniu drugiego stosu, który w każdej chwili przechowuje minimum. Podczas dodawania elementu należy dodać go również na stos minimum, jeśli nowa wartość jest <= bieżącego minimum (albo jeśli stos minimum jest pusty). Podczas usuwania elementu należy usunąć go również ze stosu minimum, jeśli usunięta wartość jest równa bieżącemu minimum.

class MinStack:
    def __init__(self):
        self.stack = []
        self.min_stack = []

    def push(self, val):
        self.stack.append(val)
        if not self.min_stack or val <= self.min_stack[-1]:
            self.min_stack.append(val)

    def pop(self):
        val = self.stack.pop()
        if val == self.min_stack[-1]:
            self.min_stack.pop()
        return val

    def top(self):
        return self.stack[-1]

    def getMin(self):
        return self.min_stack[-1]

ms = MinStack()
ms.push(-2); ms.push(0); ms.push(-3)
print(ms.getMin())  # -3
ms.pop()
print(ms.top())     # 0
print(ms.getMin())  # -2

Obliczanie odwrotnej notacji polskiej

LeetCode 150 „Evaluate Reverse Polish Notation” (notacja postfiksowa): operandy są odkładane na stos; po napotkaniu operatora należy zdjąć dwa operandy, zastosować operator i odłożyć wynik. Kolejność ma znaczenie przy odejmowaniu i dzieleniu: pierwszy zdjęty operand jest prawym operandem, a drugi — lewym.

def evalRPN(tokens):
    stack = []
    ops = set(['+', '-', '*', '/'])
    for tok in tokens:
        if tok not in ops:
            stack.append(int(tok))
        else:
            b = stack.pop()  # right operand
            a = stack.pop()  # left operand
            if tok == '+':
                stack.append(a + b)
            elif tok == '-':
                stack.append(a - b)
            elif tok == '*':
                stack.append(a * b)
            else:             # division truncated toward zero
                stack.append(int(a / b))
    return stack[0]

print(evalRPN(['2','1','+','3','*']))     # 9
print(evalRPN(['4','13','5','/','+']))    # 6
print(evalRPN(['10','6','9','3','+','-11','*','/','*','17','+','5','+']))  # 22

Dekodowanie ciągu (LeetCode 394)

LeetCode 394 „Decode String”: mając zakodowany ciąg, taki jak 3[a2[c]], należy rozwinąć go do postaci accaccacc. Należy użyć dwóch stosów: jednego dla liczników powtórzeń i drugiego dla zgromadzonych ciągów. Po napotkaniu cyfry należy zbudować pełną liczbę. Po napotkaniu [ należy odłożyć bieżący ciąg i licznik. Po napotkaniu ] należy zdjąć te wartości ze stosów i powtórzyć bieżący fragment. Po napotkaniu litery należy dodać ją do bieżącego ciągu.

def decodeString(s):
    count_stack = []
    str_stack   = []
    current_str = ''
    current_num = 0
    for ch in s:
        if ch.isdigit():
            current_num = current_num * 10 + int(ch)
        elif ch == '[':
            count_stack.append(current_num)
            str_stack.append(current_str)
            current_str = ''
            current_num = 0
        elif ch == ']':
            repeats = count_stack.pop()
            current_str = str_stack.pop() + current_str * repeats
        else:
            current_str += ch
    return current_str

print(decodeString('3[a]2[bc]'))    # 'aaabcbc'
print(decodeString('3[a2[c]]'))     # 'accaccacc'
print(decodeString('2[abc]3[cd]ef')) # 'abcabccdcdcdef'

Dzienne temperatury (wprowadzenie do stosu monotonicznego)

LeetCode 739 „Daily Temperatures”: dla każdego dnia należy znaleźć liczbę dni do wystąpienia wyższej temperatury. Naiwne rozwiązanie ma złożoność O(n²). W rozwiązaniu ze stosem należy przechodzić przez temperatury; dla każdego dnia zdejmować ze stosu wszystkie elementy (indeksy dni), których temperatura jest niższa niż temperatura dzisiejsza. Odpowiedzią dla każdego zdjętego dnia jest (today - popped_day). Następnie należy odłożyć bieżący dzień. Pozostałe elementy stosu nigdy nie doczekały się wyższej temperatury — ich odpowiedź wynosi 0.

def dailyTemperatures(temps):
    result = [0] * len(temps)
    stack  = []  # stores indices
    for i, t in enumerate(temps):
        while stack and temps[stack[-1]] < t:
            j = stack.pop()
            result[j] = i - j
        stack.append(i)
    return result

print(dailyTemperatures([73,74,75,71,69,72,76,73]))
# [1, 1, 4, 2, 1, 1, 0, 0]

Stos w przechodzeniu DFS

Stos wywołań używany w rekurencyjnym DFS można zastąpić jawnym stosem, przekształcając algorytm w iteracyjny. Należy odłożyć korzeń; dopóki stos nie jest pusty, zdejmować węzeł, przetwarzać go i odkładać jego dzieci (najpierw prawe, potem lewe, aby przetwarzać je od lewej do prawej). Iteracyjny DFS zachowuje się tak samo jak rekurencyjny DFS, ale pozwala uniknąć limitu rekurencji Pythona w przypadku głębokich drzew.

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val   = val
        self.left  = left
        self.right = right

def preorder_iterative(root):
    if not root:
        return []
    result, stack = [], [root]
    while stack:
        node = stack.pop()
        result.append(node.val)
        if node.right:
            stack.append(node.right)  # push right first
        if node.left:
            stack.append(node.left)   # so left is processed first
    return result

root = TreeNode(1, TreeNode(2, TreeNode(4), TreeNode(5)), TreeNode(3))
print(preorder_iterative(root))  # [1, 2, 4, 5, 3]

Złożoność czasowa i pamięciowa

Wszystkie operacje stosu (push, pop, peek, isEmpty) mają zamortyzowaną złożoność O(1). Zbudowanie stosu zawierającego n elementów zajmuje O(n). W najgorszym przypadku stos zajmuje O(n) pamięci, gdy przechowywane są wszystkie elementy. W zadaniach wykorzystujących stos monotoniczny każdy element jest odkładany i zdejmowany najwyżej raz, co daje łączny czas O(n) we wszystkich iteracjach — a nie O(n²), jak mogłoby sugerować naiwne odczytanie zewnętrznej pętli.

# Demonstrate O(n) total for monotonic stack
# Each element pushed once, popped at most once => 2n operations total

def count_ops(n):
    pushes = pops = 0
    stack = []
    for i in range(n):
        while stack and stack[-1] < i:  # simulated decreasing condition
            stack.pop()
            pops += 1
        stack.append(i)
        pushes += 1
    return pushes, pops

p, pp = count_ops(1000)
print(f'Pushes: {p}, Pops: {pp}, Total ops: {p+pp}')  # <= 2000

Największy prostokąt w histogramie (wprowadzenie)

LeetCode 84 „Largest Rectangle in Histogram” to najtrudniejsze z klasycznych zadań wykorzystujących stos. Dla każdego słupka prostokąt, którego może on być podstawą, rozciąga się w lewo do napotkania niższego słupka i w prawo do napotkania niższego słupka. Stos monotoniczny przechowuje indeksy słupków uporządkowanych rosnąco według wysokości. Po napotkaniu niższego słupka należy zdejmować elementy i obliczać prostokąt o wysokości zdjętego słupka. Stos pozwala znaleźć lewą i prawą granicę w czasie O(1) dla każdego zdejmowanego elementu.

def largestRectangleArea(heights):
    stack  = []  # indices, increasing heights
    result = 0
    heights = heights + [0]  # sentinel forces all pops
    for i, h in enumerate(heights):
        while stack and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            width  = i if not stack else i - stack[-1] - 1
            result = max(result, height * width)
        stack.append(i)
    return result

print(largestRectangleArea([2,1,5,6,2,3]))  # 10
print(largestRectangleArea([2,4]))           # 4

Strategia rozwiązywania zadań stosowych podczas rozmowy kwalifikacyjnej

Zadania stosowe często ukrywają się pod postacią „przetwarzania od środka na zewnątrz” albo „znajdowania następnego większego/mniejszego elementu”. O użyciu stosu mogą świadczyć następujące sygnały: potrzebują Państwo ostatnio napotkanego elementu, dopasowują Państwo pary (nawiasy, znaczniki) albo chcą Państwo uzyskać O(n) w zadaniu, które naiwnie wymaga zagnieżdżonych pętli O(n²). W szczególności stosy monotoniczne przekształcają zadanie „dla każdego elementu znajdź najbliższy większy/mniejszy” z O(n²) w O(n).

Podczas rozmowy kwalifikacyjnej należy jasno określić niezmiennik stosu: „Będę utrzymywać stos indeksów w kolejności malejących wysokości”.

Szybki test

Sprawdź swoją wiedzę z zagadnień Data Structures & Algorithms — Coding Interview Prep omówionych w tej lekcji.

Podsumowanie lekcji

W tej lekcji nauczyłeś się, że: listy Pythona implementują operacje push/pop/peek w czasie O(1), dzięki czemu idealnie nadają się na stosy, poprawne nawiasy i stos minimum to dwa klasyczne zadania stosowe podczas rozmów kwalifikacyjnych, a stosy monotoniczne rozwiązują zadania typu next-greater-element w czasie O(n), ponieważ każdy element jest odkładany i zdejmowany najwyżej raz. W następnej części zbudujemy kolejki za pomocą deque z Pythona i rozwiążemy zadanie o maksimum w przesuwanym oknie.

Często zadawane pytania

Czy lekcja „Implementacja stosu i zastosowania” jest bezpłatna?

Tak — pełny tekst „Implementacja stosu i zastosowania” 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 Coding Interview Prep, przejdź na CoddyKit PRO. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Implementacja stosu i zastosowania”?

Zaimplementują Państwo stos z operacjami push/pop/peek, a następnie rozwiążą zadania valid-parentheses, min-stack i evaluate reverse-polish notation. Ćwiczysz Coding 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ąć Coding Interview Prep?

Nie wymagamy żadnego doświadczenia. Coding 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 1 z 4.

Ile czasu zajmuje lekcja „Implementacja stosu i zastosowania”?

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 Coding Interview Prep?

Tak. Każda lekcja Coding 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 Coding Interview Prep