0Pricing
DSA Interview Prep · Lekcja

Wizualizacja stosu wywołań

Wykorzystają Państwo moduł sys w Pythonie i śledzenie za pomocą print, aby obserwować rozrastanie się i kurczenie ramek stosu oraz zrozumieć ryzyko przepełnienia stosu przy głębokiej rekurencji.

Wizualizacja stosu wywołań to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 2 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.

Czym jest stos wywołań

Każde wywołanie funkcji w Pythonie tworzy ramkę stosu na stosie wywołań. Ramka przechowuje zmienne lokalne funkcji, adres powrotu (miejsce, w którym wykonywanie zostanie wznowione po zakończeniu funkcji) oraz bieżący wskaźnik instrukcji. Gdy funkcja zwraca wynik, jej ramka jest zdejmowana ze stosu, a sterowanie wraca do funkcji wywołującej. Stos wywołań rośnie w dół przy każdym wywołaniu i zmniejsza się przy każdym powrocie.

Zrozumienie działania stosu wywołań jest niezbędne do debugowania kodu rekurencyjnego, szacowania zużycia pamięci i unikania błędów przepełnienia stosu przy głębokiej rekurencji.

import traceback

def outer():
    inner()

def inner():
    # Print the current call stack
    traceback.print_stack()

outer()
# Shows: module -> outer -> inner

Obserwowanie ramek stosu za pomocą sys

Moduł sys języka Python udostępnia narzędzia do badania stosu wywołań w czasie działania programu. sys._getframe(n) zwraca ramkę stosu znajdującą się n poziomów powyżej bieżącej funkcji. Każda ramka zawiera słownik f_locals ze zmiennymi lokalnymi oraz f_code.co_name z nazwą funkcji. Wstawienie komunikatów debugujących wewnątrz funkcji rekurencyjnej pokazuje, jak ramki narastają i znikają.

import sys

def countdown(n):
    depth = 0
    frame = sys._getframe(0)
    while frame:
        depth += 1
        frame = frame.f_back
    print(' ' * (n * 2) + f'countdown({n}) called, stack depth={depth}')
    if n <= 0:
        return
    countdown(n - 1)
    print(' ' * (n * 2) + f'countdown({n}) returning')

countdown(3)

Śledzenie silni na stosie wywołań

Należy prześledzić factorial(4) na stosie wywołań. Wywołania narastają: factorial(4) wywołuje factorial(3), które wywołuje factorial(2), następnie factorial(1), a na końcu factorial(0). W przypadku bazowym stos zawiera 5 ramek. Następnie stos jest rozwijany: factorial(0) zwraca 1; factorial(1) zwraca 1×1=1; factorial(2) zwraca 2×1=2; factorial(3) zwraca 3×2=6; factorial(4) zwraca 4×6=24. Głębokość wynosi n+1, a złożoność pamięciowa to O(n).

def factorial(n, indent=0):
    prefix = '  ' * indent
    print(prefix + f'-> factorial({n})')
    if n == 0:
        print(prefix + '<- returns 1')
        return 1
    result = n * factorial(n - 1, indent + 1)
    print(prefix + f'<- returns {result}')
    return result

factorial(4)

Przepełnienie stosu: limit rekurencji w Pythonie

Python zgłasza RecursionError, gdy stos wywołań przekroczy swój limit (domyślnie około 1000 ramek). Chroni to przed nieskończoną rekurencją, która mogłaby zużyć całą pamięć. W przypadku problemów z rozmiarem danych n = 10^4 lub większym rozwiązanie rekurencyjne o głębokości O(n) zakończy się błędem bez wcześniejszego zwiększenia limitu. Odpowiednik iteracyjny ma stałą złożoność pamięciową stosu O(1), ponieważ wykorzystuje tylko jedną ramkę funkcji zewnętrznej.

import sys

print('Recursion limit:', sys.getrecursionlimit())

def deep_recursion(n):
    if n == 0:
        return 0
    return 1 + deep_recursion(n - 1)

# Safe: within limit
try:
    print(deep_recursion(900))
except RecursionError:
    print('Overflow at 900')

# Overflow
try:
    print(deep_recursion(2000))
except RecursionError:
    print('RecursionError at 2000 — limit exceeded!')

Zwiększanie limitu rekurencji

