Recursão
Funções que chamam a si mesmas. Caso base, passo recursivo, o caminho de ida e de volta na pilha de chamadas e exemplos com números e vetores.
"To understand recursion, we must first understand recursion." (Anônimo)
Recursão é um método de resolver um problema dividindo-o em instâncias menores do mesmo problema. As soluções dos problemas menores são então combinadas para obter a solução do problema original. Em programação, isso acontece quando uma função chama a si mesma.
Pense em uma matriosca, a boneca russa que guarda outra boneca dentro de si, que guarda outra, e assim por diante. Para encontrar a menor boneca, abrimos a maior; se dentro dela houver outra, abrimos essa também, e repetimos até chegar a uma boneca que não abre. A recursão funciona da mesma forma: cada chamada "abre" um problema um pouco menor, até chegar a um problema tão simples que se resolve diretamente.
Uma função pode chamar a si mesma?
Antes de escrever código, pense nas perguntas abaixo. Tente responder antes de abrir cada resposta.
Uma função pode chamar qualquer outra função já definida no programa?
Sim, é o que fazemos o tempo todo: o main chama printf, uma função chama
outra auxiliar. Chamar uma função é pedir que outro bloco de código seja
executado e, quando ele terminar, voltar para onde parou.
E uma função pode chamar a si própria? Alguma regra do C proíbe isso?
Pode, e nenhuma regra proíbe. Para o compilador, uma função que chama a si mesma não é diferente de uma função que chama outra qualquer. Essa é a definição de recursão: uma função que se chama, direta ou indiretamente.
Se a função chama a si mesma, e essa chamada chama de novo, e de novo... o que impede isso de continuar para sempre?
Nada, a menos que a própria função pare de se chamar em algum momento. É preciso uma condição que interrompa as novas chamadas. Sem ela, as chamadas se acumulam até a memória reservada para elas acabar (estouro de pilha). Essa condição de parada é o caso base.
A chamada "nova" enxerga e pode alterar as variáveis locais da chamada "antiga"?
Não. Cada chamada, recursiva ou não, recebe seus próprios parâmetros e variáveis locais, guardados na pilha de execução. As variáveis de chamadas diferentes são independentes, como se fossem funções diferentes (a não ser que sejam globais ou passadas por ponteiro).
Caso base e passo recursivo
Toda função recursiva tem dois ingredientes:
- Caso base: a versão mais simples do problema, resolvida diretamente, sem nova chamada recursiva. É o que garante que a recursão termina.
- Passo recursivo (ou etapa de redução): a função resolve uma versão menor do mesmo problema, chamando a si mesma, e usa esse resultado para resolver o problema original.
Se o problema atual é simples, resolva-o diretamente (caso base). Caso contrário, reduza-o a um problema menor do mesmo tipo e aplique a mesma estratégia (passo recursivo).
Sem caso base, ou com um caso base que o passo recursivo nunca alcança, temos uma recursão infinita, o equivalente recursivo de um laço infinito.

