Módulo 3 · Algoritmos e Estruturas de Dados — Capítulo 05

Recursividade

Uma função que resolve um problema chamando a si mesma para resolver uma versão menor do mesmo problema — e por que isso não é mágica, é a pilha de chamadas trabalhando por baixo.

1. O que é recursividade

Uma função é recursiva quando, na sua própria definição, ela chama a si mesma para resolver uma versão menor (ou mais simples) do problema original. Pode soar circular à primeira vista — "como uma função pode chamar a si mesma sem entrar num loop infinito?" — mas a resposta está numa peça obrigatória de toda função recursiva: o caso base.

2. O caso base é obrigatório

Toda função recursiva precisa de duas partes: o caso base, uma condição simples o suficiente para ser resolvida diretamente, sem nova chamada recursiva; e o caso recursivo, onde a função chama a si mesma com uma entrada menor, progressivamente mais próxima do caso base.

Atenção

Uma função recursiva sem caso base (ou com um caso base que nunca é alcançado, por exemplo porque a entrada não diminui a cada chamada) não é diferente de um ENQUANTO VERDADEIRO sem condição de parada: ela vai chamar a si mesma indefinidamente, até estourar a memória disponível para a pilha de chamadas — um erro comumente chamado de stack overflow.

3. Fatorial: o exemplo clássico

O fatorial de um número n (escrito n!) é o produto de todos os inteiros positivos de 1 até n. Por definição, o fatorial de 0 é 1. Essa definição já é naturalmente recursiva: n! = n × (n-1)!, com o caso base sendo 0! = 1.

PSEUDOCÓDIGO fatorial.txt
FUNÇÃO fatorial(n)
    SE n = 0 ENTÃO           // caso base
        RETORNE 1
    SENÃO
        RETORNE n × fatorial(n - 1)   // caso recursivo
    FIM SE
FIM FUNÇÃO

4. Traçando a pilha de chamadas

Para entender o que acontece de verdade ao chamar fatorial(4), vale lembrar do capítulo anterior: toda chamada de função é empilhada na pilha de chamadas (call stack) e só é removida (desempilhada) quando a função termina e retorna um valor. Recursão usa exatamente esse mecanismo — cada chamada recursiva empilha uma nova "instância" da função, esperando o resultado da chamada seguinte antes de poder terminar a própria conta.

descendo: cada chamada empilha, esperando o retorno da próxima fatorial(4) → 4 × fatorial(3) fatorial(3) → 3 × fatorial(2) fatorial(2) → 2 × fatorial(1) fatorial(1) → 1 × fatorial(0) fatorial(0) → 1 (caso base) fatorial(0) retorna 1 fatorial(1) retorna 1×1 = 1 fatorial(2) retorna 2×1 = 2 fatorial(3) retorna 3×2 = 6 fatorial(4) retorna 4×6 = 24 subindo: cada nível volta a conta pronta

Fig. 1 — Pilha de chamadas de fatorial(4): empilha até o caso base, depois desempilha multiplicando os resultados.

O ponto-chave: nenhuma multiplicação acontece de verdade até o caso base ser alcançado. A partir daí, as chamadas empilhadas vão sendo resolvidas — desempilhadas — de baixo para cima, cada uma completando sua própria conta com o resultado que recebeu da chamada seguinte.

5. Outro exemplo: soma de um vetor

Fatorial não é o único jeito de pensar recursivamente. Qualquer problema que possa ser quebrado em "resolver uma peça pequena + delegar o resto para uma versão menor do mesmo problema" é candidato a uma solução recursiva. Somar os elementos de um vetor é um bom segundo exemplo:

PSEUDOCÓDIGO soma-recursiva.txt
FUNÇÃO somaRecursiva(vetor, índice)
    SE índice = tamanho(vetor) ENTÃO   // caso base: vetor esgotado
        RETORNE 0
    SENÃO
        RETORNE vetor[índice] + somaRecursiva(vetor, índice + 1)
    FIM SE
FIM FUNÇÃO

// chamada inicial: somaRecursiva(vetor, 0)

