Simulação de Recursividade

Propaganda
Recursividade
Aula 9
Em matemática vários objetos são definidos
apresentando-se um processo que os
produz.
Ex PI (circunferência/diâmetro)
Outra definição de um objeto por um
processo é o fatorial de um número
Fatorial(n) ou n!
= n*(n-1)*(n-2)...(2)*1 se n >0
=1 se n==0
Se formulamos:
0! = 0
1! = 1
2! = 2 * 1
3! = 3 * 2 * 1
4! = 4 * 3 * 2 * 1
Não é possível listar a fórmula para fatorial de cada
inteiro
Para evitar qualquer abreviatura e um cjto infinito de
definições poderemos apresentar um algoritmo que
aceite um número inteiro n e retorne n!
prod = 1;
for (x = n; x > 0; x--)
prod *= x;
return prod;
ALGORITMO ITERATIVO: Requer de repetição
explícita até uma condição ser satisfeita.
Um algoritmo pode ser considerado “Um
programa ideal” sem quaisquer limitações
práticas de um computador real e portanto
pode ser usado para definir uma função
matemática, entretanto uma função de C não
serve como definição matemática da função
fatorial por causa de limitações como
precisão e tamanho finito de uma máquina
real.
Examinemos detalhadamente a definição de n! que
lista uma fórmula separada para cada valor de n
Ex 4! = 4 * 3 * 2 * 1 isso é igual a 4 * 3!
Para todo n>0 verificamos que
n! = n* (n-1)!
Podemos definir:
0! = 1
1! = 1 * 0!
2! = 2 * 1!
3! = 3 * 2!
4! = 4 * 3!
5! = 5 * 4!
Se empregamos uma notação matemática temos
que
n! = 1 se n == 0
n! = n * (n - 1)! se n >0
Definição de uma função em termos de se mesma,
aparentemente circular.
Método conciso de escrever um número infinito de
equações necessárias para definir n!
Definição RECURSIVA
A recursividade pode ser utilizada quando um
problema puder ser definido em termos de si
próprio.
Por exemplo, quando um objeto é colocado entre
dois espelhos planos paralelos e frente a frente
surge uma imagem recursiva, porque a imagem do
objeto refletida num espelho passa a ser o objeto a
ser refletido no outro espelho e, assim,
sucessivamente.
Uma função recursiva chama ela mesma, mas com
outros parâmetros.
Algoritmos Recursivos
A idéia básica de um algoritmo recursivo consiste em
diminuir sucessivamente o problema em um problema
menor ou mais simples, até que o tamanho ou a
simplicidade do problema reduzido permita resolvê-lo de
forma direta, sem recorrer a si mesmo.
Quando isso ocorre, diz que o algoritmo atingiu uma
condição de parada, a qual deve estar presente em pelo
menos um local dentro algoritmo.
Sem esta condição o algoritmo não pára de chamar a si
mesmo, até estourar a capacidade da pilha de execução, o
que geralmente causa efeitos colaterais e até mesmo o
término indesejável do programa.
Componentes de um algoritmo
recursivo
Condição de parada, quando a parte do
problema pode ser resolvida diretamente,
sem chamar de novo a função recursiva.
Outros comandos que resolvem uma parte
do problema (chamando novamente a função
recursiva);
Algoritmos Recursivos
f1(..)
...
...
...
chama f1(..)
...
...
fim
f1(..)
...
...
chama f1(..)
...
...
f1(..)
...
...
...
condição de
parada!
5! = 5 * 4!
2.
4! = 4 * 3!
3.
3! = 3 * 2!
4.
2! = 2 * 1!
5.
1! = 1 * 0!
6.
0! = 1
Cada caso é reduzido a um caso mais simples até
chegarmos ao caso 0! Que é definido
imediatamente como 1
1.
Se voltarmos
6’ 0! = 1
5’ 1! = 1 * 0! = 1 * 1 = 1
4’ 2! = 2 * 1! = 2 * 1 = 2
3’ 3! = 3 * 2! = 3 * 2 = 6
2’ 4! = 4 * 3! = 4 * 6 = 24
1’ 5! = 5 * 4! = 5 * 24 = 120
Se levamos esse processo a um algoritmos
teremos
if (n == 0)
fact = 1;
else {
x = n -1;
ache o valor de x!. Chame-o de y;
fact = n * y;}
Definição recursiva do processo do cálculo do
fatorial
int main(void)
{
int NUMERO, RESULTADO;
scanf("%d", &NUMERO);
RESULTADO = FAT(NUMERO);
printf("%d\n", RESULTADO);
}
int FAT(int N)
{
int AUX;
if(N == 0) // condição de parada
return 1;
AUX = N * FAT(N - 1);
return AUX;
}
LEMBRE-SE!! N é variável local da função FAT.
É melhor a definição recursiva de fatorial?
Programa principal:
main
NUMERO = 2
RESULTADO =
FAT(2)
Função FAT:
PRIMEIRA
CHAMADA
N=2
AUX = 2 * FAT(1)
1
Função FAT:
PRIMEIRA CHAMADA
Função FAT:
SEGUNDA CHAMADA
N=2
AUX = 2 * FAT(1)
N=1
AUX = 1 * FAT(0)
1
2
Função FAT:
SEGUNDA CHAMADA
Função FAT:
TERCEIRA CHAMADA
N=1
AUX = 1 * FAT(0)
N=0
condição de parada
atingida!!!
if (N = = 0)
return 1;
2
3
Função FAT:
SEGUNDA CHAMADA
Função FAT:
TERCEIRA CHAMADA
N=1
AUX = 1 * FAT(0)
N=0
condição de parada
atingida!!!
if (N = = 0)
return 1;
1
AUX = 1
2
3
Função FAT:
PRIMEIRA CHAMADA
Função FAT:
SEGUNDA CHAMADA
N=2
AUX = 2 * FAT(1)
N=1
AUX = 1 * FAT(0)
1
1
AUX = 1
AUX = 2
1
2
Programa principal:
main
Função FAT:
PRIMEIRA CHAMADA
NUMERO = 2
N=2
AUX = 2 * FAT(1)
RESULTADO = FAT(2)
2
2
AUX = 2
RESULTADO = 2
1
Problemas associados a
recursividade
Recursões que não terminam!!!
Um requisito fundamental é que a chamada
recursiva esteja sujeita à uma condição
booleana que em um determinado momento
irá se tornar falsa e não mais ativará a
recursão (condição de parada!).
Um subprograma recursivo deve ter
obrigatoriamente uma condição que controla
sua execução ou término, sob pena de
produzir uma computação interminável.
Multiplicação de números
naturais
Outro exemplo de definição recursiva
O produto A*B em que a e b são inteiros
positivos pode ser definido como A somado a
se mesmo b vezes. Essa definição é
ITERATIVA
Definição recursiva
a * b = a se b == 1
a * b = a * (b - 1) + a se b > 1
Exemplo:
6 * 3 = 6 * 2 + 6 = 6 * 1 + 6 + 6 = 6 + 6 + 6 = 18
Exercício: Converter num algoritmo recursivo.
Observe padrão de definições recursivas (caso
simples e avaliação de casos mais simples)
Por que não podemos usar definições como ??
n! = (n + 1)! / (n + 1)
a * b = a * (b + 1) - a
A seqüência de Fibonacci
Seqüência de inteiros do tipo
0, 1, 2, 2, 3, 5, 8, 13, 21, 34
Cada elemento da seqüência é a soma dos dois
elementos anteriores. Se permitimos que fib(0) = 0
e fib(1) = 1
fib(n) = n se n == 0 ou n == 1
fib(n) = fib(n-2) + fib(n-1) se n >= 2
Que diferencia a definição da seqüência de
Fibonacci da do fatorial e da multiplicação dos
números naturais??
A seqüência de Fibonacci
Note que:
fib(6) = fib(5) + fib(4)
= fib(4) + fib(3) + fib(3) + fib(2)
= fib(3) + fib(2) + fib(3) + fib(3) + fib(2)
E assim por diante. Que tem de errado??????
Seria mais eficiente lembrar quanto é fib(3) na primeira
vez que ele for chamado
Solução iterativa:
if (n <= 1)
return(n);
lofib = 0;
hifib = 1;
for (i = 2; i <= n; i++)
{
x = lofib;
lofib = hifib;
hifib = x + lofib;
}
return hifib;
Solução recursiva
FIB(5)
FIB(5)
FIB(4)
FIB(3)
FIB(5)
FIB(4)
FIB(3)
FIB(2)
FIB(3)
FIB(5)
FIB(4)
FIB(3)
FIB(2)
FIB(3)
FIB(2)
FIB(1)
FIB(5)
FIB(4)
FIB(3)
FIB(2)
FIB(1)
FIB(2)
FIB(3)
FIB(2)
FIB(1)
FIB(5)
FIB(4)
FIB(3)
1
FIB(1)
FIB(2)
FIB(3)
FIB(2)
FIB(1)
FIB(5)
FIB(4)
FIB(3)
1
FIB(2)
1
FIB(3)
FIB(2)
FIB(1)
FIB(5)
FIB(4)
2
1
FIB(2)
1
FIB(3)
FIB(2)
FIB(1)
FIB(5)
FIB(4)
2
1
FIB(3)
1
1
FIB(2)
FIB(1)
FIB(5)
FIB(4)
2
1
FIB(3)
1
1
1
FIB(1)
FIB(5)
FIB(4)
2
1
FIB(3)
1
1
1
1
FIB(5)
3
2
1
FIB(3)
1
1
1
1
FIB(5)
3
2
1
2
1
1
1
1
5
3
2
2
1
1
1
Resposta: O 5º termo da série é 5.
1
1
Torres de Hanoi
Problema criado pelo matemático francês Edouard
Lucas, em 1883;
3 torres;
x discos de vários tamanhos na primeira torre;
Objetivo: transportar os discos, 1 a 1, para a terceira
torre;
Regras:
nunca colocar um disco maior sobre um disco menor;
pode-se mover um único disco por vez;
nunca colocar um disco noutro lugar que não numa das
três hastes.
Torres de Hanoi
Torres de Hanoi
Lucas também criou uma “lenda” que dizia que em
Benares, durante o reinado do imperador Fo Hi,
existia um templo que marcaria o centro do
universo. Dentro deste templo, alguns monges
moviam discos de ouro entre 3 torres de diamante.
Deus colocou 64 discos de ouro em uma das torres
na hora da criação do universo. Diz-se que, quando
os monges completarem a tarefa de transportar
todos os discos para a terceira torre, o universo
terminará.
(como vai levar pelo menos 264 - 1 movimentos para
completar a tarefa, estamos a salvo por enquanto.
Assumindo que os monges realizem 1 movimento
por segundo, e não cometam erros, eles irão levar
um total de 585,000,000,000 anos.)
Como resolver?
Sejam 4 discos na torre 1.
Problema principal: Mover 4 discos da torre 1
para a torre 3.
Recursão!
Mover 4 discos da torre 1 para a torre 3:
mover 3 discos da torre 1 para a torre 2;
mover 1 disco da torre 1 para a torre 3;
mover 3 discos da torre 2 para a torre 3.
Recursão!
Mover 4 discos da torre 1 para a torre 3:
• mover 3 discos da torre 1 para a torre 2;
• mover 1 disco da torre 1 para a torre 3;
• mover 3 discos da torre 2 para a torre 3.
Recursão!
Mover 4 discos da torre 1 para a torre 3:
• mover 3 discos da torre 1 para a torre 2;
• mover 1 disco da torre 1 para a torre 3;
• mover 3 discos da torre 2 para a torre 3.
Recursão!
Mover 4 discos da torre 1 para a torre 3:
• mover 3 discos da torre 1 para a torre 2;
• mover 1 disco da torre 1 para a torre 3;
• mover 3 discos da torre 2 para a torre 3.
Divisão do problema
Logo o problema principal:
Se transforma em um problema menor:
Mover 4 discos da torre 1 para a torre 3
mover 3 discos da torre 1 para a torre 2;
mover 1 disco da torre 1 para a torre 3;
mover 3 discos da torre 2 para a torre 3.
Mas, como mover 3 discos?
Divisão do problema
Mover 3 discos da torre 1 para a torre 2:
mover 2 discos da torre 1 para a torre 3;
mover 1 disco da torre 1 para a torre 2;
mover 2 discos da torre 3 para a torre 2.
Divisão do problema
Mover 3 discos da torre 1 para a torre 2:
• mover 2 discos da torre 1 para a torre 3;
• mover 1 disco da torre 1 para a torre 2;
• mover 2 discos da torre 3 para a torre 2.
Divisão do problema
Mover 3 discos da torre 1 para a torre 2:
• mover 2 discos da torre 1 para a torre 3;
• mover 1 disco da torre 1 para a torre 2;
• mover 2 discos da torre 3 para a torre 2.
Divisão do problema
Mover 3 discos da torre 1 para a torre 2:
• mover 2 discos da torre 1 para a torre 3;
• mover 1 disco da torre 1 para a torre 2;
• mover 2 discos da torre 3 para a torre 2.
Algoritmo
Mover x discos, da torre a para a torre b:
mover (x-1) discos, da torre a para a torre c
mover 1 disco da torre a para a torre b
mover (x-1) discos, da torre c para a torre b
Função de parada:
Se o número de discos = 1, move direto.
Observações
Sejam 3 torres, com os números 1, 2 e 3.
Dadas 2 torres, como descobrir qual a
terceira?
Isto é, dadas as torres 1 e 3, como descobrir
que a outra torre é a 2?
Dadas as torres 2 e 3, como descobrir que a
outra torre é a 1?
Observação
Note: 1 + 2 + 3 = 6
Logo: A outra torre é
6 - (soma das torres indicadas)
Exemplos:
torres 1 e 2
Outra torre: 6 - (1 + 2) = 6 - 3 = 3
torres 2 e 3
Outra torre: 6 - (2 + 3) = 6 - 5 = 1
Solução
void HANOI(int ND, int DE, int PARA)
{
int OUTRA_TORRE = 6 - (DE + PARA);
if(ND == 1)
{
printf("Mover disco da torre %d para a
torre %d.\n", DE, PARA);
return;
}
HANOI(ND-1, DE, OUTRA_TORRE);
HANOI(1, DE, PARA);
HANOI(ND-1, OUTRA_TORRE, PARA);
return;
}
Solução
Digite o numero de discos: 4
Mover disco da torre 1 para a
Mover disco da torre 1 para a
Mover disco da torre 2 para a
Mover disco da torre 1 para a
Mover disco da torre 3 para a
Mover disco da torre 3 para a
Mover disco da torre 1 para a
Mover disco da torre 1 para a
Mover disco da torre 2 para a
Mover disco da torre 2 para a
Mover disco da torre 3 para a
Mover disco da torre 2 para a
Mover disco da torre 1 para a
Mover disco da torre 1 para a
Mover disco da torre 2 para a
Pressione qualquer tecla para
torre 2.
torre 3.
torre 3.
torre 2.
torre 1.
torre 2.
torre 2.
torre 3.
torre 3.
torre 1.
torre 1.
torre 3.
torre 2.
torre 3.
torre 3.
continuar . . .
Mover disco da torre 1 para a torre 2.
Mover disco da torre 1 para a torre 3.
Mover disco da torre 2 para a torre 3.
Mover disco da torre 1 para a torre 2.
Mover disco da torre 3 para a torre 1.
Mover disco da torre 3 para a torre 2.
Mover disco da torre 1 para a torre 2.
Mover disco da torre 1 para a torre 3.
Mover disco da torre 2 para a torre 3.
Mover disco da torre 2 para a torre 1.
Mover disco da torre 3 para a torre 1.
Mover disco da torre 2 para a torre 3.
Mover disco da torre 1 para a torre 2.
Mover disco da torre 1 para a torre 3.
Existem linguagens de programação que não
suportam recursividade como FORTAM, COBOL e
muitos linguagens de máquina
Porem soluções recursivas podem ser simuladas se
entendermos o conceito e funcionamento da
recursividade
Não é raro que um programador consiga enunciar
uma solução recursiva para um problema, as vezes
a solução recursiva é a mais natural e simples
porem as soluções recursivas são com freqüência
mais dispendiosas que a solução não recursiva.
Em geral uma solução não recursiva de um programa executará
com mais eficiência em termos de espaço e tempo, isso
acontece porque a solução não recursiva evita o trabalho extra
de entrar e sair de um bloco e o empilhamento de desnecessário
de variáveis
Contudo verificaremos que a solução recursiva é as vezes o
método mais natural e lógico de desenvolver como é o caso das
torres de Hanoi e menos propenso a erros
Conflito EFICIENCIA DA MAQUINA X EFICIENCIA DO
PROGRAMADOR
Nestes casos versões simuladas dos casos recursivos é uma
excelente solução
1.
2.
3.
Que acontece quando uma função é chamada??
Passar argumentos: Para um parâmetro em C
uma copia é criada localmente dentro da função e
quaisquer mudança e feita nessa copia local. O
original não é alterado
Alocar e inicializar variáveis locais: Depois que os
argumentos forem passados as variáveis locais da
função serão alocadas. As diretamente
declaradas na função e as temporais
Transferir o controle para a função: Deve ser
passado o endereço de retorno como parâmetro
1.
2.
3.
Quando retorna que acontece?
O endereço de retorno é recuperado e
armazenado num logar seguro
A área de dados da função é liberada
Usa-se um desvio para o endereço de
retorno salvo anteriormente, deve também
salvar se os valores de retorno para que o
chamador possa recuperar esse valor.
Programa principal
Procedimento b
Endereço de
retorno
Procedimento c
Chamada de b
Chamada de c
Chamada de d
Endereço de
retorno
Programa principal
Procedimento b
Endereço de
retorno
Procedimento c
Chamada de b
Chamada de c
Chamada de d
Procedimento d
Endereço de
retorno
controle
Endereço de
retorno
controle
Observe que a seqüência de endereços de retorno
forma uma pilha isto é o mais recente endereço de
retorno a ser incluído na cadeia é o primeiro a ser
removido. Em qualquer ponto só poderemos
acessar o endereço de retorno a partir da função
atualmente em execução que representa o topo da
pilha. Quando uma pilha é esvaziada (isto é quando
a função retornar) será revelado um novo topo
dentro da rotina de chamadas.
Chamar uma função tem o efeito de incluir um
elemento numa pilha e retornar da função de
eliminá-lo
Cada vez que uma função recursiva chama a
si mesma uma área de dados totalmente
nova para essa chamada precisa ser
alocada. Essa área contem todos os
parâmetros variáveis locais temporárias e
endereços de retorno. Todo retorno acareia a
liberação da atual área de dados. Sugere-se
o uso de uma pilha
Poderemos usar uma estrutura do tipo
#define MAXSTACK 50;
struct stack{
int top;
struct dataarea item[MAXSTACK]
}
Passos da conversão
Desenvolver caso recursivo
Desenvolver simulação com todas as pilhas e
temporários
Eliminar todas as pilhas e variáveis supérfluas
Quando uma pilha não pode ser eliminada da
versão não recursiva e quando a versão
recursiva não contem nenhum dos parâmetros
adicionais o variáveis locais, a versão recursiva
pode ser tão o mais veloz que a não recursiva
sob um compilador eficiente. Exemplo Torres de
Hanoi
O fatorial que não precisa pilha na
implementação não recursiva e a geração
de números de fibonacci onde temos uma
chamada desnecessária e também não
precisa de pilha a solução recursiva deve ser
evitada na pratica
Até onde uma solução recursiva pode ser
transformada em direta dependerá do
problema
e
a
engenhosidade
do
programador
Simulação de Recursividade
struct stack {
int top;
struct dataarea item[MAXSTACK];
};
void popsub(struct stack *s, struct dataarea *a){
*a = s->item[s->top];
s->top--;
}
void push(struct stack *s, struct dataarea *a){
s->top++;
s->item[s->top] = *a;
}
Simulação de Recursividade
int fact(int n){
int x, y;
if (n==0)
return(1);
x = n-1;
y = fact(x);
return (n * y);
}
Ponto 1 para retornar (labe2 y = result;)
Ponto 2 para retornar (label1: return(result);)
Simulação de Recursividade
struct dataarea{
int param;
//parametro simulado (n)
int x;
//variaveis atuais da chamada
long int y;
short int retaddr; //endereço de retorno (1 ou 2)
};
switch(i){
case 1: goto label1; //retorno ao prg principal
case 2: goto label2; //simulando à atribuição do valor
//retornado à variavel y na execução
//anterior de fact
}
Simulação de Recursividade
//retorno a partir de fact
result = valor a ser retornado;
i = currarea.retaddr;
popsub(&s, &currarea);
switch(i){
case 1: goto label1;
case 2: goto label2;
}
Simulação de Recursividade
//chamada introduz a atual área na pilha
//atualiza os valores dos parâmetros e
//transfere o controle para o inicio da rotina
push(&s, &currarea);
currarea.param = currarea.x;
currarea.retaddr = 2;
goto start;
long int simfact(int n){
struct dataarea currarea;
struct stack s;
short i;
long int result;
s.top = -1;
currarea.param = 0;
currarea.x = 0;
currarea.y = 0;
currarea.retaddr = 0;
push (&s, &currarea);
currarea.param = n;
currarea.retaddr = 1;
start:
if (currarea.param ==0){
result = 1;
i = currarea.retaddr;
popsub(&s, &currarea);
switch(i){
case 1: goto label1;
case 2: goto label2;
}
}
currarea.x = currarea.param -1;
push(&s, &currarea);
currarea.param = currarea.x;
currarea.retaddr = 2;
goto start;
label2:
currarea.y = result;
result = currarea.param*currarea.y;
i = currarea.retaddr;
popsub(&s, &currarea);
switch(i){
case 1: goto label1;
case 2: goto label2;
}
label1:
return result;
}
Simulação de Recursividade
São necessárias todas as variáveis temporais? (n,
x, y)
n necessita ser empilhada (y=n*fact(x);)
Embora x seja definida na chamada nunca será
usada depois de retornar (se x e y não fossem
declaradas na função e sim globais, a rotina
funcionaria igualmente bem.
x e y não precisam ser empilhadas
E o endereço de retorno?? Só existe um endereço
de retorno importante (fact), mas se não
empilhamos a area de dados artificial UNDERFLOW
Podemos fazer popandtest...
#include<stdio.h>
#include<stdlib.h>
#define MAXSTACK 50
struct stack {
int top;
int param[MAXSTACK];
};
int empty(struct stack *s){
return (s->top == -1);
}
void popandtest(struct stack *s, int *a,
short int *und){
if (!empty(s)){
und = 0;
*a = s->param[s->top];
s->top--;
return;
}
*und = 1;
}
void push(struct stack *s, int *a){
s->top++;
s->param[s->top] = *a;
}
int simfact1(int n){
struct stack s;
short int und;
long int result, y;
int currparam, x;
s.top = -1;
currparam = n;
start:
if (currparam == 0){
result = 1;
popandtest(&s, &currparam, &und);
switch(und){
case 0: goto label2;
case 1: goto label1;
}
}
x = currparam -1;
push(&s, &currparam);
currparam = x;
goto start;
label2:
y = result;
result = currparam*y;
popandtest(&s, &currparam, &und);
switch(und){
case 1: goto label1;
case 0: goto label2;
}
label1:
return(result);
}
Simulação de Recursividade
As instruções goto start e goto label2;
?????? Repetições de código
Para currparam == 0 e currparam != 0
popandtest(&s, &currparam, &und);
switch(und){
case 0: goto label2;
case 1: goto label1;
}
int simfact2(int n){
struct stack s;
short int und;
long int y;
int x;
s.top = -1;
x = n;
start:
if (x == 0) y = 1;
else{
push(&s, x--);
goto start;
}
label1:
popandtest(&s, &x, &und);
if(und == 1) return(y);
label2:
y*=x;
goto label1;
}
Repetição normal
Repetição normal
int simfact3(int n){
struct stack s;
short int und;
long int y;
int x;
s.top = -1;
x = n;
start:
while (x!=0) push(&s, x--);
y = 1;
popandtest(&s, &x, &und);
label1:
while (und == 0){
y*=x;
popandtest(&s, &x, &und);
}
return (y);
}
int simfact4(int n){
long int y;
int x;
for(y=x=1; x<=n; x++)
y*=x;
return (y);
}
Download