Aula 19 - IC

Propaganda
NOTAS DE AULA – ALGORITMOS E PROGRAMAÇÃO DE COMPUTADORES
Prof. Rodrigo de Oliveira
59
Recursão
•
•
•
•
A maioria dos programas possui frações de código que se repetem várias vezes. Isso é necessário
principalmente para cálculos matemáticos.
o Exemplo:
2×3 =
2 + (2 × 2) =
2 + 2 + (2 × 1) = 6
A linguagem de programação C fornece três estruturas de repetição para realizar cálculos iterativos: for,
while e do-while. No entanto, para casos mais complexos que o exemplo anterior, a leitura desses códigos
nem sempre é muito simples para a identificação de erros.
Recursão é um conceito fundamental em matemática e ciência da computação. Uma função recursiva é toda
aquela que chama a si mesma dentro do corpo de sua função
Exemplo: Tente enxergar a recursão transformando uma potência de expoente alto em outra de expoente
menor usando a própria teoria de potenciação:
34 =
3 × 33 =
3 × 3 × 32 =
3 × 3 × 3 × 31 = 81
Pode-se dizer que, para todo inteiro positivo x e y: x y = x × x y −1 . Logo, para uma função recursiva:
int potencia(int base, int expoente)
que calcula o resultado de se elevar a base ao expoente, é correto afirmar que:
resultado = base * potencia( base, (expoente – 1) );
Pelo código anterior, a cada nova chamada recursiva da função potencia(), o expoente será menor em
uma unidade. Esse processo continua até que o expoente seja igual a 1, quando já se tem a resposta, visto
que todo número elevado a 1 é igual a ele mesmo. A função recursiva fica como a seguir:
usando recursão
usando iteração for
1: int pot(int base, int exp)
2: {
3:
if (exp == 1)
4:
return base;
5:
else
6:
return base * pot(base, exp – 1);
7: }
1: int pot (int base, int exp)
2: {
3:
int i, resultado = 1;
4:
5:
for (i = 0; i < exp; i++)
6:
resultado *= base;
7:
return resultado;
8: }
iguais
Note que esta é uma função válida apenas para potências com expoente maior que zero (como todo número
elevado a zero é igual a um, a função também pode receber expoente zero se a condição da linha 3 for
alterada para checar se o expoente é zero e retornar o valor 1 ao invés da base).
NOTAS DE AULA – ALGORITMOS E PROGRAMAÇÃO DE COMPUTADORES
Prof. Rodrigo de Oliveira
•
60
As abordagens da solução de problemas com recursão têm diversos elementos em comum. Genericamente,
um módulo recursivo segue o algoritmo abaixo:
if (<condição de parada>) {
// Resolva
} else {
// Divida o problema num caso mais simples utilizando recursão
}
Note que, sem a condição de parada, o problema entraria em “loop”, dividindo o problema infinitas vezes
sem nunca parar. Seguindo a idéia do algoritmo proposto, a função recursiva só sabe como resolver o(s)
caso(s) mais simples (caso base identificado na condição de parada). Na função que calcula potência, o
caso base é elevar um número qualquer ao expoente 1 e retornar o próprio número (linhas 3 e 4). Se a
função é chamada com um problema mais complexo, a função divide o problema em dois pedaços
conceituais: um pedaço que a função sabe como resolver (caso base) e um pedaço que a função não sabe
como resolver (linhas 5 e 6). Como esse novo problema se parece com o problema original, a função faz
uma chamada a si mesma para resolver o problema menor (etapa de recursão). A etapa de recursão é
executada enquanto a chamada original para a função ainda está ativa, podendo resultar ainda em muitas
outras chamadas recursivas, sempre dividindo cada sub-problema novo em dois pedaços conceituais. Para
que a recursão termine em algum momento, a seqüência de problemas cada vez menores deve convergir
para o caso base. Nesse ponto, a função reconhece o caso base, devolve um resultado para a cópia da
função anterior e uma seqüência de devoluções se segue até a chamada original da função devolver o
resultado final.
•
Outro problema comumente usado para exemplificar recursão é a série de Fibonacci:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ...
Ela inicia com 0 e 1 e tem a propriedade de que cada número de Fibonacci subseqüente é a soma dos dois
números que o precedem. A série é definida recursivamente como:
fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(n) = fibonacci(n – 1) + fibonacci(n – 2)
Note que neste caso temos dois casos base: fibonacci(0) e fibonacci(1). A função recursiva que retorna o iésimo elemento da série de Fibonacci é dada a seguir:
1: int fibonacci(int num)
2: {
3:
if (num == 0)
4:
return 0;
5:
else if (num == 1)
6:
return 1;
7:
else
8:
return fibonacci(num - 1) + fibonacci(num - 2);
9: }
NOTAS DE AULA – ALGORITMOS E PROGRAMAÇÃO DE COMPUTADORES
Prof. Rodrigo de Oliveira
61
Esquematização de fibonacci(4):
fib(4) = fib(3) + fib(2)
fib(3) = fib(2) + fib(1)
fib(2) = fib(1) + fib(0)
fib(1) = 1
fib (2) = fib (1) + fib (0)
fib(1) = 1
fib(0) = 0
fib(1) = 1
fib(0) = 0
fib(2) = 1 + 0 = 1
fib(2) = 1 + 0 = 1
fib(3) = 1 + 1 = 2
fib(4) = 2 + 1 = 3
•
Cada invocação à função fibonacci que não corresponde a um dos casos base, resulta em mais duas
chamadas recursivas. Esse número de chamadas cresce rapidamente mesmo para números bem pequenos da
série. Por exemplo, o fibonacci de 31 exige 4.356.617 chamadas à função enquanto que o fibonacci de 32
exige 7.049.155, ou seja, 2.692.538 chamadas adicionais à função. Logo, é interessante evitar programas
recursivos no estilo de Fibonacci que resultam em uma “explosão” exponencial de chamadas. Isso porque, a
cada chamada recursiva da função, ela é adicionada a uma pilha; da mesma forma, a cada término da
função, ela é desempilhada. Esse empilhamento e desempilhamento repetitivo consome tempo, fazendo
com que o mesmo programa implementado iterativamente venha a ser mais rápido que o recursivo (embora,
por outro lado, provavelmente não seja de compreensão e manutenção tão simples). Além disso, a pilha de
funções apresenta um tamanho máximo, limitando assim o uso de recursões que mantenham muitas funções
na pilha (como o fibonacci de um número grande).
Outro problema recursivo é o fatorial de um inteiro n não-negativo, escrito n! = n × (n − 1) × (n − 2) × K × 1 .
int fatorial(int num) {
if (num <= 1)
return num;
else
return num * fatorial(num - 1);
}
Recursão versus Iteração
•
Tem-se a seguir uma comparação entre as duas abordagens de implementação para que o programador
possa escolher qual a ideal de acordo com uma situação em particular:
Recursão
Estruturas de seleção if, if-else ou switch.
Repetição por chamadas de funções repetidas.
Termina quando um caso base é reconhecido.
Pode ocorrer infinitamente.
Lento.
Soluções simples e de fácil manutenção.
Iteração
Estruturas de repetição for, while ou do-while.
Repetição explícita.
Termina quando teste do laço falha.
Pode ocorrer infinitamente.
Rápido.
Soluções complicadas e de difícil manutenção.
NOTAS DE AULA – ALGORITMOS E PROGRAMAÇÃO DE COMPUTADORES
Prof. Rodrigo de Oliveira
62
Exercícios: Faça funções que usem recursão comum para resolver os seguintes problemas.
a) Multiplicação – Recebe dois números positivos não nulos a e b e retorna o resultado de a × b .
int multiplicacao(int a, int b) {
if (b == 1)
return a;
else
return a + multiplicacao( a, (b – 1) );
}
b) Produtório – Recebe dois números m e n tais que m <= n e retorna o produtório de todos os números no
intervalo [m,n].
c) Maior – Recebe um ponteiro para vetor de inteiros e seu tamanho e retorna o maior da lista.
d) Somatório de vetor – Recebe um ponteiro para um vetor de inteiros e seu tamanho e retorna o somatório de
todos os elementos do vetor.
e) Somatório de intervalo de vetor – Recebe um ponteiro para um vetor de inteiros, seu tamanho, um índice de
início e um de fim e retorna o somatório de todos os elementos do intervalo [início, fim] do vetor.
Observação: sempre serão passados valores tais que 0 <= início <= fim < tamanho.
f) Pertence - Recebe um número, um ponteiro para vetor de inteiros e seu tamanho e retorna 1 se o número
pertence ao vetor e 0 caso contrário.
g) Ocorrências – Recebe um número, um ponteiro para vetor de inteiros e seu tamanho e retorna o número de
ocorrências desse número no vetor.
h) Torre de Hanói (desafio) – Em um templo no Extremo Oriente, os sacerdotes estavam tentando mover uma
pilha de discos de um pino para outro. A pilha inicial tinha 64 discos sobrepostos em um pino e ordenados
de baixo para cima por tamanho decrescente. Os sacerdotes tentavam mover a pilha desse pino A para um
terceiro pino C usando um segundo pino B para conter os discos temporariamente. Apenas um disco podia
ser movido por vez e em nenhum momento um disco maior podia ser colocado sobre um disco menor.
Dica: Pense no mais simples possível: mover 1 disco apenas. Basta tirá-lo do pino A e colocá-lo no C.
Agora pense em mover 2 discos. Primeiro move-se o disco 2 de A para B e caímos no caso anterior (pois o
pino A agora só tem o disco 1). Então move-se o disco 1 de A para C e o disco 2 de B para C. Veja que
mover n discos pode ser visualizado em termos de mover n – 1 discos como a seguir:
● Mover n – 1 discos do pino A para o pino B, utilizando o pino C como armazenamento temporário;
● Mover o último disco (o maior) do pino A para o pino C;
● Mover os n – 1 discos do pino B para o pino C, utilizando o pino A como armazenamento temporário;
Implemente a função com a seguinte declaração: void hanoi (char a, char b, char c, int discos)
A
B
C
Download