O caso base aqui não é um número fixo como 0, é uma condição: "não sobrou mais nada para somar". Isso reforça a regra geral — o caso base é qualquer situação simples o bastante para ser respondida sem uma nova chamada recursiva, e cada chamada recursiva precisa se aproximar dele.

Nota

Fibonacci é outro exemplo clássico de recursão, definido como fib(n) = fib(n-1) + fib(n-2), com caso base fib(0) = 0 e fib(1) = 1. Vale notar que essa versão "ingênua" de Fibonacci recalcula os mesmos valores repetidamente e acaba sendo exponencial em complexidade — um lembrete de que recursão elegante nem sempre é recursão eficiente.

6. Recursão vs. iteração

Praticamente todo problema resolvido com recursão também pode ser resolvido com um laço (iteração), e vice-versa. A escolha entre as duas abordagens é um trade-off, não uma regra fixa:

PSEUDOCÓDIGO fatorial-recursivo.txt
FUNÇÃO fatorial(n)
    SE n = 0 ENTÃO
        RETORNE 1
    SENÃO
        RETORNE n × fatorial(n - 1)
    FIM SE
FIM FUNÇÃO
PSEUDOCÓDIGO fatorial-iterativo.txt
FUNÇÃO fatorial(n)
    resultado ← 1
    PARA i DE 1 ATÉ n
        resultado ← resultado × i
    FIM PARA
    RETORNE resultado
FIM FUNÇÃO
AspectoRecursãoIteração
LegibilidadeCostuma espelhar a definição matemática/conceitual do problema, ficando mais direta de lerPode exigir mais código para controlar estado manualmente
Uso de memóriaCada chamada ocupa espaço na pilha de chamadas — entrada muito grande pode causar stack overflowUsa memória constante independente do tamanho da entrada (normalmente)
Melhor paraEstruturas naturalmente recursivas: árvores, algumas divisões e conquistasLaços simples sobre coleções, quando performance de memória importa

Não existe resposta universal sobre qual é "melhor" — depende do problema. Estruturas de dados hierárquicas (o assunto do próximo capítulo, árvores) costumam ficar muito mais claras resolvidas com recursão do que com iteração pura. Já processar uma lista simples de valores, como no exemplo do fatorial, normalmente é resolvido de forma mais direta e econômica com iteração.

📌 Resumo do capítulo

  • Função recursiva: chama a si mesma para resolver uma versão menor do mesmo problema.
  • Toda recursão precisa de um caso base (condição de parada) e um caso recursivo que se aproxima dele.
  • Sem caso base alcançável, a recursão nunca termina e estoura a pilha de chamadas (stack overflow).
  • Cada chamada recursiva empilha uma nova instância na pilha de chamadas; o resultado só é calculado ao desempilhar, de trás para frente.
  • Fatorial e soma de vetor são exemplos clássicos de recursão simples, linear.
  • Todo problema recursivo pode (em teoria) ser reescrito com iteração, e vice-versa.
  • Recursão tende a ser mais legível para problemas naturalmente hierárquicos; iteração tende a usar menos memória.

✏️ Praticando

  1. Faça o trace manual (como na Fig. 1) de fatorial(5), mostrando cada nível empilhado e o valor retornado ao desempilhar.
  2. Escreva em pseudocódigo uma função recursiva contarAté(n) que imprime todos os números de 1 até n. Identifique claramente o caso base e o caso recursivo.
  3. Pegue a função somaRecursiva da seção 5 e reescreva-a de forma iterativa, com um laço PARA. Compare as duas versões.
  4. Escreva em pseudocódigo uma função recursiva que calcula a potência baseᵉˣᵖᵒᵉⁿᵗᵉ (dica: potência(base, expoente) = base × potência(base, expoente - 1), com caso base quando o expoente for 0).
  5. Explique com suas palavras por que uma função recursiva que soma os elementos de um vetor de 1 milhão de posições, chamando a si mesma uma vez por elemento, pode ser mais arriscada em termos de memória do que a versão iterativa equivalente.