Aula 16 — Recursão

Funções que chamam a si mesmas — caso base, caso recursivo e a pilha de chamadas

1. O que é Recursão?

Até agora todas as funções que escrevemos chamavam outras funções. Recursão é a técnica em que uma função chama a si mesma para resolver uma versão menor do mesmo problema, repetindo esse processo até chegar a um caso simples que pode ser resolvido diretamente.

Recursão é uma técnica em que uma função resolve um problema chamando a si mesma com uma versão menor ou mais simples do mesmo problema. Toda função recursiva deve ter obrigatoriamente dois elementos: um caso base (que encerra a recursão) e um caso recursivo (que avança em direção ao caso base).

A estrutura obrigatória de toda função recursiva

tipo funcaoRecursiva(parametros) {

    /* CASO BASE — condição de parada: resolve sem chamar a si mesma */
    if (condicaoDeParada)
        return valorSimples;

    /* CASO RECURSIVO — chama a si mesma com problema menor */
    return funcaoRecursiva(parametrosMenores);
}
⚠️ Sem caso base = stack overflow. Se uma função recursiva nunca atingir a condição de parada, ela se chamará infinitamente até esgotar a memória da pilha — causando um stack overflow e encerrando o programa com erro.

2. Primeiro Exemplo: Contagem Regressiva

Antes de exemplos matemáticos, vamos ver recursão da forma mais visual possível: uma contagem regressiva.

#include <stdio.h>

void contagem(int n) {
    /* Caso base: quando n chega a 0, encerra */
    if (n == 0) {
        printf("Fim!\n");
        return;
    }

    /* Caso recursivo: imprime n e chama com n-1 */
    printf("%d...\n", n);
    contagem(n - 1);
}

int main() {
    contagem(5);
    return 0;
}
5...
4...
3...
2...
1...
Fim!

O que acontece passo a passo

Chamada n Ação Próxima chamada
1ª5Imprime "5..."contagem(4)
2ª4Imprime "4..."contagem(3)
3ª3Imprime "3..."contagem(2)
4ª2Imprime "2..."contagem(1)
5ª1Imprime "1..."contagem(0)
6ª0Imprime "Fim!" e retorna— caso base atingido
Antes vs. depois da chamada recursiva: O printf aqui vem antes da chamada recursiva — por isso os números aparecem em ordem decrescente (5, 4, 3...). Se você movesse o printf para depois da chamada, a ordem seria crescente (1, 2, 3...) porque os printf só executariam ao retornar de cada nível. Experimente!

3. A Pilha de Chamadas (Call Stack)

Cada chamada de função reserva um espaço na pilha de memória (stack) para armazenar seus parâmetros e variáveis locais. Com recursão, várias chamadas ficam empilhadas ao mesmo tempo, aguardando umas às outras:

Estado da pilha durante contagem(3)
contagem(0) ← topo — em execução (caso base)
contagem(1) ← aguardando retorno
contagem(2) ← aguardando retorno
contagem(3) ← aguardando retorno
main() ← base da pilha

Após o caso base retornar, cada chamada desempilha em ordem inversa: contagem(0) retorna para contagem(1), que retorna para contagem(2), e assim por diante até voltar ao main.

Stack overflow: A pilha tem tamanho limitado (tipicamente 1–8 MB). Uma recursão muito profunda — ou sem caso base — empilha tantas chamadas que esgota esse espaço, causando encerramento abrupto do programa.

4. Fatorial — O Exemplo Clássico

O fatorial de um número inteiro positivo n é o produto de todos os inteiros de 1 até n. A definição matemática já é naturalmente recursiva:

n! = n × (n−1)!      com caso base      0! = 1
#include <stdio.h>

long fatorial(int n) {
    /* Caso base */
    if (n == 0) return 1;

    /* Caso recursivo: n! = n × (n-1)! */
    return n * fatorial(n - 1);
}

int main() {
    for (int i = 0; i <= 10; i++)
        printf("%2d! = %ld\n", i, fatorial(i));
    return 0;
}
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800

Expandindo fatorial(4) passo a passo