Limit rekurencji w Pythonie można zwiększyć za pomocą sys.setrecursionlimit(n), ale jest to tylko doraźne rozwiązanie. Domyślny limit istnieje, ponieważ każda ramka stosu zajmuje pamięć (zwykle kilkaset bajtów w CPythonie). Ustawienie limitu na 10^6, a następnie wywołanie rekurencji o głębokości 10^5 może przydzielić setki megabajtów pamięci stosu. Właściwym rozwiązaniem jest zwykle przekształcenie algorytmu w wersję iteracyjną albo użycie memoizacji w celu zmniejszenia głębokości.

import sys

# Only increase when you are certain of the maximum depth
# and have confirmed it is safe
original = sys.getrecursionlimit()
sys.setrecursionlimit(5000)

def sum_to(n):
    if n == 0:
        return 0
    return n + sum_to(n - 1)

print(sum_to(3000))  # Works with increased limit
sys.setrecursionlimit(original)  # restore
print('Limit restored:', sys.getrecursionlimit())

Stos wywołań przy rekurencji wzajemnej

Rekurencja wzajemna występuje wtedy, gdy funkcja A wywołuje funkcję B, a funkcja B wywołuje funkcję A. Stos wywołań przełącza się między ramkami funkcji A i B. Ten schemat pojawia się przy określaniu parzystości i nieparzystości liczb oraz w symulacjach automatów stanów. Jest poprawny, dopóki głębokość stosu pozostaje ograniczona — jednak oszacowanie głębokości może być trudniejsze niż w przypadku prostej rekurencji liniowej.

def is_even(n):
    if n == 0:
        return True
    return is_odd(n - 1)

def is_odd(n):
    if n == 0:
        return False
    return is_even(n - 1)

# Stack alternates: is_even(4)->is_odd(3)->is_even(2)->is_odd(1)->is_even(0)
print(is_even(4))  # True
print(is_odd(5))   # True
print(is_even(7))  # False

Wywołania ogonowe i dlaczego Python ich nie optymalizuje

Wywołanie ogonowe to wywołanie rekurencyjne będące ostatnią operacją przed zwróceniem wyniku — po nim nie wykonuje się już żadnych obliczeń. W językach takich jak Haskell czy Scheme wywołania ogonowe są optymalizowane do postaci pętli (optymalizacja wywołań ogonowych, TCO), co daje stałą złożoność pamięciową stosu O(1). Python celowo nie implementuje TCO. Jak wyjaśniał Guido van Rossum, zachowanie pełnego śladu stosu na potrzeby debugowania było ważniejsze niż oszczędność pamięci. Dlatego w Pythonie kod rekurencyjny z wywołaniami ogonowymi nadal wykorzystuje stos o złożoności O(n).

# Tail-recursive factorial (accumulator pattern)
def factorial_tail(n, acc=1):
    if n == 0:
        return acc
    return factorial_tail(n - 1, acc * n)  # tail call

# In Python, this still uses O(n) stack space (no TCO)
# But it IS semantically tail-recursive
print(factorial_tail(6))   # 720
print(factorial_tail(10))  # 3628800

# Iterative version: same logic, O(1) stack
def factorial_iter(n):
    acc = 1
    while n > 0:
        acc *= n
        n -= 1
    return acc

print(factorial_iter(10))  # 3628800

Wypisywanie drzew rekurencji

Wizualizacja drzewa rekurencji pomaga identyfikować powtarzające się podproblemy (czyli elementy, które można objąć memoizacją). Prostym sposobem wypisania drzewa jest dodanie parametru indent, który zwiększa się o 2 spacje na każdym poziomie. Każde wywołanie wypisuje swoje argumenty przy wejściu oraz zwracaną wartość przy wyjściu. Uruchomienie takiego kodu dla Fibonacci(5) wyraźnie pokazuje wykładnicze rozgałęzianie i powtarzające się wywołania.

def fib_traced(n, indent=0):
    prefix = '  ' * indent
    print(prefix + f'fib({n})')
    if n <= 1:
        print(prefix + f'=> {n}')
        return n
    result = fib_traced(n-1, indent+1) + fib_traced(n-2, indent+1)
    print(prefix + f'=> {result}')
    return result

fib_traced(4)
# Shows the branching tree with duplicated sub-problems

Głębokość stosu = złożoność pamięciowa

