Estrutura de Dados – Básica
Professor: Osvaldo Kotaro Takai.
Aula 7: Recursividade
O objetivo desta aula é apresentar o conceito de recursão para solução de problemas.
A recursão é uma técnica de programação em que uma rotina (função) chama a si mesma. A
recursão é uma das técnicas mais interessantes e surpreendentemente eficazes em
programação. Ela não só funciona, mas também oferece um quadro conceitual única para
resolver muitos problemas.
Demonstrando a recursão com números triangulares
Diz-se que os pitagóricos, um bando de matemáticos da Grécia antiga que trabalhavam com o
Pitágoras (do famoso teorema de Pitágoras), sentiram uma conexão mística com a seguinte
seqüência de números: 1, 3, 6, 10, 15, 21, ...
O termo da n-ésima seqüência é obtido acrescentando-se n ao termo anterior. Dessa forma, o
segundo termo é encontrado adicionando-se 2 ao primeiro termo (que é 1), dando 3. O terceiro
é 3 adicionando ao segundo termo (que é 3) dando 6, etc. Os números da seqüência são
chamados números triangulares porque eles podem ser visualizados como uma organização
triangular de objetos, ilustrados como pequenos quadrados como na figura a seguir:
Suponha que você quisesse encontrar o valor de algum termo na seqüência; digamos o quarto
termo (cujo valor é 10). Você poderá perceber que o valor de qualquer termo pode ser obtido
somando todas as colunas verticais de quadrados. Veja a figura abaixo:
No quarto termo, a primeira coluna tem quatro pequenos quadrados, a segunda coluna tem
três, etc. Adicionando, 4 + 3 + 2 + 1, temos 10.
A seguinte função triangular() utiliza esta técnica, baseada em colunas, para encontrar um
número triangular. Ela soma todas as colunas, da altura de n até a altura 1.
1
É claro que você poderia resolver o problema da seguinte forma:
A abordagem anterior pode parecer direta, mas há outra forma de ver este problema. O valor
do n-ésimo termo pode ser pensado como uma soma de duas coisas, em vez de uma
seqüência. São elas:
1. A primeira coluna (mais alta), que tem n quadrados.
2. A soma de todos os quadrados das colunas restantes.
Isto é:
Se conhecêssemos uma função que descobrisse a soma de todas as colunas restantes,
poderíamos escrever nossa função triangulo(), que retorna o valor do n-ésimo número
triangular, da seguinte forma:
Note que a função somaDasColunasRestantes() fará exatamente a mesma coisa que a função
triangulo(). Ou seja, somando todas as colunas para algum número n passado como
argumento. Então, por que não usar a própria função triangulo(), em vez de alguma outra
função? Isso se pareceria com:
Pode parecer surpreendente que uma função possa chamar a si mesma, mas por que ela não
deveria ser capaz de fazê-lo?
Uma chamada da função é (entre outras coisas) uma transferência de controle para o início da
função.
Tudo isso se parece como uma transferência de responsabilidade para outro, só que reduzindo
o tamanho do problema. Alguém me diz para encontrar o nono número triangular. Sei que ele é
9 mais o oitavo número triangular; então eu chamo o João e lhe peço para encontrar o oitavo
número triangular. Quando o João me devolver o oitavo número triangular, acrescento 9 a esse
valor e devolvo o resultado.
2
O João sabe que o oitavo número triangular é 8 mais o sétimo número triangular; então ele
chama a Maria e pede para encontrar o sétimo número triangular. Esse processo continua com
cada pessoa passando a responsabilidade para o outro.
Onde este processo de transferência de responsabilidade acaba? Alguém em algum ponto
deve ser capaz de descobrir uma resposta sem precisar pedir a uma outra pessoa para ajudálo. Se isso não ocorresse, haveria uma cadeia infinita de pessoas transferindo
responsabilidades uma para as outras.
Para evitar isso, a pessoa que receber a incumbência de encontrar o primeiro número
triangular da seqüência (quando n é igual a 1), deve saber, sem perguntar a qualquer pessoa,
que a resposta é 1. Não há números menores para perguntar a outras pessoas, não há nada
para adicionar a qualquer outra coisa, então a transferência pára aqui.
A condição que leva à função recursiva retornar sem fazer outra chamada recursiva é
conhecida como caso base. É importante que cada função recursiva tenha um caso base para
evitar recursão infinita e a conseqüente finalização da função.
Método para solução de problemas de estrutura recursiva
Muitos problemas têm a seguinte propriedade: cada instância — ou seja, cada exemplo
concreto — do problema contém uma instância menor do mesmo problema. Dizemos que
esses problemas têm estrutura recursiva. Para resolver tais problemas podemos aplicar o
seguinte método:
1) Se o problema é pequeno (caso base).
a) Resolva-o diretamente (use força bruta se necessário);
2) Se o problema é grande,
a) Reduza-o a uma versão menor do mesmo problema,
b) Aplique o método ao problema menor e
c) Volte ao problema original.
A aplicação desse método produz um algoritmo recursivo. Para mostrar como isso funciona,
considere o seguinte problema:
Determinar o valor do maior elemento de um vetor v que tem n elementos.
É claro que o problema só faz sentido se o vetor não é vazio, ou seja, se n > 1. Para preparar o
terreno, examine uma tradicional solução iterativa do problema:
3
Agora vamos usar o método para encontrar soluções recursivas. Imagine que n = 8 e o vetor v
contenha os seguintes valores {5, 10, 3, 7, 2, 1, 30, 25}
1) Se o problema é pequeno (caso base)
a) Resolva-o diretamente (use força bruta se necessário).
O problema pequeno para o problema de encontrar o maior elemento de um vetor de n
posições é quando o vetor tem apenas um único número (n==1). Neste caso, o máximo
é v[0].
2) Se o problema é grande,
a) Reduza-o a uma versão menor do mesmo problema.
Como n é grande (n==8), reduzimos o problema dividindo o vetor em duas partes.
Ultima posição do vetor: v[n-1]
0
1
2
3
4
5
6
7
5
10
3
7
2
1
30
25
Vetor sem a última posição (possui n-1 posições)
Assim, o máximo será o maior entre o v[n-1] e o máximo do vetor que possuir n-1
posições.
b) Aplique o método ao problema menor.
Se n-1 == 1, então aplicamos 1a. e retornamos v[0]. Se n-1 > 1 aplicamos novamente
2a. ao vetor com n-2 posições. Esse processo é aplicado sucessivamente até que o
vetor tenha apenas uma posição.
•
•
•
•
•
•
•
•
maximo (8, v) Å maior entre 25 e maximo(7, v)
maximo (7, v) Å maior entre 30 e maximo(6, v)
maximo (6, v) Å maior entre 1 e maximo(5, v)
maximo (5, v) Å maior entre 2 e maximo(4, v)
maximo (4, v) Å maior entre 7 e maximo(3, v)
maximo (3, v) Å maior entre 3 e maximo(2, v)
maximo (2, v) Å maior entre 10 e maximo(1, v)
maximo (1, v) Å 5
c) Volte ao problema original.
•
•
•
•
•
•
•
•
maximo(1, v) Æ 5
maximo(2, v) Æ maior entre 10 e 5
maximo(3, v) Æ maior entre 3 e 10
maximo(4, v) Æ maior entre 7 e 10
maximo(5, v) Æ maior entre 2 e 10
maximo(6, v) Æ maior entre 1 e 10
maximo(7, v) Æ maior entre 30 e 10
maximo(8, v) Æ maior entre 25 e 30
Logo maximo(8, v) Æ 30.
4
A implementação dessa solução recursiva é a seguinte:
Nota: Algumas pessoas acham que funções recursivas consomem muito tempo. Mas isso é
apenas uma lenda propagada por programadores que não sabem usar a recursão.
As função maximo recursiva discutida acima "puxa para a esquerda" o fim do vetor, ou seja,
troca v[0..n-1] pelo vetor v[0..n-2]. É possível escrever uma versão que "empurre para a direita"
o início do vetor.
Observe que essa nova versão da função maximo é apenas uma "embalagem": o serviço
pesado é executado pela função recursiva mx. A função mx resolve um problema mais geral
que o original. Isso ocorre freqüentemente na construção de algoritmos recursivos: é preciso
generalizar o problema para que uma solução recursiva se torne possível.
5
Exercícios
1. Faça um trabalho sobre “As Torres de Hanói”. Apresente o problema, a análise do
algoritmo que soluciona o problema e implemente a solução.
2. Considere a função iterativa maximo exemplificado anteriormente. Faz sentido trocar "x
= v[0]" por "x = 0", como fazem alguns programadores descuidados? Faz sentido trocar
"x = v[0]" por "x = INT_MIN"? Faz sentido trocar "x < v[j]" por "x <= v[j]"?
3. A função abaixo promete encontrar o valor de um elemento máximo de v[0..n-1]. A
função cumpre a promessa?
int maxi (int n, int v[]) {
int j, m = v[0];
for (j = 1; j < n; j++)
if (v[j-1] < v[j]) m = v[j];
return m;
}
4. Critique a seguinte função recursiva; ela promete encontrar o valor de um elemento
máximo de v[0..n-1].
int maximo1A (int n, int v[]) {
int x;
if (n == 1) return v[0];
if (n == 2) {
if (v[0] < v[1])
return v[1];
else return v[0];
}
x = maximo1A (n-1, v);
if (x < v[n-1]) return v[n-1];
else return x;
}
5. Critique a seguinte função recursiva; ela promete encontrar o valor de um elemento
máximo de v[0..n-1].
int maximo1B (int n, int v[]) {
if (n == 1) return v[0];
if (maximo1B (n-1, v) < v[n-1]) return v[n-1];
else return maximo1B (n-1, v);
}
6. Escreva uma função recursiva maxmin que calcule o valor de um elemento máximo e o
valor de um elemento mínimo de um vetor v[0..n-1]. Quantas comparações envolvendo
os elementos do vetor a sua função faz?
7. Escreva uma função recursiva que calcule a soma dos elementos positivos do vetor de
inteiros v[0..n-1]. O problema faz sentido quando n é igual a 0? Quanto deve valer a
soma nesse caso?
8. Escreva uma função recursiva que calcule a soma dos dígitos de um inteiro positivo n.
A soma dos dígitos de 132, por exemplo, é 6.
9. Qual o valor de X (4)?
int X (int n) {
if (n == 1 || n == 2) return n;
else return X(n-1) + n * X(n-2);
}
10. Qual é o valor de f (1,10)? Escreva uma função equivalente que seja mais simples.
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));
}
6
11. Qual o resultado da execução do programa abaixo?
int main (void) {
printf ("%d", ff(7));
return 0;
}
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);
}
12. Execute fusc(7,0).
int fusc (int n, int profund) {
int i;
for (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);
}
13. A função de Fibonacci é definida assim: F (0) = 0, F (1) = 1 e F (n) = F(n-1) + F(n-2)
para n > 1. Descreva a função F em linguagem C. Faça uma versão iterativa e uma
recursiva.
14. A seguinte função calcula o maior divisor comum dos inteiros positivos m e n. Escreva
uma função recursiva equivalente.
int Euclides(int m, int n) {
int r;
do {
r = m % n;
m = n;
n = r;
} while (r != 0);
return m;
}
15. Escreva uma função recursiva eficiente que receba inteiros positivos k e n e calcule kn.
(Suponha que kn cabe em um int.) Quantas multiplicações sua função executa
aproximadamente?
16. Faça um programa recursivo, em C, que calcule o n-ésimo número:
• Quadrado.
• Fatorial.
7
Download

Estrutura de Dados – Básica