0Pricing
DSA Interview Prep · Aula

Compromissos entre Recursão e Iteração

Converta fatorial e Fibonacci recursivos em laços iterativos e explique quando o limite de recursão e o tamanho da pilha do Python tornam a iteração preferível.

Compromissos entre Recursão e Iteração é uma aula grátis de DSA Interview Prep no CoddyKit. Esta é a aula 3 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DSA Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DSA Interview Prep inclui 4 aulas no total.

A dualidade entre recursão e iteração

Todo algoritmo que pode ser escrito recursivamente também pode ser escrito iterativamente, e vice-versa. A versão recursiva costuma refletir mais de perto a definição matemática do problema, enquanto a versão iterativa oferece controle explícito sobre a memória e evita riscos de estouro da pilha. Escolher entre elas é uma decisão pragmática baseada na legibilidade, nos limites de profundidade e nos requisitos de desempenho.

Em entrevistas, ser capaz de apresentar as duas versões e explicar os compromissos envolvidos é um forte sinal de domínio.

factorial: recursivo versus iterativo

factorial é o exemplo clássico. A versão recursiva codifica diretamente a definição matemática n! = n × (n-1)!. Ela usa espaço O(n) na pilha devido aos n valores de retorno pendentes. A versão iterativa percorre um laço de 1 até n, usando espaço O(1). Para n = 1000, a versão recursiva atinge o limite padrão do Python; a versão iterativa lida com n arbitrariamente grande.

def factorial_rec(n):
    if n == 0:
        return 1
    return n * factorial_rec(n - 1)   # O(n) stack

def factorial_iter(n):
    result = 1
    for i in range(2, n + 1):
        result *= i                    # O(1) stack
    return result

print(factorial_rec(10))   # 3628800
print(factorial_iter(10))  # 3628800

# Large n: iterative works, recursive may overflow
print(factorial_iter(1000) > 0)  # True (Python handles big ints)

Fibonacci: exponencial versus linear

O Fibonacci recursivo ingênuo tem complexidade O(2^n) em time — é desastrosamente lento para valores grandes de n. A versão iterativa tem complexidade O(n) em time e espaço O(1). A recursão com memoização (na próxima lição) também tem complexidade O(n) em time, mas usa espaço O(n) devido ao dicionário de memoização e à pilha O(n). Para Fibonacci, a abordagem iterativa é ideal em todos os aspectos. Para n = 50, a recursão ingênua leva segundos; a versão iterativa leva microssegundos.

import time

def fib_rec(n):
    if n <= 1: return n
    return fib_rec(n-1) + fib_rec(n-2)   # O(2^n)

