Durante a aula de hoje, mergulhamos no conceito de recursão – uma técnica fundamental na ciência da computação onde uma função chama a si mesma para resolver versões menores de um problema, até atingir uma condição de parada. A recursão aparece em algoritmos de ordenação, travessia de árvores, problemas combinatórios e até mesmo na definição de certas estruturas de dados. Vou registrar aqui os principais pontos discutidos.
1. O que é recursão?
Uma função recursiva é definida em termos de si mesma. A ideia central é quebrar um problema maior em subproblemas mais simples (passo recursivo) e parar quando o problema se torna trivial (caso base). Sem um caso base, a função continuaria chamando a si mesma indefinidamente, causando um estouro de pilha (stack overflow).
Exemplo simples (contagem regressiva em Python):
def contagem(n):
if n == 0: # caso base
print("Fim!")
else:
print(n)
contagem(n - 1) # passo recursivo
A cada chamada, o problema se aproxima do caso base. Essa estrutura é a marca registrada de qualquer função recursiva bem formada.
2. Pilha de chamadas e profundidade
Quando uma função recursiva executa, cada chamada é empilhada na pilha de chamadas (call stack). O sistema operacional aloca um quadro de pilha para armazenar variáveis locais e o endereço de retorno. Se a recursão for muito profunda (milhares de chamadas), a pilha pode transbordar – é o famoso RecursionError em Python ou StackOverflowException em outras linguagens.
A profundidade máxima depende da linguagem e do ambiente. Em Python, o limite padrão é 1000 (ajustável com sys.setrecursionlimit). Em linguagens compiladas como C, a pilha é fixa e estourá-la pode causar um crash. Por isso, é importante conhecer o tamanho do problema antes de optar pela recursão.
3. Exemplos clássicos
Vimos três exemplos que a maioria dos cursos apresenta:
3.1 Fatorial
def fatorial(n):
if n == 0 or n == 1:
return 1
return n * fatorial(n - 1)
Matematicamente, n! = n × (n‑1)!. A recursão segue naturalmente a definição.
3.2 Sequência de Fibonacci
def fib(n):
if n == 0:
return 0
if n == 1:
return 1
return fib(n - 1) + fib(n - 2)
Embora simples, essa implementação tem complexidade exponencial – cada chamada gera duas novas chamadas. Mostra que recursão nem sempre é a melhor solução; para Fibonacci, iteração ou programação dinâmica são mais eficientes.
3.3 Torres de Hanói
def hanoi(n, origem, destino, auxiliar):
if n == 1:
print(f"Mova o disco 1 de {origem} para {destino}")
else:
hanoi(n-1, origem, auxiliar, destino)
print(f"Mova o disco {n} de {origem} para {destino}")
hanoi(n-1, auxiliar, destino, origem)
Esse problema ilustra perfeitamente a quebra recursiva: mover n-1 discos para o pino auxiliar, mover o maior disco para o destino e depois mover os n-1 discos do auxiliar para o destino.
4. Recursão versus Iteração
Recursão geralmente produz código mais legível e mais próximo da definição matemática. Por outro lado, pode consumir mais memória (pilha) e ser mais lenta devido ao overhead de chamadas de função. A iteração (for, while) usa apenas variáveis locais, sem empilhamento excessivo.
Existem técnicas para mitigar as desvantagens:
- Recursão de cauda (tail recursion): quando a chamada recursiva é a última operação, alguns compiladores otimizam para iteração (eliminam a pilha).
- Memoização: armazenar resultados de chamadas anteriores para evitar recomputação (ex.: Fibonacci com dicionário).
A escolha entre recursão e iteração depende do problema, da linguagem e da legibilidade desejada.
5. Dicas para escrever funções recursivas
Com base no que discutimos em sala, listo um passo‑a‑passo que ajuda a construir recursões corretas:
- Identifique o caso base: qual a menor instância do problema? Quando ela pode ser resolvida trivialmente?
- Defina o passo recursivo: como reduzir o problema atual em direção ao caso base? A chamada recursiva deve trabalhar com uma entrada menor.
- Garanta progresso: a cada chamada, a entrada precisa se aproximar do caso base. Caso contrário, a recursão será infinita.
- Teste com casos pequenos: simule manualmente com valores pequenos para verificar se a lógica está correta.
- Cuidado com profundidade: se o problema exigir muitas chamadas aninhadas, prefira a versão iterativa ou aplique otimizações.
6. Recursão indireta
Um tópico interessante que surgiu foi a recursão indireta (ou mútua), onde duas ou mais funções se chamam entre si:
def par(n):
if n == 0:
return True
return impar(n - 1)
def impar(n):
if n == 0:
return False
return par(n - 1)
Embora menos comum, é útil em certos analisadores sintáticos (parsers) e na implementação de máquinas de estado.
Resumo dos pontos principais
- Recursão resolve problemas dividindo‑os em subproblemas idênticos ao original, porém menores.
- Toda função recursiva precisa de um caso base e de um passo recursivo que avance em direção a ele.
- A pilha de chamadas limita a profundidade prática da recursão.
- Recursão de cauda e memoização podem melhorar o desempenho.
- Nem sempre a recursão é a melhor escolha – iteração pode ser mais rápida e econômica.
Perguntas frequentes (FAQ)
Quando devo usar recursão em vez de iteração?
Use recursão quando a definição natural do problema for recursiva (árvores, grafos, expressões matemáticas) e a profundidade de chamadas for pequena. Para loops simples ou quando o número de iterações é grande, a iteração é mais segura e eficiente.
O que é tail recursion e por que ela é importante?
Tail recursion ocorre quando a chamada recursiva é a última operação executada pela função. Linguagens com otimização de tail call convertem a recursão em iteração, reutilizando o mesmo quadro de pilha, evitando estouro. Python não implementa essa otimização nativamente, mas linguagens como Scheme e Lua o fazem.
Recursão pode causar vazamento de memória?
Não exatamente, mas cada chamada consome memória na pilha. Se a profundidade for excessiva, o programa pode travar. Após a função retornar, os quadros de pilha são liberados, portanto não há vazamento. Ferramentas como sys.getrecursionlimit() ajudam a monitorar o limite.