Trilha 03 · Algoritmos e complexidade

Recursão e divisão e conquista

Uma função que chama a si mesma parece magia — mas é uma das ferramentas mais poderosas de programação. A recursão resolve problemas dividindo-os em versões menores do mesmo problema. Quando combinada com a estratégia de dividir e conquistar, produz alguns dos algoritmos mais eficientes que existem.

① Intuição

Bonecas russas e espelhos opostos

Recursão é como abrir uma Matryoshka (boneca russa): dentro de cada boneca há uma boneca menor, até chegar à menor de todas. Para abrir a grande, você abre a do meio, que abre a próxima, e assim por diante. O caso base é a menor boneca — a que não tem mais nada dentro.

A estratégia de dividir e conquistar vai além: divide o problema em partes independentes, resolve cada parte recursivamente, e depois combina os resultados. Merge sort divide a lista ao meio, ordena cada metade recursivamente, e combina — resultando em O(n log n), muito melhor que O(n²).

Stack overflow: cada chamada recursiva ocupa um frame na pilha de execução. Sem caso base, ou com recursão muito profunda, a pilha esgota e o programa trava. Python tem limite de ~1000 chamadas recursivas por padrão. Para recursões profundas, prefira a versão iterativa ou use sys.setrecursionlimit com cuidado.
② Visualização interativa

A árvore de recursão do fib(4)

Avance passo a passo e veja como as chamadas se empilham e os resultados se propagam de volta. Note quantas vezes fib(2) e fib(1) são recalculados.

fib(4)fib(3)fib(2)fib(1)fib(0)fib(1)fib(2)fib(1)fib(0)
Pilha de chamadas
fib(4)
ativa concluída
Chamamos fib(4). Ainda não sabemos o resultado — precisamos de fib(3) e fib(2).
1 / 14
③ Explicação técnica

Caso base, passo recursivo e memoização

# Toda recursão precisa de dois elementos:
# 1. Caso base: condição de parada (sem ela → stack overflow)
# 2. Passo recursivo: chama a si mesmo com entrada MENOR

def fatorial(n):
    if n <= 1: return 1           # caso base
    return n * fatorial(n - 1)    # passo recursivo

# fatorial(4):
# → 4 * fatorial(3)
# → 4 * (3 * fatorial(2))
# → 4 * (3 * (2 * fatorial(1)))
# → 4 * (3 * (2 * 1)) = 24

def fibonacci(n):
    if n <= 1: return n             # casos base: fib(0)=0, fib(1)=1
    return fibonacci(n-1) + fibonacci(n-2)  # O(2ⁿ) — muito lento!

# Fibonacci com memoização — O(n)
from functools import lru_cache

@lru_cache(maxsize=None)
def fib_memo(n):
    if n <= 1: return n
    return fib_memo(n-1) + fib_memo(n-2)  # agora O(n) — cada subproblema calculado 1×

Dividir e conquistar: Merge Sort

# Merge sort: divide e conquista clássico
# T(n) = 2 T(n/2) + O(n)  →  O(n log n)

def merge_sort(lista):
    if len(lista) <= 1:       # caso base: lista de 0 ou 1 elemento
        return lista

    meio = len(lista) // 2
    esq  = merge_sort(lista[:meio])    # divide
    dir_ = merge_sort(lista[meio:])    # divide
    return merge(esq, dir_)            # conquista (combina)

def merge(a, b):                       # O(n) — combina dois arrays ordenados
    result, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]: result.append(a[i]); i += 1
        else:            result.append(b[j]); j += 1
    return result + a[i:] + b[j:]

# Outros exemplos de dividir e conquistar:
# - Quicksort: pivô separa, ordena cada metade recursivamente
# - Binary search: divide ao meio a cada chamada
# - Algoritmo de Karatsuba: multiplicação de grandes números O(n^1.58)
Teorema mestre (para análise de recorrências): T(n) = a·T(n/b) + O(nᵈ). Se a < bᵈ → O(nᵈ); se a = bᵈ → O(nᵈ log n); se a > bᵈ → O(n^log_b(a)). Merge sort: a=2, b=2, d=1. Como 2 = 2¹ (a=bᵈ) → O(n log n). Esse teorema resolve a maioria das recorrências de dividir e conquistar sem precisar expandir a recursão manualmente.
④ Projeto para programar

Da recursão ingênua à eficiente

Mini projeto: implemente fibonacci recursivo ingênuo e meça o tempo para n=30, 35, 40. Adicione @lru_cache e meça de novo. Plote o número de chamadas de função (adicione um contador global) com e sem memoização. Calcule o ratio para ver a diferença exponencial.

Projeto principal: implemente o algoritmo de Karatsuba para multiplicar dois números grandes (como strings de dígitos). Ele multiplica n×n dígitos em O(n^1.585) em vez de O(n²). Compare com a multiplicação ingênua para números de 100, 500 e 1000 dígitos. Python usa Karatsuba internamente para inteiros grandes — você está re-implementando algo que a linguagem já faz!

Desafio extra: implemente o problema das Torres de Hanói recursivamente. Para n discos, prove que são necessários exatamente 2ⁿ-1 movimentos. Implemente também a versão iterativa usando uma pilha explícita para simular a pilha de chamadas — isso mostra que toda recursão pode ser convertida em iteração com uma pilha.

⑤ Exercícios rápidos

Teste sua intuição

O que acontece se uma função recursiva não tem caso base?
Qual é a complexidade de fibonacci recursivo SEM memoização?
O que caracteriza a estratégia de dividir e conquistar?
⑥ Aplicações no mundo real

Onde você encontra isso

🌳

Sistemas de arquivos e compiladores

Percorrer diretórios recursivamente (os.walk) é recursão natural: cada pasta contém pastas. Compiladores usam recursão para parsear expressões aninhadas: f(g(h(x))) é uma árvore onde cada nó chama o parser do filho. A gramática de linguagens de programação é intrinsecamente recursiva — um bloco if pode conter outro if.

🧬

Algoritmos de bioinformática

O alinhamento de sequências de DNA usa programação dinâmica — que é basicamente recursão com memoização. O algoritmo de Smith-Waterman (alinhamento local) computa T(i,j) a partir de T(i-1,j), T(i,j-1) e T(i-1,j-1). Com genomas de bilhões de bases, otimizar essa recursão é questão de horas vs dias de computação.

🖥️

Renderização de cenas 3D

Ray tracing (gráficos 3D fotorrealistas) é recursivo: um raio de luz bate em uma superfície espelhada, gera outro raio, que pode bater em outra superfície, e assim por diante. A profundidade de recursão determina a qualidade do reflexo — jogos limitam a ~5-10 bounces. O engine Lumen da Unreal Engine usa ray tracing com recursão limitada em tempo real via hardware especializado.

← Anterior: Ordenação Próxima: Estruturas de dados →