Saltar para o conteúdo
Topos Uranos

Resumo

Uma função pode chamar a si mesma: a recursão resolve um problema reduzindo-o a uma instância menor do mesmo problema. Esta aula traduz para C++ definições recursivas da matemática (o fatorial, a potência, a soma dos algarismos), cada uma com seu caso base e seu caso recursivo; mostra que a indução garante sua correção e que uma medida que decresce garante sua terminação; acompanha com a pilha de chamadas os quadros que se empilham e se desfazem, e explica quando a pilha se esgota. Examina o limite do tipo do resultado, que estoura muito antes da pilha; compara a versão recursiva e a iterativa de Fibonacci contando chamadas, e fecha com um critério para escolher entre a recursão e um laço.

Objetivos de aprendizagem

  1. Escrever uma função recursiva, com seu caso base e seu caso recursivo, a partir de uma definição recursiva.
  2. Justificar a correção e a terminação de uma função recursiva por indução sobre o tamanho do problema.
  3. Acompanhar com a pilha de chamadas a execução de uma função recursiva e explicar o estouro da pilha.
  4. Comparar o custo da versão recursiva e da iterativa de Fibonacci contando suas chamadas e suas operações.

Definições recursivas

O fatorial

O fatorial de um natural nn é o produto dos naturais de 1 a nn: n!=1⋅2⋯nn! = 1 \cdot 2 \cdots n, com a convenção 0!=10! = 1. As reticências dessa fórmula escondem um laço, e a aula Laços ensinou a escrevê-lo. Ora, a mesma definição admite outra forma, que não menciona nenhum laço: o fatorial de nn é nn vezes o fatorial de n−1n - 1, e o de 0 é 1.

0!=1,n!=n⋅(n−1)!(n≥1).0! = 1, \qquad n! = n \cdot (n - 1)! \quad (n \geq 1).

Esta é uma definição recursiva: define o fatorial de nn por meio do fatorial de um número menor. Não é circular, porque cada aplicação da segunda regra desce um degrau e a escada termina na primeira: 3!=3⋅2!=3⋅2⋅1!=3⋅2⋅1⋅0!=63! = 3 \cdot 2! = 3 \cdot 2 \cdot 1! = 3 \cdot 2 \cdot 1 \cdot 0! = 6. As duas regras têm nome. A primeira é o caso base, que dá o resultado sem se referir a nada mais; a segunda é o caso recursivo, que reduz o problema a uma instância menor do mesmo problema.

C++ permite que uma função chame a si mesma, e com isso a definição se traduz quase palavra por palavra.

ExemploO fatorial recursivo
Programa em C++
#include <print>​// Fatorial de n.// Pré-condição: 0 <= n <= 12 (a cota se justifica mais adiante).// Pós-condição: retorna n! = 1 · 2 · ... · n, com 0! = 1.int factorial(int n){    if (n == 0) {        return 1;                   // caso base    }    return n * factorial(n - 1);    // caso recursivo}​int main(){    for (int n{0}; n <= 6; ++n) {        std::println("{}! = {}", n, factorial(n));    }}
Saída
0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720

A função tem exatamente a forma da definição: um if que atende o caso base e um return que aplica o caso recursivo. A chamada factorial(n - 1) não é diferente de qualquer outra chamada da aula Funções: avalia-se seu argumento, cria-se um quadro novo com seu próprio n e, ao terminar, o valor retornado é multiplicado pelo n de quem chamou.

O contrato não é decorativo: com n=−1n = -1 o caso recursivo chamaria com −2-2, depois com −3-3, e nunca alcançaria o 0; e 13!13! não cabe em um int. Muitos textos introdutórios escrevem este fatorial com o caso base if (n == 0 || n == 1), redundante mas inofensivo, e sem dizer nada dos negativos nem do tamanho do resultado: seu código não termina com n<0n < 0 e dá um resultado sem sentido a partir de 13!13!. Ambos os defeitos serão examinados em sua seção.

Caso base e caso recursivo

Toda função recursiva tem um ou vários casos base, que se resolvem diretamente, e um ou vários casos recursivos, que chamam a mesma função com um argumento menor em algum sentido preciso e combinam o que ela retorna.

A potência xnx^n, com nn natural, define-se por x0=1x^0 = 1 e xn=x⋅xn−1x^n = x \cdot x^{n - 1}: o tamanho é o expoente. A soma dos algarismos de um natural nn (a de 2026 é 2+0+2+6=102 + 0 + 2 + 6 = 10) define-se assim: se n<10n < 10, a soma é nn, porque tem um só algarismo; se não, é a soma dos algarismos de ⌊n/10⌋\lfloor n / 10 \rfloor, que é nn sem seu último algarismo, mais esse último algarismo, n mod 10n \bmod 10. Aqui o tamanho é o número de algarismos.

ExemploA potência e a soma dos algarismos
Programa em C++
#include <print>​// x elevado a n.// Pré-condição: n >= 0.// Pós-condição: retorna x^n, com x^0 = 1.double power(double x, int n){    if (n == 0) {        return 1.0;    }    return x * power(x, n - 1);}​// Soma dos algarismos decimais de n.// Pré-condição: n >= 0.// Pós-condição: retorna a soma dos algarismos de n.int digitSum(int n){    if (n < 10) {        return n;    }    return digitSum(n / 10) + n % 10;}​int main(){    std::println("2^10 = {}", power(2.0, 10));    std::println("1.5^3 = {}", power(1.5, 3));    std::println("soma dos algarismos de 2026: {}", digitSum(2026));}
Saída
2^10 = 10241.5^3 = 3.375soma dos algarismos de 2026: 10

Em digitSum, a divisão inteira n / 10 tira o último algarismo e n % 10 o extrai, como na mudança de base da aula Algoritmos numéricos. Para n=2026n = 2026, a função pede a soma dos algarismos de 202, que pede a de 20, que pede a de 2; esta é um caso base e retorna 2, e os resultados se compõem na volta: 2+0=22 + 0 = 2, 2+2=42 + 2 = 4, 4+6=104 + 6 = 10.

Escrever uma função recursiva consiste, portanto, em responder a duas perguntas: que casos se resolvem sem ajuda, e como se obtém a solução de um caso a partir da de um menor. Se ambas as respostas estão corretas, a função também está, e a seção seguinte o demonstra.