Ao tentar resolver o problema,
encontrei obstáculos dentro de obstáculos.
Por isso, adotei uma solução recursiva.
— aluno S.Y.
To understand recursion,
we must first understand recursion.
— anônimo
Para fazer um procedimento recursivo
é preciso ter fé.
— prof. Siang Wun Song
Muitos problemas computacionais têm a seguinte propriedade: cada instância do problema contém uma instância menor do mesmo problema. Dizemos que esses problemas têm estrutura recursiva. Para resolver uma instância de um problema desse tipo, podemos aplicar o seguinte método:
resolva-a diretamente (use força bruta se necessário);
reduza-a a uma instância menor do mesmo problema,
aplique o método à instância menor,
volte à instância original.
O programador só precisa mostrar como obter uma solução da instância original a partir de uma solução da instância menor; o computador faz o resto. A aplicação desse método produz um algoritmo recursivo.
Sumário:
Para ilustrar o conceito de algoritmo recursivo, considere o seguinte problema: Encontrar o valor de um elemento máximo de um vetor v[0..n-1]. (O problema já foi mencionado num dos exercícios na página Vetores.)
Observe que o problema só faz sentido se o vetor não é vazio, ou seja, se n ≥ 1. Eis uma função recursiva que resolve o problema:
// Ao receber v e n >= 1, a função devolve // o valor de um elemento máximo do // vetor v[0..n-1]. int maximo (int n, int v[]) { if (n == 1) return v[0]; else { int x; x = maximo (n-1, v); // x é o máximo de v[0..n-2] if (x > v[n-1]) return x; else return v[n-1]; } }
A análise da correção da função tem a forma de uma prova por indução. Para começar, considere uma instância em que n vale 1. Nessa instância, a resposta que a função devolve, v[0], está correta pois v[0] é o único elemento relevante do vetor. Agora considere uma instância em que n é maior que 1. Nessa instância, o vetor tem duas partes não vazias:
v[0..n-2] e v[n-1].
Podemos supor que a função resolve corretamente
a instância v[0..n-2] do problema,
pois essa instância é menor que a original.
Portanto,
depois da linha x = maximo (n-1, v)
,
podemos supor que x é um máximo correto de v[0..n-2].
Logo, a solução da instância original é o maior dos números
x e v[n-1].
E é justamente esse o número que a função calcula e devolve.
Isso conclui a prova de que a função
maximo está correta.
Para que uma função recursiva seja compreensível, é essencial que o autor diga o que a função faz. Isso deve ser dito de maneira explicita e completa, como fiz acima no comentário que precede o código da função.
Como o computador executa uma função recursiva? Embora relevante e importante, essa pergunta será ignorada por enquanto. (Mas você pode ver o apêndice A pilha de execução de um programa do capítulo Pilhas.) Vamos nos limitar a mostra um exemplo de execução. Se v[0..3] é o vetor 77 88 66 99 então teremos a seguinte sequência de chamadas da função:
rmaximum(4,v) │ rmaximum(3,v) │ │ rmaximum(2,v) │ │ │ rmaximum(1,v) │ │ │ └──────────── returns 77 │ │ └────────────── returns 88 │ └──────────────── returns 88 └────────────────── returns 99
Desempenho.
Algumas pessoas acreditam que
funções recursivas são inerentemente lentas,
mas isso não passa de lenda.
Talvez a lenda tenha origem em certos usos descuidadas da recursão,
como em alguns
dos exercícios abaixo.
(Nem tudo são flores, entretanto:
o espaço de memória que uma função recursiva consome
para rascunho
pode ser grande.)
return v[0]por
return 0? Faz sentido trocar
return v[0]por
return INT_MIN? Faz sentido trocar
x > v[n-1]por
x >= v[n-1]?
int maximo (int n, int v[]) { int x; if (n == 1) x = v[0]; else { x = maximo (n-1, v); if (x < v[n-1]) x = v[n-1]; } return x; }
int maximo (int n, int v[]) { if (n == 1) return v[0]; int x = maximo (n-1, v); if (x > v[n-1]) return x; else return v[n-1]; }
int maxim1 (int n, int v[]) { if (n == 1) return v[0]; if (n == 2) { if (v[0] < v[1]) return v[1]; else return v[0]; } int x = maxim1 (n-1, v); if (x < v[n-1]) return v[n-1]; else return x; }
int maxim2 (int n, int v[]) { if (n == 1) return v[0]; if (maxim2 (n-1, v) < v[n-1]) return v[n-1]; else return maxim2 (n-1, v); }
A função maximo discutida acima
reduz a instância v[0..n-1] do problema
à instância v[0..n-2].
Podemos escrever uma função que reduza v[0..n-1]
a v[1..n-1],
ou seja, empurre pra direita
o início do vetor:
// Ao receber v e n >= 1, esta função devolve
// o valor de um elemento máximo do vetor
// v[0..n-1].
int
maximo2 (int n, int v[])
{
return max (0, n, v);
}
A função maximo2 é apenas uma embalagem
ou invólucro
(= wrapper-function);
o serviço propriamente dito
é executado pela função recursiva max:
// Recebe v e i < n e devolve o valor de
// um elemento máximo do vetor v[i..n-1].
int
max (int i, int n, int v[])
{
if (i == n-1) return v[i];
else {
int x;
x = max (i + 1, n, v);
if (x > v[i]) return x;
else return v[i];
}
}
A função max resolve um problema mais geral que o original (veja o parâmetro i). A necessidade de generalizar o problema original ocorre com frequência no projeto de algoritmos recursivos.
A título de curiosidade, eis outra maneira, talvez surpreendente, de aplicar recursão ao segmento v[1..n-1] do vetor. Ela usa aritmética de endereços em lugar do parâmetro adicional i.
int maximo2b (int n, int v[]) { if (n == 1) return v[0]; int x = maximo2b (n - 1, v + 1); if (x > v[0]) return x; return v[0]; }
if (x > v[i])por
if (x >= v[i])?
int maxx (int p, int u, int v[]) { if (p == u) return v[u]; else { int m, x, y; m = (p + u)/2; // p ≤ m < u x = maxx (p, m, v); y = maxx (m+1, u, v); if (x >= y) return x; else return y; } }
// Recebe um vetor de tamanho n e devolve // a soma dos elementos do vetor. int soma (int v[], int n) { return sss (v, n+1); } int sss (int v[], int n) { if (n == 1) return 0; return sss (v, n-1) + v[n-1]; }
int ttt (int x[], int n) { if (n == 0) return 0; if (x[n] > 100) return x[n] + ttt (x, n-1); else return ttt (x, n-1); }
int X (int n) { if (n == 1 || n == 2) return n; else return X (n-1) + n * X (n-2); }
double f (double x, double y) { if (x >= y) return (x + y)/2; else return f (f (x+2, y-1), f (x+1, y-2)); }
int ff (int n) { if (n == 1) return 1; if (n % 2 == 0) return ff (n/2); return ff ((n-1)/2) + ff ((n+1)/2); } int main (void) { printf ("%d", ff(7)); return EXIT_SUCCESS; }
int fusc (int n, int profund) { for (int i = 0; i < profund; ++i) printf (" "); printf ("fusc(%d,%d)\n", n, profund); if (n = 1) return 1; if (n % 2 == 0) return fusc (n/2, profund+1); return fusc ((n-1)/2, profund+1) + fusc ((n+1)/2, profund+1); }
int XX (int n) { if (n == 0) return 0; else return XX (n/3+1) + n; }
F(3) F(2) F(1) F(0) F(1)Qual a sequência de invocações da função durante o cálculo de F(5)?
int Euclides (int m, int n) { int r; do { r = m % n; m = n; n = r; } while (r != 0); return m; }
. . - . -- . - . --- . - . -- . - . ---- . - . -- . - . --- . - . -- . - .
Resposta: Não. Os parênteses adicionais são redundantes porque o operador == tem precedência sobre ||.