Fatorial
O fatorial de um inteiro
A definição já é recursiva: o fatorial de
int fatorial(int n) {
return n * fatorial(n - 1); /* passo recursivo */
}Essa versão nunca para: fatorial(0) chamaria fatorial(-1), que chamaria
fatorial(-2)... Falta o caso base, o problema mais simples, que sabemos
resolver diretamente:
A função ainda tem problemas que não estão sendo tratados. Consegue identificá-los? (Teste com as entradas -3 e 13.)
- Números negativos: com
n = -3, o passo recursivo chamafatorial(-4),fatorial(-5), ..., e o caso basen == 0nunca é alcançado. As chamadas se acumulam até a pilha estourar (stack overflow). Uma solução é tratarn <= 0como caso base, ou rejeitar valores negativos antes de chamar. - Overflow:
não cabe em um int(o maiorinté). Com long long, dá para ir até.
Ida e volta
Este é o ponto central para entender recursão. Uma função recursiva tem um caminho de ida, em que as chamadas se empilham, uma dentro da outra, e um caminho de volta, em que as chamadas terminam na ordem inversa, à medida que a pilha é desempilhada.
- 1
Código antes da chamada#
Execute como está e observe a saída:
5 4 3 2 1 0. Oprintfvem antes da chamada recursiva, então executa na ida. - 2
Código depois da chamada#
Comente o primeiro
printfe descomente o segundo. Antes de executar, preveja a saída. Agora ela é0 1 2 3 4 5: oprintfvem depois da chamada, então executa na volta. - 3
Antes e depois#
Deixe os dois
printfativos. Antes de executar, escreva a saída que você espera e depois confira.
Cada chamada de imprime(n) fica pausada, esperando imprime(n - 1)
terminar, antes de continuar o que vem depois dela. O código antes da
chamada recursiva executa na ida (5, 4, 3, ...); o código depois executa
na volta, na ordem inversa (0, 1, 2, ...).
No fatorial, a multiplicação n * fatorial(n - 1) só pode ser feita depois
que fatorial(n - 1) devolve seu valor. Por isso, na ida, as chamadas apenas
se empilham; as multiplicações acontecem todas na volta:
O programa abaixo mostra o mesmo processo, com um recuo por nível de recursão:
Os parâmetros e as variáveis locais de cada chamada ficam num quadro próprio
na pilha de execução. No rastreio acima, os cinco n e os cinco
resultado existem ao mesmo tempo, cada um na sua chamada, sem que um
interfira no outro.
Exemplos
Antes de olhar cada solução, tente responder:
- O que a função precisa receber para funcionar (quais parâmetros)?
- O que ela deve devolver?
- Qual é o caso mais simples (caso base)? Como reduzir o problema a um menor (passo recursivo)?
Soma de 1 a n
É quase o mesmo problema do fatorial, mas somando em vez de multiplicar:
Com a definição matemática em mãos, o código é direto:
int soma(int n) {
if (n == 0) /* caso base */
return 0;
else /* passo recursivo */
return n + soma(n - 1);
}São feitas
Soma dos pares de 2 até n
Para
int f(int n) {
if (n == 2) /* caso base */
return 2;
else if (n % 2 == 1) /* ímpar: não soma */
return f(n - 1);
else /* par: soma */
return n + f(n - 1);
}Quando n é par, precisamos chamar f(n - 1)? Há uma forma mais eficiente?
Se f(n - 1) só serve para chamar
f(n - 2). Podemos ir direto: return n + f(n - 2);. A função continua
correta para qualquer
Quantidade de dígitos
O caso base é um número com um único dígito (
int contaDigitos(int n) {
if (n < 10) /* caso base */
return 1;
else /* passo recursivo */
return 1 + contaDigitos(n / 10);
}Cada chamada divide
- A função supõe
. Como adaptá-la para aceitar negativos? - Como somar os dígitos em vez de contá-los? Por exemplo,
somaDigitos(123)deveria devolver . (Esse é um dos exercícios no fim da página.)
Encontre o erro
A função somaDigitos abaixo deveria devolver 10 para 1234. Antes de
executar, tente prever o que ela devolve.
Resposta
O passo recursivo chama contaDigitos(n / 10) em vez de somaDigitos(n / 10):
a função soma o último dígito (4) com a quantidade de dígitos do resto
(3), e devolve 7. O compilador não acusa nada, porque as duas funções têm a
mesma assinatura. Corrija para return n % 10 + somaDigitos(n / 10);.
Qual é o mistério?
O que a função abaixo calcula?
int misterio(int n) {
if (n == 1)
return 0;
else
return 1 + misterio(n / 2);
}Resposta
A melhor estratégia é o teste de mesa: executar a função à mão para alguns valores.
misterio(4) = 1 + misterio(2) = 1 + 1 + misterio(1) = 2misterio(6) = 1 + misterio(3) = 1 + 1 + misterio(1) = 2misterio(8) = 1 + misterio(4) = 3misterio(10) = 1 + misterio(5) = 1 + 1 + misterio(2) = 3misterio(16) = 1 + misterio(8) = 4misterio(20) = 1 + misterio(10) = 4
A função conta quantas vezes dá para dividir
Recursão com vetores
Procurar um valor y no vetor x de n elementos, devolvendo seu índice
ou
int procura(int x[], int y, int n) {
if (n == 0) /* vetor vazio: não está */
return -1;
if (y == x[n - 1]) /* está na última posição */
return n - 1;
return procura(x, y, n - 1); /* procura nos n - 1 primeiros */
}Imprimir os elementos do vetor:
void escreve(int x[], int n) {
if (n == 0)
return;
printf("%d ", x[0]);
escreve(x + 1, n - 1);
}Por que a chamada recursiva é escreve(x + 1, n - 1), e não escreve(x, n - 1)?
x + 1 é o endereço do segundo elemento (aritmética de ponteiros): a chamada
recursiva recebe o "resto" do vetor, que começa uma posição à frente e tem um
elemento a menos. Com escreve(x, n - 1), todas as chamadas imprimiriam
x[0].
Somar os elementos do vetor, em duas versões:
int soma(int x[], int n) {
if (n == 0)
return 0;
else
return x[0] + soma(x + 1, n - 1); /* primeiro + soma do resto */
}A segunda versão tem um erro. Execute e leia a mensagem:
Resposta
Um vetor com n elementos tem índices de 0 a n - 1: x[n] está fora
dos limites já na primeira chamada (e x[0] nunca é somado). O correto é
return x[n - 1] + soma(x, n - 1);. Corrigida, ela soma do último elemento
para o primeiro; a primeira versão soma do primeiro para o último.
Fibonacci: duas chamadas recursivas
A sequência de Fibonacci começa com 0 e 1, e cada termo seguinte é a soma dos
dois anteriores:
int fib(int n) {
if (n == 0 || n == 1) /* caso base */
return n;
return fib(n - 1) + fib(n - 2);
}Diferente dos exemplos anteriores, o passo recursivo faz duas chamadas. As chamadas deixam de formar uma linha e passam a formar uma árvore, e os mesmos valores são calculados várias vezes, de forma independente:
O programa conta as chamadas para valores crescentes de
A cada 5 unidades de
Toda função recursiva pode ser reescrita com laços, e vice-versa. A recursão deixa muitos algoritmos mais simples de escrever e de entender, mas cada chamada ocupa espaço na pilha e, como no Fibonacci, uma recursão ingênua pode repetir muito trabalho.
Recursão e a pilha
Cada chamada ainda não terminada ocupa um quadro na pilha de execução. Uma recursão muito profunda (ou infinita) esgota a pilha e o programa termina com um estouro de pilha, o mesmo problema de um vetor local grande demais (veja Alocação de Memória). Por isso, além do tempo, vale perguntar quantas chamadas ficam empilhadas ao mesmo tempo.
| Função | Chamadas | Profundidade máxima da pilha | Tempo |
|---|---|---|---|
fatorial(n), soma(n) | |||
contaDigitos(n) | número de dígitos | número de dígitos | |
misterio(n) | |||
procura(x, y, n) | até | até | |
fib(n) | cerca de | exponencial |
Exercícios
Encontre e corrija o erro
Cada programa tem um erro. Execute, leia a mensagem (inclusive os avisos do compilador), corrija e execute de novo.
Programa 1
Resposta
O passo recursivo reduz o expoente de 2 em 2. Com expoente ímpar, ele passa
de 1 para exp == 0: recursão infinita e
estouro de pilha. Uma correção é tratar o ímpar à parte:
if (exp % 2 == 1)
return base * potencia(base, exp - 1);Programa 2
Resposta
Quando v[n - 1] não é maior que m, a função termina sem return. O valor
devolvido é indefinido (o compilador avisa: control reaches end of non-void
function). Falta return m; no fim. Leve sempre a sério os avisos do
compilador.
Programa 3
Volte ao programa da soma dos dígitos e ao da soma do vetor, se ainda não os corrigiu.
Rastreie
O que a função abaixo imprime para f(13)? E para f(8)?
void f(int n) {
if (n == 0)
return;
f(n / 2);
printf("%d", n % 2);
}Resposta
As chamadas vão de f(13) a f(6), f(3), f(1) e f(0). Como o printf
vem depois da chamada, os restos são impressos na volta, do último para o
primeiro: 1, 1, 0, 1, ou seja, 1101. Para f(8), imprime 1000. A
função imprime
Implemente
Soma dos dígitos. Implemente somaDigitos de forma recursiva.
Palíndromo. Uma palavra é um palíndromo se for igual lida da esquerda
para a direita e da direita para a esquerda (como arara e osso).
Implemente palindromo(s, ini, fim), que verifica o trecho s[ini..fim]:
compare as pontas e, se forem iguais, verifique recursivamente o trecho sem
elas.
Potência rápida. Calcular
Como o expoente cai pela metade a cada chamada (como na
busca binária), são cerca de
long long.
Referências
- P. Feofiloff. Recursão e algoritmos recursivos. IME-USP.
- Recursividade. Wikipedia.
- Recursion basics. Hyperskill.
Valgrind e Sanitizers
Como detectar erros de memória e comportamento indefinido em C com o Valgrind e os sanitizers do GCC e do Clang.
Backtracking
Busca exaustiva, poda e backtracking. Gerar todas as possibilidades, cortar cedo as que não servem e desfazer escolhas, com o labirinto como estudo de caso.