W przypadku każdej funkcji rekurencyjnej maksymalna głębokość stosu wywołań jest równa maksymalnej głębokości rekurencji w dowolnym momencie wykonywania programu. Głębokość ta bezpośrednio odpowiada pomocniczej złożoności pamięciowej. Dla rekurencji liniowej (silnia, Fibonacci, odwracanie ciągu znaków) głębokość wynosi O(n). Dla algorytmów dziel i zwyciężaj (sortowanie przez scalanie, wyszukiwanie binarne) głębokość wynosi O(log n). Dla przechodzenia drzew głębokość wynosi O(h), gdzie h jest wysokością drzewa (O(log n) dla drzewa zrównoważonego i O(n) w najgorszym przypadku).

# Recursion depth = space complexity

# Linear recursion: O(n) stack
def linear_depth(n):
    if n == 0: return 0
    return 1 + linear_depth(n - 1)  # depth = n

# Logarithmic recursion: O(log n) stack
def log_depth(n):
    if n <= 1: return 0
    return 1 + log_depth(n // 2)    # depth = log2(n)

print('n=32 linear depth:', 32)
print('n=32 log depth:', log_depth(32))     # 5
print('n=1024 log depth:', log_depth(1024)) # 10

Konwersja rekurencji na iterację z użyciem jawnego stosu

Każdy algorytm rekurencyjny można przekształcić w iteracyjny, jawnie zarządzając stosem wywołań za pomocą listy języka Python. Zamiast pozwalać systemowi operacyjnemu zarządzać ramkami, należy umieszczać „zadania” na liście i zdejmować je w pętli. Eliminuje to limit rekurencji w Pythonie i zmniejsza narzut związany z ramkami, kosztem bardziej złożonego kodu. Pokazane wcześniej iteracyjne DFS z jawnym stosem dokładnie realizuje ten schemat.

# Recursive inorder traversal -> iterative with explicit stack
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val   = val
        self.left  = left
        self.right = right

def inorder_iterative(root):
    result = []
    stack  = []
    curr   = root
    while curr or stack:
        while curr:
            stack.append(curr)
            curr = curr.left
        curr = stack.pop()
        result.append(curr.val)
        curr = curr.right
    return result

root = TreeNode(4, TreeNode(2, TreeNode(1), TreeNode(3)), TreeNode(6))
print(inorder_iterative(root))  # [1, 2, 3, 4, 6]

Podsumowanie: stos wywołań i pamięć

Stos wywołań to ukryta struktura danych stojąca za każdą rekurencją. Jego głębokość odpowiada złożoności pamięciowej algorytmu rekurencyjnego. Python ogranicza ją do około 1000, dlatego algorytmy o głębokości rekurencji O(n) wymagają albo zwiększenia limitu (co jest ryzykowne), albo przepisania na wersję iteracyjną. Pisząc kod rekurencyjny podczas rozmowy kwalifikacyjnej, należy zawsze podać złożoność pamięciową wynikającą ze stosu wywołań: „To rozwiązanie wykorzystuje O(n) pamięci na głębokość rekurencji” lub „O(log n) w przypadku przechodzenia zrównoważonego drzewa”.

Szybki test

Sprawdź swoje rozumienie zagadnień z kursu Data Structures & Algorithms — Coding Interview Prep omówionych w tej lekcji.

Podsumowanie lekcji

W tej lekcji poznano: każde wywołanie rekurencyjne tworzy ramkę stosu zawierającą zmienne lokalne i adres powrotu, maksymalna głębokość stosu jest równa pomocniczej złożoności pamięciowej rekurencji oraz limit rekurencji w Pythonie (około 1000) sprawia, że algorytmy o głębokości O(n) są ryzykowne dla dużych wartości n — należy przekształcić je w wersję iteracyjną z użyciem jawnego stosu. W następnej części porównamy rozwiązania rekurencyjne i iteracyjne oraz omówimy, kiedy należy stosować każde z nich.

Często zadawane pytania

Czy lekcja „Wizualizacja stosu wywołań” jest bezpłatna?

Tak — pełny tekst „Wizualizacja stosu wywołań” 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 „Wizualizacja stosu wywołań”?

Wykorzystają Państwo moduł sys w Pythonie i śledzenie za pomocą print, aby obserwować rozrastanie się i kurczenie ramek stosu oraz zrozumieć ryzyko przepełnienia stosu przy głębokiej rekurencji. Ć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 2 z 4.

Ile czasu zajmuje lekcja „Wizualizacja stosu wywołań”?

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. Schemat rekurencji: przypadek bazowy, zaufanie, budowa
  2. Wizualizacja stosu wywołań
  3. Kompromisy między rekurencją a iteracją
  4. Memoizacja: buforowanie wyników rekurencji
← Powrót do DSA Interview Prep