Recursão

Programação Estruturada

Victor S. Portella

UFABC

2026-10-09

Aviso rápido sobre flags de compilação

Recebendo warning de variáveis não inicializadas

Durante o laboratório 1, um colega teve problemas com a correção do CodeRunners

Depois de muito tempo, descobrimos que o problema eram variáveis não inicializadas

Mas, mesmo com as flags -Wall -Wextra não teve warning de variáveis não inicializadas???

Solução: Usar flag de otmização (-O1, por exemplo). Alguns warnings sobre variáveis não inicializadas dependem das análises feitas durante a otimização.

Recursão

Torres de Hanoi

Temos três pinos e uma pilha de discos de tamanhos diferentes.

Objetivo: Mover todos os discos do primeiro para o último pino.

Regras de Torres de Hanoi

Regras do jogo:

  • Podemos mover um disco por vez.
  • Só podemos mover o disco que está no topo de um pino.
  • Nunca podemos colocar um disco maior sobre um menor.

Como mover todos os discos para o último pino?

Aquecimento - Casos menores

1 disco: Fácil, só mover o disco.

2 discos: Guardamos primeiro o disco menor em uma torre auxiliar

Fonte: https://textbooks.cs.ksu.edu/cc210/images/16-recurse/6.9.tower.png

Como resolvemos o problema para 3 discos?

E para muitos discos?


Como escrever um programa que produza a sequência de movimentos para qualquer número de discos?

Resolver o problema diretamente parece muito difícil

Mas os problemas pequenos eram fáceis


Ideia: Quebrar o problema grande em problemas pequenos

Movendo o maior disco

Para mover \(n\) discos de A para C, em algum momento precisamos mover o maior disco até C.

Antes disso, precisamos tirar os outros \(n-1\) discos de cima dele.


Fonte: Slides do prof. Renzo

E se já soubéssemos mover \(n-1\) discos?

Queremos mover \(n\) discos de A para C, usando B como auxiliar.

  1. Mover os \(n-1\) discos menores de A para B.
  1. Mover o maior disco de A para C.
  1. Mover os \(n-1\) discos menores de B para C.


Mas como movemos os \(n-1\) discos?

Do mesmo jeito.

Recursão

Uma solução recursiva resolve um problema usando soluções de instâncias menores do mesmo problema.


Em programação, isso normalmente aparece como uma função que chama a si mesma.

Um exemplo mais simples

Fatorial

Fatorial de um inteiro não-negativo \(n\):

\[ n! = n \cdot (n-1) \cdot (n-2) \cdots 2 \cdot 1. \]


Por exemplo:

\[ 5! = 5\cdot4\cdot3\cdot2\cdot1 = 120. \]

Podemos escrever essa definição usando uma instância menor de fatorial?

Fatorial – definição recursiva

\(0! = 1\)

\(n!\) \(= n\,\cdot\) \((n-1)!\) , para \(n>0\).


  • \(0! = 1\): sabemos responder diretamente;
  • \((n-1)!\): é uma instância menor do mesmo problema.

Pensando como uma função

Suponha que a função fatorial(n - 1) já funciona corretamente.


Como podemos obter fatorial(n)?

return n * fatorial(n - 1);

Ao escrever a função, podemos confiar no resultado da chamada recursiva.


“Para fazer um procedimento recursivo é preciso ter fé.” – prof. Siang Wun Song

Mas precisamos parar!

O que acontece com esta função?

int fatorial(int n) {
    return n * fatorial(n - 1);
}
fatorial(3)
fatorial(2)
fatorial(1)
fatorial(0)
fatorial(-1)
fatorial(-2)
...

Eventualmente falta espaço na pilha de chamadas (stack overflow).

Precisamos de um caso base!

Fatorial recursivo

int fatorial(int n) {
    if (n == 0)
        return 1;

    return n * fatorial(n - 1);
}
  • n == 0: caso base;
  • fatorial(n - 1): problema menor.

Estamos supondo que a função será chamada com \(n \geq 0\).

Três ingredientes

Uma solução recursiva precisa de:

  1. um caso base, que sabemos resolver diretamente;
  2. um passo recursivo, que usa (pelo menos) uma instância menor do problema;
  3. progresso: as chamadas devem se aproximar do caso base.


O que acontece na execução?

Para fatorial(4):

fatorial(4)
    4 * fatorial(3)
        3 * fatorial(2)
            2 * fatorial(1)
                1 * fatorial(0)
                    1
fatorial(0) = 1
fatorial(1) = 1
fatorial(2) = 2
fatorial(3) = 6
fatorial(4) = 24

Pilha de chamadas

Cada chamada fica esperando a chamada seguinte terminar.

fatorial(4)    esperando fatorial(3)
fatorial(3)    esperando fatorial(2)
fatorial(2)    esperando fatorial(1)
fatorial(1)    esperando fatorial(0)
fatorial(0)    retorna 1

Essas chamadas formam uma pilha de chamadas (call stack).

Cada chamada possui seus próprios parâmetros e variáveis locais.

Voltando para Hanoi

O que queremos que a seguinte função faça?

void hanoi(int n, char origem,
           char destino, char auxiliar);


A chamada

hanoi(n, 'A', 'C', 'B');

promete:

Mover n discos de A para C, usando B como pino auxiliar.

