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); }