ChamadaCálculoRetorna
fatorial(4)4 × fatorial(3)aguarda...
fatorial(3)3 × fatorial(2)aguarda...
fatorial(2)2 × fatorial(1)aguarda...
fatorial(1)1 × fatorial(0)aguarda...
fatorial(0)caso base!1
↩ fatorial(1)1 × 11
↩ fatorial(2)2 × 12
↩ fatorial(3)3 × 26
↩ fatorial(4)4 × 624

5. Fibonacci — Dois Casos Base

A sequência de Fibonacci começa com 0 e 1, e cada número seguinte é a soma dos dois anteriores: 0, 1, 1, 2, 3, 5, 8, 13, 21... É um exemplo com dois casos base e duas chamadas recursivas:

fib(0) = 0     fib(1) = 1     fib(n) = fib(n−1) + fib(n−2)
#include <stdio.h>

long fib(int n) {
    /* Dois casos base */
    if (n == 0) return 0;
    if (n == 1) return 1;

    /* Caso recursivo */
    return fib(n - 1) + fib(n - 2);
}

int main() {
    for (int i = 0; i <= 12; i++)
        printf("fib(%2d) = %ld\n", i, fib(i));
    return 0;
}
fib( 0) = 0
fib( 1) = 1
fib( 2) = 1
fib( 3) = 2
fib( 4) = 3
fib( 5) = 5
fib( 6) = 8
fib( 7) = 13
fib( 8) = 21
fib( 9) = 34
fib(10) = 55
fib(11) = 89
fib(12) = 144

Árvore de chamadas de fib(4) — o problema do retrabalho

                    fib(4)
                   /      \
              fib(3)      fib(2)
             /     \      /    \
         fib(2)  fib(1) fib(1) fib(0)
         /    \
      fib(1) fib(0)
⚠️ Fibonacci recursivo é ineficiente. fib(2) é calculada duas vezes, fib(1) três vezes. Para fib(40) o número de chamadas passa de 300 milhões. Em aplicações reais, use a versão iterativa ou a técnica de memoização (guardar resultados já calculados). Aqui o objetivo é apenas ilustrar a estrutura recursiva com dois casos base.

6. Outros Exemplos Práticos

6.1 — Soma dos elementos de um vetor

#include <stdio.h>

int somaVetor(int v[], int n) {
    if (n == 0) return 0;                       // vetor vazio: soma é 0
    return v[0] + somaVetor(v + 1, n - 1);     // primeiro + soma do resto
}

int main() {
    int notas[] = {7, 8, 6, 9, 5};
    int total = somaVetor(notas, 5);
    printf("Soma: %d | Media: %.1f\n", total, total / 5.0f);
    return 0;
}
Soma: 35 | Media: 7.0

6.2 — Potência (base elevada ao expoente)

#include <stdio.h>

double potencia(double base, int exp) {
    if (exp == 0) return 1.0;                   // base^0 = 1
    return base * potencia(base, exp - 1);      // base^n = base × base^(n-1)
}

int main() {
    printf("2^10  = %.0f\n",  potencia(2,   10));
    printf("3^5   = %.0f\n",  potencia(3,    5));
    printf("1.5^4 = %.4f\n",  potencia(1.5,  4));
    return 0;
}
2^10 = 1024
3^5 = 243
1.5^4 = 5.0625

6.3 — Maior Divisor Comum (algoritmo de Euclides)

Um dos algoritmos mais antigos da matemática — e um dos exemplos mais elegantes de recursão. A ideia: mdc(a, b) = mdc(b, a % b), com caso base mdc(a, 0) = a.

#include <stdio.h>

int mdc(int a, int b) {
    if (b == 0) return a;           // caso base: MDC(a, 0) = a
    return mdc(b, a % b);           // caso recursivo
}

int main() {
    printf("MDC(48, 18)  = %d\n", mdc(48, 18));   // 6
    printf("MDC(100, 75) = %d\n", mdc(100, 75));  // 25
    printf("MDC(17, 5)   = %d\n", mdc(17,  5));   // 1 — primos entre si
    return 0;
}
MDC(48, 18) = 6
MDC(100, 75) = 25
MDC(17, 5) = 1

Acompanhando mdc(48, 18) passo a passo

Chamadaaba % bPróxima
mdc(48, 18)481812mdc(18, 12)
mdc(18, 12)18126mdc(12, 6)
mdc(12, 6)1260mdc(6, 0)
mdc(6, 0)60—caso base! retorna 6