def fib_iter(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a                              # O(n) time, O(1) space

# Timing comparison for n=35
start = time.time()
fib_rec(35)
print(f'Recursive n=35: {time.time()-start:.3f}s')

start = time.time()
fib_iter(35)
print(f'Iterative n=35: {time.time()-start:.6f}s')

print(fib_iter(100))  # handles large n

Percurso de Árvores: Recursivo versus Iterativo

O percurso recursivo de uma árvore é naturalmente limpo, pois a estrutura da árvore reflete a recursão. Porém, em uma árvore profundamente desbalanceada (essencialmente uma lista encadeada), a profundidade da recursão é igual à altura da árvore = O(n), o que pode causar um estouro de pilha. A versão iterativa, que usa uma pilha explícita, não tem limite de profundidade e permite que o tamanho da pilha cresça na memória dinâmica, em vez de na pilha de chamadas.

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

def preorder_rec(root, result=None):
    if result is None: result = []
    if root:
        result.append(root.val)
        preorder_rec(root.left, result)
        preorder_rec(root.right, result)
    return result

def preorder_iter(root):
    if not root: return []
    result, stack = [], [root]
    while stack:
        node = stack.pop()
        result.append(node.val)
        if node.right: stack.append(node.right)
        if node.left:  stack.append(node.left)
    return result

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

Ordenação por Mesclagem: Recursiva versus Iterativa (de Baixo para Cima)

A ordenação por mesclagem é naturalmente recursiva (dividir, aplicar a recursão, mesclar). A ordenação por mesclagem iterativa de baixo para cima evita completamente a recursão: começa com subvetores de tamanho 1, mescla pares adjacentes em subvetores de tamanho 2, depois de tamanho 4 e assim por diante, dobrando o tamanho do subvetor a cada passagem. A ordenação por mesclagem de baixo para cima tem tempo O(n log n), espaço O(n) (para o buffer de mesclagem) e espaço O(1) na pilha.

def merge_sort_iterative(arr):
    n = len(arr)
    size = 1
    while size < n:
        for start in range(0, n, 2 * size):
            mid   = min(start + size, n)
            end   = min(start + 2 * size, n)
            left  = arr[start:mid]
            right = arr[mid:end]
            # Merge
            i = j = 0
            for k in range(start, end):
                if i < len(left) and (j >= len(right) or left[i] <= right[j]):
                    arr[k] = left[i]; i += 1
                else:
                    arr[k] = right[j]; j += 1
        size *= 2
    return arr

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

Quando a Recursão é Claramente Melhor

A recursão se destaca quando o problema tem uma estrutura semelhante a uma árvore que corresponde diretamente ao grafo de chamadas, quando os casos-base são naturais e quando a profundidade é limitada (O(log n) para árvores balanceadas e divisão e conquista). Exemplos: análise de JSON, percurso de diretórios, árvores de jogos e problemas de retrocesso. Nesses casos, o código recursivo é mais curto, mais claro e mais fácil de ter sua correção demonstrada do que a versão iterativa equivalente.

# Recursion is clearest for JSON-like nested structures
def flatten(nested):
    result = []
    for item in nested:
        if isinstance(item, list):
            result.extend(flatten(item))  # recurse on sub-list
        else:
            result.append(item)
    return result

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

Quando a Iteração é Claramente Melhor

A iteração é a escolha certa quando: a profundidade é O(n) e n é grande (mais de aproximadamente 500 em um código Python seguro), as versões recursiva e iterativa são igualmente legíveis (Fibonacci, fatorial) ou o problema é fundamentalmente sequencial, sem uma decomposição natural em subproblemas. Laços simples que processam vetores da esquerda para a direita — somas acumuladas, janelas deslizantes e dois ponteiros — devem sempre ser iterativos.

# Iterative is clearest for sequential array processing
def running_max(nums):
    result = []
    curr_max = float('-inf')
    for n in nums:
        curr_max = max(curr_max, n)
        result.append(curr_max)
    return result

print(running_max([3, 1, 4, 1, 5, 9, 2, 6]))  # [3,3,4,4,5,9,9,9]

# No natural recursion here — iteration is the only sensible choice

Convertendo a Recursão de DFS em Iteração

Uma abordagem sistemática: toda DFS recursiva pode se tornar iterativa ao colocar os argumentos recursivos em uma pilha explícita. A ideia principal é que a chamada recursiva f(args) equivale a colocar args na pilha e executar um laço. Para o processamento em pós-ordem (quando são necessários os resultados dos filhos antes do pai), pode ser necessária uma abordagem em duas passagens ou um sinalizador de visitado.

# Post-order iterative using two stacks
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val=val; self.left=left; self.right=right

def postorder_iter(root):
    if not root: return []
    s1, s2 = [root], []
    while s1:
        node = s1.pop()
        s2.append(node.val)
        if node.left:  s1.append(node.left)
        if node.right: s1.append(node.right)
    return s2[::-1]  # reverse gives post-order

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

Sobrecarga de Desempenho da Recursão

Cada chamada recursiva em Python tem uma sobrecarga significativa: um novo quadro é criado (alocando memória na memória dinâmica), as variáveis locais são inicializadas e um ponteiro para o endereço de retorno é armazenado. Testes de desempenho mostram que a sobrecarga de uma chamada de função em Python é de aproximadamente 100–200 nanossegundos por chamada. Para uma profundidade de recursão de 10^6, isso totaliza de 0,1 a 0,2 segundo de sobrecarga pura, independentemente do trabalho do algoritmo. Laços iterativos evitam completamente essa sobrecarga.

import time

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

def iter_sum(n):
    total = 0
    for i in range(n + 1):
        total += i
    return total

import sys; sys.setrecursionlimit(10000)

n = 5000
start = time.time()
for _ in range(100): rec_sum(n)
print(f'Recursive sum({n}) x100: {(time.time()-start)*1000:.2f}ms')

start = time.time()
for _ in range(100): iter_sum(n)
print(f'Iterative sum({n}) x100: {(time.time()-start)*1000:.2f}ms')

Tomando uma Decisão em uma Entrevista

Em uma entrevista de programação, se você tiver escolha, pergunte: 'A profundidade da recursão é limitada por O(log n)?' Se sim, a recursão é adequada. 'A profundidade da recursão é O(n)?' — prefira a iteração ou mencione que a converteria para uma versão iterativa em produção. 'O problema tem naturalmente a forma de uma árvore ou envolve divisão e conquista?' — opte pela recursão. 'O problema é uma varredura sequencial?' — use a iteração.

Sempre explique seu raciocínio: 'Usarei a recursão aqui porque a profundidade é O(log n) para uma BST balanceada, portanto o espaço de pilha O(log n) é aceitável.'

Resumo: Tabela de Compromissos

Resumindo os compromissos: o código recursivo costuma ser mais curto e refletir a estrutura do problema, mas custa O(profundidade) de espaço na pilha e tem sobrecarga de chamadas de função. O código iterativo é mais longo, mas usa espaço O(1) na pilha e evita os limites da recursão. A recursão com memoização (na próxima lição) é um meio-termo: preserva a clareza da recursão e elimina o recálculo redundante. Sempre seja explícito sobre a complexidade espacial, incluindo o espaço da pilha de chamadas, ao analisar sua solução.

rows = [
    ('Factorial',   'O(n) / O(1)', 'O(n) / O(1)', 'Same time; iter wins on space'),
    ('Fibonacci',   'O(2^n) / O(n)', 'O(n) / O(1)', 'Iter massively wins'),
    ('Binary search','O(log n) / O(log n)', 'O(log n) / O(1)', 'Iter wins on space'),
    ('Tree DFS',    'O(n) / O(h)',  'O(n) / O(h)', 'Equal; rec cleaner'),
    ('Merge sort',  'O(n log n) / O(log n)', 'O(n log n) / O(1)', 'BU-iter wins on stack'),
]
for name, rec, it, note in rows:
    print(f'{name:<15} rec={rec:<22} iter={it:<22} {note}')

Verificação Rápida

Verifique sua compreensão dos conceitos de Estruturas de Dados e Algoritmos — Preparação para Entrevistas de Programação desta lição.

Recapitulação da Lição

Nesta lição, você aprendeu: a recursão é preferível quando a profundidade é O(log n) ou quando o problema tem naturalmente a forma de uma árvore; a iteração é preferível quando a profundidade é O(n) ou quando o problema é sequencial, o Fibonacci recursivo ingênuo tem complexidade O(2^n) — a versão iterativa tem tempo O(n) e espaço O(1) e qualquer DFS recursiva pode ser convertida em iterativa ao gerenciar uma pilha explícita na memória dinâmica. A seguir, aplicaremos a memoização para eliminar chamadas recursivas redundantes.

Perguntas Frequentes

A aula “Compromissos entre Recursão e Iteração” é grátis?

Sim — o texto completo de “Compromissos entre Recursão e Iteração” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA Interview Prep inclui 4 aulas no total.

O que vou aprender em “Compromissos entre Recursão e Iteração”?

Converta fatorial e Fibonacci recursivos em laços iterativos e explique quando o limite de recursão e o tamanho da pilha do Python tornam a iteração preferível. Você pratica DSA Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar DSA Interview Prep?

Nenhuma experiência prévia é necessária. DSA Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 3 de 4.

Quanto tempo leva a aula “Compromissos entre Recursão e Iteração”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de DSA Interview Prep?

Sim. Cada aula de DSA Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Estrutura da Recursão: Caso Base, Confiança, Construção
  2. Visualizando a Pilha de Chamadas
  3. Compromissos entre Recursão e Iteração
  4. Memoização: Armazenando Resultados Recursivos em Cache
← Voltar para DSA Interview Prep