Definir precisamente o que a função deve fazer usualmente é a maior parte do trabalho de resolver um problema usando recursão.

Qual é o caso base?

Qual é o menor problema que podemos resolver diretamente?

Se não há discos para mover, não precisamos fazer nada:

if (n == 0)
    return;

Caso base: n == 0.

Também poderíamos usar n == 1 como caso base. Usar n == 0 deixa o passo recursivo mais uniforme.

Primeiro: liberar o maior disco

Para mover \(n\) discos de origem para destino, primeiro precisamos mover os \(n-1\) menores para o pino auxiliar.

Os \(n-1\) discors menores podem usar os outros dois pinos normalemnte. Logo, movê-los para auxiliar é exatamente um problema de torres de Hanoi com \(n-1\) discos.

Tendo fé que a função funciona para \(n-1\) discos podemos chamar:

hanoi(n - 1, origem, auxiliar, destino);

Note as escolhas de Origem, Destino, e Auxiliar para essa insância menor:

Origem \(\leftarrow\) origem
Destino \(\leftarrow\) auxiliar
Auxiliar \(\leftarrow\) destino

Depois: mover o maior disco

Agora o maior disco está livre, logo:

printf("%c -> %c\n", origem, destino);


Fonte: slides do prof. Renzo

Por fim: mover os \(n-1\) discos de volta

Precisamos mover os \(n-1\) discos do pino auxiliar para destino.

hanoi(n - 1, auxiliar, destino, origem);


Novamente, estamos confiando que a função resolve corretamente o problema menor.

Torres de Hanoi – solução completa

void hanoi(int n, char origem,
           char destino, char auxiliar) {
    if (n == 0)
        return;

    hanoi(n - 1, origem, auxiliar, destino);

    printf("%c -> %c\n", origem, destino);

    hanoi(n - 1, auxiliar, destino, origem);
}

O código é quase uma tradução direta da solução que descrevemos.

Diferente do fatorial, cada chamada faz duas chamadas recursivas.

A execução pode ser vista como uma árvore

cada chamada de hanoi gera outras duas chamadas menores

Quantos movimentos? (Bônus)

Para mover \(n\) discos:

  • movemos \(n-1\) discos;
  • fazemos 1 movimento;
  • movemos \(n-1\) discos novamente.

Se \(T(n)\) é o número de movimentos:

\[ T(n) = 2T(n-1) + 1. \]

Resolvendo a recursão, podemos mostrar que \[ T(n) = 2^n - 1. \]

O número de movimentos cresce exponencialmente.

Como desenhar uma figura dessas??

O fractal é feito de fractais

Cada região é uma cópia menor do mesmo problema.

Essa é exatamente a estrutura que usamos nas Torres de Hanoi.

Pensando recursivamente

Suponha que já sabemos desenhar corretamente o fractal no nível nivel - 1.


Para desenhar o nível nivel, precisamos descobrir:

  • quantas cópias menores aparecem;
  • qual o tamanho de cada cópia;
  • onde cada cópia começa;
  • se sua orientação muda.

Hanoi e fractais

Hanoi

resolver(n)
├── resolver(n - 1)
├── fazer um movimento
└── resolver(n - 1)

Fractal

desenhar(n)
├── desenhar(n - 1)
├── desenhar(n - 1)
├── desenhar(n - 1)
└── ...

Resolver o problema é, essencialmente, identificar a estrutura recursiva do problema.

Recursão nem sempre é uma boa ideia

Fibonacci

A sequência de Fibonacci também possui uma definição recursiva:

\[ F_0 = 0, \qquad F_1 = 1, \]

\[ F_n = F_{n-1} + F_{n-2}. \]

Uma tradução quase literal para C:

long fib(int n) {
    if (n <= 1)
        return n;

    return fib(n - 1) + fib(n - 2);
}

Parece uma boa solução?

Trabalho repetido

Estamos resolvendo os mesmos subproblemas várias vezes.

Uma solução recursiva pode estar correta e ainda ser muito ineficiente.

Recursivo ou iterativo?

Por exemplo, fatorial também pode ser calculado assim:

int fatorial(int n) {
    int fat = 1;

    for (int i = 2; i <= n; i++)
        fat *= i;

    return fat;
}

Para fatorial, a solução iterativa provavelmente é mais simples.

Como projetar uma função recursiva?

1. Defina precisamente o que a função faz.

O que significa f(n, ...)?

2. Encontre o caso base.

Qual instância sabemos resolver diretamente?

3. Encontre um problema menor.

Se f(n - 1, ...) funciona, como usamos sua resposta?

4. Garanta progresso.

As chamadas realmente chegam ao caso base?

Resumão

  • Recursão: resolver um problema usando instâncias menores do mesmo problema;
  • Caso base: situação resolvida diretamente, onde a recursão termina;
  • Chamadas recursivas podem formar uma pilha ou uma árvore de chamadas;
  • Recursão é especialmente natural em problemas auto-semelhantes;
  • Uma solução recursiva correta não é necessariamente eficiente.

Exercícios

  1. Implemente de forma recursiva a função int max(int vec[], n) que retorna o valor máximo de um vetor de tamanho n;
  2. Implemente de forma recursiva a função imprime_binario(unsigned int n) que imprime em binário um número não-negativo n;

Referências