6.4 — Impressão dos dígitos de um número

#include <stdio.h>

void imprimirDigitos(int n) {
    if (n < 10) {                   // caso base: um único dígito
        printf("%d ", n);
        return;
    }
    imprimirDigitos(n / 10);        // imprime todos exceto o último
    printf("%d ", n % 10);          // depois imprime o último
}

int main() {
    printf("Digitos de 7483: ");
    imprimirDigitos(7483);
    printf("\n");
    return 0;
}
Digitos de 7483: 7 4 8 3

7. Recursão vs. Iteração — Quando Usar Cada Uma

Qualquer problema resolvível com recursão também pode ser resolvido com loops, e vice-versa. A escolha depende de qual deixa o código mais claro e eficiente para aquele problema específico.

Critério Recursão Iteração (loop)
Problemas naturalmente hierárquicos (árvores, grafos) ✅ Natural e elegante ❌ Exige pilha manual — muito complexo
Legibilidade para divisão em subproblemas ✅ Espelha a definição matemática ⚠️ Pode ser mais verboso
Uso de memória ⚠️ Cada chamada usa a stack ✅ Apenas as variáveis do loop
Risco de stack overflow ⚠️ Sim, em recursões profundas ✅ Não
Desempenho geral ⚠️ Overhead de chamadas de função ✅ Geralmente mais rápida
Problemas de sequência simples ⚠️ Funciona, mas é desnecessário ✅ Natural e direto

Fatorial: recursão vs. iteração lado a lado

Recursivo — espelha a definição
long fatorial(int n) {
    if (n == 0) return 1;
    return n * fatorial(n - 1);
}

✅ Idêntico à notação matemática.
⚠️ n chamadas empilhadas.

Iterativo — eficiente e seguro
long fatorial(int n) {
    long r = 1;
    for (int i = 2; i <= n; i++)
        r *= i;
    return r;
}

✅ Memória mínima (O(1)).
✅ Sem risco de stack overflow.

Regra prática: Use recursão quando o problema for naturalmente hierárquico ou se divide em subproblemas da mesma natureza (árvores, buscas, algoritmos de divisão). Use iteração para sequências simples — somar, buscar em array, converter unidades — onde o loop é igualmente claro e mais eficiente.

8. C vs. Java — Recursão

Aspecto C Java
Sintaxe da recursão Idêntica — função chama a si mesma Idêntica — método chama a si mesmo
Tamanho padrão da stack Tipicamente 1–8 MB (dependente do SO) 512 KB a 1 MB por thread (configurável com -Xss)
Stack overflow Falha com sinal do SO (segfault) Lança StackOverflowError (capturável)
Recursão mútua (A chama B, B chama A) Requer protótipos antecipados para A e B Métodos podem se chamar mutuamente sem declaração prévia

Side-by-side: fatorial recursivo

C
long fatorial(int n) {
    if (n == 0) return 1;
    return n * fatorial(n - 1);
}
Java
static long fatorial(int n) {
    if (n == 0) return 1;
    return n * fatorial(n - 1);
}

A estrutura é praticamente idêntica. A única diferença é que em Java o método precisa ser static para ser chamado sem instância de objeto, enquanto em C funções já são naturalmente estáticas no sentido de pertencer ao arquivo.

Quiz — Aula 16

1. O que é obrigatório em toda função recursiva para evitar um stack overflow?

2. Qual é a saída do código abaixo?

int f(int n) {
    if (n == 0) return 0;
    return n + f(n - 1);
}
int main() { printf("%d", f(4)); }

3. Ao calcular fatorial(5), quantas chamadas à função são feitas no total (incluindo a chamada inicial)?

4. O que acontece se uma função recursiva nunca atingir o caso base?

5. Qual é a saída do código abaixo?

void f(int n) {
    if (n == 0) return;
    f(n - 1);
    printf("%d ", n);
}
int main() { f(4); }

6. Por que a implementação recursiva de Fibonacci é considerada ineficiente?

7. Qual é o resultado de mdc(12, 8) usando o algoritmo de Euclides recursivo?

8. Em qual situação a recursão é claramente preferível à iteração?