0Pricing
DSA Interview Prep · Урок

Визуализация стека вызовов

Используйте модуль sys Python и трассировку с помощью print, чтобы наблюдать за расширением и сокращением кадров стека и понимать риски переполнения стека при глубокой рекурсии

«Визуализация стека вызовов» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA Interview Prep содержит 4 уроков всего.

Что такое стек вызовов

Каждый вызов функции в Python создаёт кадр стека в стеке вызовов. В кадре хранятся локальные переменные функции, адрес возврата (куда продолжится выполнение после возврата функции) и указатель на текущую инструкцию. Когда функция возвращает управление, её кадр извлекается из стека, а управление передаётся вызывающей функции. Стек вызовов растёт вниз при каждом вызове и сокращается при каждом возврате.

Понимание стека вызовов необходимо для отладки рекурсивного кода, оценки использования памяти и предотвращения ошибок переполнения стека при глубокой рекурсии.

import traceback

def outer():
    inner()

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

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

Наблюдение за кадрами стека с помощью sys

Модуль sys Python предоставляет инструменты для проверки стека вызовов во время выполнения. sys._getframe(n) возвращает кадр стека, расположенный на n уровней выше текущей функции. Каждый кадр содержит словарь f_locals с локальными переменными и f_code.co_name с именем функции. Добавление отладочных сообщений внутрь рекурсивной функции показывает, как кадры накапливаются и исчезают.

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)

Трассировка factorial в стеке вызовов

Проследим выполнение factorial(4) в стеке вызовов. Вызовы накапливаются: factorial(4) вызывает factorial(3), затем factorial(2), factorial(1) и factorial(0). В базовом случае в стеке находится 5 кадров. При возврате стек разматывается: factorial(0) возвращает 1; factorial(1) возвращает 1×1=1; factorial(2) возвращает 2×1=2; factorial(3) возвращает 3×2=6; factorial(4) возвращает 4×6=24. Глубина равна n+1, а сложность по памяти — 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)

Переполнение стека: предел рекурсии в Python

Python выдаёт RecursionError, когда стек вызовов превышает установленный предел (по умолчанию около 1000 кадров). Это защищает от бесконечной рекурсии, которая могла бы исчерпать всю память. Для задач с размером входных данных n = 10^4 и более рекурсивное решение с глубиной O(n) завершится сбоем, если не увеличить предел. Итерационный эквивалент использует O(1) памяти стека, потому что для внешней функции нужен только один кадр.

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!')

Увеличение предела рекурсии

Вы можете увеличить предел рекурсии Python с помощью sys.setrecursionlimit(n), но это лишь временная мера. Предел по умолчанию существует потому, что каждый кадр стека занимает память (обычно несколько сотен байт в CPython). Установка предела 10^6 и последующий запуск рекурсии глубиной 10^5 может выделить сотни мегабайт памяти под стек. Правильное решение обычно состоит в преобразовании алгоритма в итеративный или использовании мемоизации для уменьшения глубины.

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())

Стек вызовов при взаимной рекурсии

Взаимная рекурсия — это ситуация, когда функция A вызывает функцию B, а функция B вызывает функцию A. В стеке вызовов чередуются кадры A и B. Такой шаблон встречается при определении чётности и нечётности чисел, а также при моделировании конечных автоматов. Он корректен, пока глубина стека остаётся ограниченной, но определить глубину бывает сложнее, чем при простой линейной рекурсии.

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

Хвостовые вызовы и почему Python их не оптимизирует

Хвостовой вызов — это рекурсивный вызов, выполняемый последним перед возвратом: после него вычисления не выполняются. В языках вроде Haskell или Scheme хвостовые вызовы оптимизируются до циклов (оптимизация хвостовых вызовов, TCO), что даёт память стека O(1). Python намеренно не реализует TCO. Как объяснял Гвидо ван Россум, сохранение полной трассировки стека для отладки было важнее экономии памяти. Поэтому в Python код с хвостовой рекурсией по-прежнему использует 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

Вывод деревьев рекурсии

Визуализация дерева рекурсии помогает выявить повторяющиеся подзадачи — цель последующей мемоизации. Простой способ вывести дерево: добавить параметр indent, увеличивающийся на 2 пробела с каждым уровнем. Каждый вызов выводит свои аргументы при входе и возвращаемое значение при выходе. Запуск для Фибоначчи(5) наглядно показывает экспоненциальное разветвление и повторные вызовы.

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

Глубина стека = сложность по памяти

Для любой рекурсивной функции максимальная глубина стека вызовов равна максимальной глубине рекурсии в любой момент выполнения. Эта глубина напрямую определяет сложность по дополнительной памяти. Для линейной рекурсии (factorial, Фибоначчи, разворот строки) глубина равна O(n). Для алгоритмов «разделяй и властвуй» (сортировка слиянием, двоичный поиск) глубина равна O(log n). Для обходов деревьев глубина равна O(h), где h — высота дерева (O(log n) для сбалансированного дерева, O(n) в худшем случае).

# 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

Преобразование рекурсии в итерацию с явным стеком

Любой рекурсивный алгоритм можно сделать итеративным, явно управляя стеком вызовов с помощью списка Python. Вместо того чтобы позволять OS управлять кадрами, вы помещаете «задачи» в список и извлекаете их в цикле. Это избавляет от ограничения рекурсии Python и уменьшает накладные расходы на кадры, но усложняет код. Итеративный DFS с явным стеком, который мы рассматривали ранее, следует именно этому шаблону.

# 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]

Итоги: стек вызовов и память

Стек вызовов — скрытая структура данных, лежащая в основе любой рекурсии. Его глубина равна сложности по памяти рекурсивного алгоритма. Python ограничивает её примерно 1000 кадрами, поэтому алгоритмам с глубиной рекурсии O(n) нужен либо увеличенный предел (что рискованно), либо итеративная версия. При написании рекурсивного кода на собеседованиях всегда указывайте сложность по памяти из-за стека вызовов: «Здесь используется O(n) памяти для глубины рекурсии» или «O(log n) для обхода сбалансированного дерева».

Быстрая проверка

Проверьте, насколько вы поняли концепции курса «Структуры данных и алгоритмы — подготовка к собеседованию по программированию», изложенные в этом уроке.

Итоги урока

В этом уроке вы узнали: каждый рекурсивный вызов создаёт кадр стека, в котором хранятся локальные переменные и адрес возврата, максимальная глубина стека равна сложности рекурсии по дополнительной памяти, а также предел рекурсии Python (около 1000) делает алгоритмы с глубиной O(n) рискованными для больших n — преобразуйте их в итеративные с помощью явного стека. Далее мы сравним рекурсивные и итеративные решения и обсудим, когда использовать каждое из них.

Часто задаваемые вопросы

Урок «Визуализация стека вызовов» бесплатный?

Да — полный текст урока «Визуализация стека вызовов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Визуализация стека вызовов»?

Используйте модуль sys Python и трассировку с помощью print, чтобы наблюдать за расширением и сокращением кадров стека и понимать риски переполнения стека при глубокой рекурсии Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать DSA Interview Prep?

Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Визуализация стека вызовов»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке DSA Interview Prep?

Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Структура рекурсии: базовый случай, доверие, построение
  2. Визуализация стека вызовов
  3. Компромиссы рекурсивного и итеративного подходов
  4. Мемоизация: кэширование результатов рекурсии
← Назад к DSA Interview Prep