Métodos de Programação I Ana Maria de Almeida 92. Um objecto (uma função, um método) diz-se recorrente se é definido em termos de si próprio.

Documentos relacionados
Métodos de Ordenamento e

Capítulo VII : A Recorrência

termo Para resolver o problema, basta construir um Módulo (procedimento) para cada uma das definições: factor termo enquanto '+' ou '-'

tipoveiculo = (bicicleta, motociclo, motorizada, automovel, autocomreb, camioneta, autocarro, camiao, reboque); veiculo : tipoveiculo;

Métodos de Programação I Ana Maria de Almeida

Procedimento. Função. Selecção Condicional - a instrução if-then-else. expressão if lógica then instrução else instrução

2.2.5 EXPRESSÕES - Regras para o cálculo de valores

Pesquisa: operação elementar

Ciclo com Contador : instrução for. for de variável := expressão to. expressão do instrução

Métodos de Programação I Ana Maria de Almeida

Análise e Desenvolvimento de Algoritmos (2006/2007)

Programação: Vetores

Capítulo V : Um Tipo Estruturado de Dados: o array

ESTRUTURAS DE REPETIÇÃO - PARTE 2

Informática para Ciências e Engenharias 2013/14. Teórica 7

Algoritmos e Estruturas de Dados I (DCC/003) Estruturas Condicionais e de Repetição

5. Expressões aritméticas

Capítulo VI : Subprogramas

Números Inteiros Algoritmo da Divisão e suas Aplicações

Informática para Ciências e Engenharias 2014/15. Teórica 7

Tipos de Dados Dinâmicos

Capítulo V : A Linguagem Pascal Um Tipo Estruturado de Dados: o array 18. Para limite de n até 2 (* passagens de 1 até (n-1) *)

10 a Aula - Operadores de Molde ( Casting ). Atribuição de Memória. Ponteiros. Enumerados. Mestrado em Engenharia Física Tecnológica

SSC304 Introdução à Programação Para Engenharias

SEBENTA INTRODUÇÃO Á ALGORITMIA

Linguagem e Técnicas em Programação. Gilson de Souza Carvalho

Laços de repetição for, while, do-while

MC102 Algoritmos e Programação de Computadores

Introdução à Computação II Unesp Rio Claro 2012Prof. Rafael Oliveira

Métodos de Programação I Ana Maria de Almeida

Programação Estruturada

Estrutura de Dados Polinómio

INE5403 FUNDAMENTOS DE MATEMÁTICA DISCRETA

UNIVERSIDADE FEDERAL RURAL DO SEMI-ÁRIDO CURSO: CIÊNCIA DA COMPUTAÇÃO. Prof.ª Danielle Casillo

Aula prática 5. Funções Recursivas

Variáveis e instruções de repetição

Pascal. -Cabeçalho do programa. - label - const - type - var - procedure - function. - integer - real - byte - boolean - char - string

Critérios de Correcção. Informática

Linguagem e Técnicas em Programação. Gilson de Souza Carvalho

Estruturas de Repetição

Ciência da Computação

Instituto Superior Politécnico de VISEU. Escola Superior de Tecnologia

Um algoritmo deve conter passos não ambíguos, executáveis e que sejam terminados quando seguidos.

Informática II Cap. 4-4

Introdução a Algoritmos Parte 08

1 Congruências e aritmética modular

Aulas Anteriores. Detalhes da linguagem de programação

Oficina de Introdução de Programação usando Linguagem Python Prof. Ms. Perini

Prática 10 - Funções

Algoritmos de pesquisa

Linguagem e Técnicas em Programação. Gilson de Souza Carvalho

Marina Andretta. 10 de outubro de Baseado no livro Introduction to Linear Optimization, de D. Bertsimas e J. N. Tsitsiklis.

Investigação Operacional

Desenho e Análise de Algoritmos CC /2018. Folha 1 - Revisão: Escrita de algoritmos em pseudo-código e verificação de correção

Introdução à Programação 2006/07. Algoritmos

Resolução De Problemas Em Informática. Docente: Ana Paula Afonso Resolução de Problemas. 1. Analisar o problema

MC-102 Aula 06 Comandos Repetitivos

Linguagem Computacional. Estruturas de Controle: Estruturas de Repetição. Prof. Dr. Adriano Cansian Prof. Dr. Leandro Alves Neves

1º Exame de INTRODUÇÃO À PROGRAMAÇÃO Licenciatura em Engenharia Mecânica 30 de Junho de º Semestre

Linguagem Computacional. Estruturas de Controle: Estruturas de Decisão ou de Seleção. Prof. Dr. Adriano Cansian Prof. Dr. Leandro Alves Neves

BUSCA. 4.1 Busca seqüencial

Sumário. Ciência da Computação. Prof. Dr. Leandro Alves Neves. Aula 10. Algoritmos e Programação. Enquanto (Teste no início) Repeat (Teste no final)

LABORG. Parte 4 Programação em Linguagem de Montagem do MIPS. Fernando Gehm Moraes Matheus Trevisan Moreira

Aula 4 Introdução ao C

Vejamos agora mais alguns exemplos de problemas envolvendo seqüência de números.

Introdução à Ciência da Computação

6. Determinação do Conjunto dos Estados Atingíveis

Algoritmos e Programação

Algoritmos de pesquisa

UNIVERSIDADE FEDERAL RURAL DO SEMI-ÁRIDO CURSO: CIÊNCIA DA COMPUTAÇÃO. Prof.ª Danielle Casillo

13 a Aula - Instruções Condicionais. Ciclos. Pré-processador. Variáveis de ambiente. Mestrado em Engenharia Física Tecnológica

Módulo 3 Subprogramas

Matemática Discreta. Fundamentos e Conceitos da Teoria dos Números. Universidade do Estado de Mato Grosso. 4 de setembro de 2017

Desenvolvimento de programas

Teste de P1 12 de Dezembro 2001

a = bq + r e 0 r < b.

Recursão. Definição. Características. Recursividade 31/08/2010

Estruturas Condicionais

Aula 7 - Mais problemas com inteiros

Linguagem de programação: Pascal

Processamento da Informação

Análise de complexidade

CAPÍTULO 5 - UMA LINGUAGEM ALGORÍTMICA

Correção do 1º Exame de INTRODUÇÃO À PROGRAMAÇÃO Licenciatura em Engenharia Mecânica 30 de Junho de 2006, 2º Semestre

Professor: Domingos Equipe Haskell: Lucas Montesuma, Francisco Leonardo CONCEITOS DA LINGUAGEM DE PROGRAMAÇÃO CÁLCULADORA EM HASKELL

FACCAT Sistemas de Informação. Estruturas de Dados

Waldemar Celes e Roberto Ierusalimschy. 29 de Fevereiro de 2012

Oficina de Python Prof. Me. José Carlos Perini

Refinamentos sucessivos

NÚMEROS INTEIROS E CRIPTOGRAFIA UFRJ

Modularização. Prof. Antonio Almeida de Barros Junior

Aula 11 - Repetições Encaixadas

Introdução à Programação em C. Prof. Ricardo Teixeira Tecnologia em Mecatrônica Industrial SENAI

A palavra ALGORITMO teve origem com um Matemático Persa, al. Khawarizmi. O seu trabalho mais famoso foi Al-jabr walmuquabalah,

Instituto Superior Politécnico de VISEU. Escola Superior de Tecnologia

Oficina de Python Prof. Me. José Carlos Perini

ESTRUTURAS DE REPETIÇÃO - PARTE 1

6. Pesquisa e Ordenação

Implementando subprogramas

Recursão. Prof. Cristiano André da Costa. [Versão de Março de 2000] Definição

Transcrição:

Métodos de Programação I Ana Maria de Almeida 92 3.2 Recorrência Um objecto (uma função, um método) diz-se recorrente se é definido em termos de si próprio. Figura 3.6: Triangulo de Serpinski (imagem recorrente) Um exemplo matematicamente famoso de uma definição recorrente é o da função factorial de n (sendo n um inteiro não negativo): o factorial de n é o produto do próprio valor de n pelo factorial de n 1, onde, por convenção, se decidiu que factorial de zero é um. Formalmente temos, { n (n 1)! se n 1 n! = 1 se n = 0 Um algoritmo iterativo para, dado n IN, calcular o seu factorial poderia, então, ser: Algoritmo Iterativo para o cálculo de factorial: Dados: n do tipo inteiro não-negativo Saída: Valor de n! fact 1; para i a variar de 1 a n: fact fact * i; devolver valor de fact; Cuja implementação seria dada por: Implementação Iterativa para o Cálculo de Factorial: function factorial (n : 0..Maxint): 0..Maxint; var fact : integer; i : 1..Maxint; {inicializacao de fact ao valor de 0!} fact := 1; for i:=1 to n do {calculo de n! se n>0} fact := fact * i; { fact = n! se n>=0 } factorial := fact;

Métodos de Programação I Ana Maria de Almeida 93 No entanto, este algoritmo (e este subprograma) nada tem de recorrente: efectua n iterações e, em cada uma, actualiza o valor de uma variável, multiplicando-lhe o valor i da iteração actual. Assim, este algoritmo implementa a fórmula iterativa que diz que o valor do factorial é o produto de todos os naturais inferiores ou iguais a n, ou seja, n! = 1 2 3... (n 1) n Como calcular o valor de n! recorrentemente? Nada mais simples: basta implementar directamente a fórmula recorrente anteriormente apresentada, como se pode ver a seguir: Implementação Recorrente para o Cálculo de Factorial: function factorialr (n : 0..Maxint): 0..Maxint; if n = 0 then factorialr := 1 else factorialr := n * factorial(n-1); Notas: 1. Observe que o identificador da função (factorialr) aparece várias vezes no bloco da função: duas vezes do lado esquerdo de instruções de atribuição e uma vez do lado direito. Quando aparece do lado esquerdo é a instrução necessária para atribuir um valor para a function factorialr devolver para o exterior (uma quando sai do braço do then outra para quando sai pelo braço do else), logo, não tem (nem deve ter) parentesis e/ou parâmetros. Para construir a solução recorrente é, obviamente, necessário fazer uma chamada recorrente à própria função, o que acontece no else, quando, no lado direito da instrução de atribuição, efectuamos a chamada recorrente, logo, com o necessário valor de parâmetro que permite calcular o valor (ainda) em falta. 2. A chamada recorrente diminui o tamanho do que é necessário calcular: na entrada da função temos um parâmetro de valor n e a chamada recorrente é feita usando um valor menor: n 1. A versão recorrente de um algoritmo é, muitas vezes, mais simples, clara e elegante do que a correspondente versão iterativa, como se pode ver pelo exemplo que se segue. Para calcular o máximo divisor comum entre dois números inteiros não negativos, a, b IN, podemos usar o Algoritmo de Euclides, que se baseia nas propriedades seguintes,: P1) mdc(a, 0) = a P2) mdc(a, b) = mdc(b, a mod b) se b 0

Métodos de Programação I Ana Maria de Almeida 94 Implementação Recorrente para o Cálculo do Máximo Divisor Comum: function mdcrec (a, b : 0..Maxint): 0..Maxint; var aux: 0..Maxint; if b=0 then mdcrec = a { usando P1 } else mdcrec := mdcrec(b, a mod b) { usando P2 } Implementação Iterativa para o Cálculo do Máximo Divisor Comum: function mdcite (a, b : 0..Maxint): 0..Maxint; var aux: 0..Maxint; while b <> 0 do aux := a mod b; a := b; b := aux; {fim while} mdcite := a Claramente, enquanto que a versão recorrente é concisa e reflecte directamente as propriedades expressas atrás, o código da versão iterativa é muito mais hermético e requer algum tempo para se perceber o que está a fazer (Experimente!). Exercício: Implemente a versão original do Algoritmo de Euclides, Para calcular o máximo divisor comum entre dois números, subtraia o menor ao maior, até ficarem iguais. quer recorrentemente, quer iterativamente. A resolução recorrente pode, ainda, evitar o uso de estruturas auxiliares para guardar temporariamente informação. Por exemplo, imagine que necessita de implementar um módulo que leia uma palavra (terminada por um espaço em branco), e escreva as suas letras por ordem inversa. Uma solução iterativa necessita de conhecer todas as letras da palavra para poder começar a inverter. Assim, seria necessário ler toda a palavra e guardar, por exemplo, num vector de caracteres, as letras lidas.

Métodos de Programação I Ana Maria de Almeida 95 1 2 3 4 5 6 7 8... Dim e x e m p l o /... / fim Após o término da leitura, poderíamos começar a escrever essas letras, do fim (última posição que guardou letra) para o ínicio, obtendo, assim, o desejado efeito de inversão. Este vector tem, como qualquer tabela, que ser declarado como um array com um dado tamanho fixo. Neste exemplo, usamos uma estrutura de tabela com índices a variar de 1 a Dim, onde esta última dimensão será um valor constante pré declarado. Obviamente, ninguém sabe, à partida, quantos caracteres vão ser lidos, por isso, se pensarmos em manter este algoritmo o mais geral possível, devemos fazer uma previsão por excesso do número máximo de caracteres admitidos e, portanto, podemos fixar, por exemplo, dim = 100. É, no entanto, fácil de imaginar que, de um modo geral, na maior parte das vezes, a tabela vai ter um valor fim muito menor de que 100. Em conclusão, usamos uma estrutura que vai desperdiçar, em geral, memória, só como auxiliar na resolução da escrita invertida de uma palavra. Se usarmos a recorrência, não é necessário usar qualquer tipo de memória auxiliar, senão veja-se o seguite algoritmo e respectiva implementação: Algoritmo Recorrente para a Escrita Invertida de uma palavra: Inverter: ler a próxima letra; se letra não é um espaço em branco então Inverter o resto da palavra); escrever letra; Implementação: procedure inverter; var letra:char; read(letra); if letra <> write(letra:1) then inverter; A versão recorrente é mais eficiente em termos de memória pois vai usar, exactamente, tantos caracteres quantos os necessários para inverter a palavra, nem mais, nem menos. Note que o algoritmo recorrente apresentado, só vai construir a solução após o final da chamada ao procedimento inverter. Ou seja, vai fazendo sucessivas chamadas recorrentes e, quando chega ao fim da palavra (encontra um espaço em branco), pára de recorrer a si mesmo e, imediatamente

Métodos de Programação I Ana Maria de Almeida 96 antes de sair do bloco, escreve a letra que tinha guardado. Este método de construção da solução é denominado Recorrência com construção à saída. Vejamos outro exemplo bastante simples e, também, de inversão: escrever, por ordem inversa, os dígitos de um dado número inteiro. Neste caso, é dado o número, pelo que não necessitamos de ler (recolher) informação, bastando passar à inversão própriamente dita. Temos, então, o seguinte algoritmo: Algoritmo Recorrente para a Escrita Invertida dos dígitos de um dado número: Dados: n IN Saída: escrita invertida dos dígitos de n Inverter(numero): escrever o último dígito; se ainda houver dígitos então Inverter(resto do número); Implementação: procedure inverten(numero: 0..MaxInt); var aux: 0..Maxint; write(numero mod 10:1); aux := numero div 10; if aux <> 0 then inverten(aux); Mais uma vez, temos uma solução extremamente simples e engenhosa para resolver o problema dado, só que, agora, a solução foi construída à entrada do procedimento e, só depois, se faz chamada recorrente: este método pode ser denominado por Recorrência com construção à entrada. Na verdade, a recorrência é um modo bastante confortável de raciocinar. Quando temos um problema a resolver com dimensão n, pensamos no seguinte modo: conseguir resolver um passo simples e deixar a recorrência tratar os restantes n 1 passos. Assim, como regras práticas para a elaboração correcta de uma solução recorrente temos as seguintes: 1. Definir o problema em termos de um problema do mesmo tipo mas de menor dimensão 2. Aplicar a recorrência aos restantes n 1 passos. Obviamente que, se não assegurarmos que existe um caso especial que obrigue a recorrência a parar, vamos ter programas infinitos (que só páram quando esgotarem todos os recursos da máquina). Assim, como regras práticas para a elaboração correcta de uma solução recorrente temos as seguintes:

Métodos de Programação I Ana Maria de Almeida 97 1. Definir o problema em termos de um problema do mesmo tipo mas de menor dimensão. 2. Identificar (pelo menos) um caso particular que possa servir como caso de paragem para a recorrência (isto é, onde não seja necessário fazer uma chamada recorrente). 3. Garantir que o modo como o tamanho do problema, na chamada recorrente, diminui, garante atingir o caso de paragem. Exercícios: Construa uma solução recorrente pura, isto é, sem ciclos, para escrever uma figura como a abaixo (cuja altura pde variar entre 1 e 9): 1 1 2 1 1 2 3 2 1 1 2 3 4 3 2 1 1 2 3 2 1 1 2 1 1 3.2.1 Pesquisa Recorrente Existem algoritmos típicos (cuja versão iterativa, de resto, já conhecemos 1 ) que podem ser (re)escritos de forma recorrente. Nomeadamente, os algoritmos de pesquisa de pertença de um dado elemento a uma dada tabela prestam-se, de um modo bastante natural, a este tipo de estratégia de computação, como podemos ver em seguida. Pesquisa Recorrente numa Tabela Suponha-se dada uma tabela de valores inteiros, v[1..n] (onde n DimM ax). Considere-se também um dado valor inteiro, numx. Pretende-se devolver uma resposta booleana para saber se o valor numx pertence, ou não, a v e, caso pertença, qual a posição em que foi encontrado. Algoritmo Recorrente para a Pesquisa (numa tabela qualquer): Dados: numx ZZ e v[1..n], de elementos inteiros e com n DimMax Saída: falso numx v[1..n] e verdadeiro se i {1,..., n} : numx = v[i] Ora, a pesquisa vai ter que passar a tabela, posição a posição, para verificar a igualdade entre o elemento respectivo e o valor dado, parando assim que for detectada uma igualdade. O 1 Ver páginas 42 a 45 do Capítulo 2.

Métodos de Programação I Ana Maria de Almeida 98 algoritmo vai ser, então, extremamente simples pois, pesquisar recorrentemente a tabela é escolher uma posição para verificar (a primeira ou a última): se for igual parar e devolver índice; senão, pesquisar o resto da tabela. Ou seja, escolhendo a última posição com informação útil da tabela: Pesquisar(numx em v[1..n]): se numx = v[n] devolver n; senã o devolver pesquisar(numx em v[1..n 1]) Tendo em atenção as regras práticas para a construção recorrente de uma solução, vemos que, de facto, dado um n positivo, a chamada recorrente diminui de uma unidade a dimensão total do problema. No entanto, será que garantimos que, para todos os casos possíveis, está assegurada a paragem da recorrência? Na verdade, só garantimos a paragem da recorrência se encontrarmos um valor igual ao procurado mas, se esse valor não pertencer à tabela dada, temos que parar de chamar assim que isso se tornar notório. Neste caso, sabemos que já percorremos toda a tabela quando n < 1, logo, só fazemos algum tipo de trabalho na chamada à pesquisa se e só se n 1. Finalmente, uma proposta de implementação completa pode ser a seguinte: Implementação: procedure pesquisar(num: integer; v : vector; dim : 0..DimMax; var posicao : 0..DimMax); if dim >= 1 { se ainda existem elementos nao vistos no vector } then if v[dim] = num then posicao := dim else pesquisar(num, v, dim-1, posicao) else posicao := 0; Esta versão, apesar de correcta, não é muito eficiente pois, como vimos já nas aulas teóricas, a recorrência vai abrindo, com cada nova chamada recorrente, mais um novo bloco de execução, mantendo as chamadas anteriores abertas até as conseguir fechar, uma a uma, do interior para o exterior. Para cada chamada que fica pendente até poder fechar, são feitas cópias de todos os parâmetros e de todas as variáveis locais. Por exemplo, na implementação anterior, se o vector tiver 1000 posições, (ou seja, se DimMax = 1000) e se o valor a pesquisar não se encontrar na lista, serão feitas 1000 chamadas recorrentes, donde a memória ocupada para a resolução desta pesquisa será, aproximadamente, 1000 1000 inteiros devido a cópias de vector 1000 inteiros devido a cópias de num + 1000 inteiros devido a cópias de dim 1002000 unidades de memória para inteiros

Métodos de Programação I Ana Maria de Almeida 99 O que é uma quantidade enorme de memória usada quando só precisamos de uma resposta simples. Se notarmos que, em cada chamada recorrente, apenas alteramos o parâmetro n, podemos reescrever este algoritmo do modo seguinte. Implementação de Pesquisa com Recorrência Encapsulada: procedure pesquisa(num: integer; v : vector; dim : 0..DimMax; var posicao : 0..DimMax); function pesq(n : integer): integer; { pesquisa num em v[1..n] } if n >= 1 { se ainda existem elementos nao vistos no vector } then if v[n] = num then pesq := n else pesq := pesq(n-1) end else pesq := 0; { fim pesq } posicao := pesq(dim); { fim pesquisa } Assim, escondemos a recorrência no interior do procedimento pesquisar, usando um módulo auxiliar realmente recorrente, neste caso a função pesq, onde v e num funcionam como constantes globais às diferentes chamadas recorrentes. Deste modo, não é, portanto, preciso copiar nada, a não ser o único paraâmetro que é, de facto, alterado com a estratégia recorrente: a última posição a testar no momento. A este método de implementação da recorrência chamamos (como se pode adivinhar pelo cabeçalho desta implementação) Recorrência Encapsulada Pesquisa Recorrente numa Tabela Ordenada Um outro exemplo para o uso da recorrência encapsulada vem da implementação recorrente da pesquisa binária. Mais uma vez, e se formos observar com atenção o modo como descrevemos a estratégia deste método, ele é claramente recorrente 2, pelo que a sua transcrição para um algoritmo não iterativo é muito directa. 2 Ver páginas 43 a 45 do capítulo 2.

Métodos de Programação I Ana Maria de Almeida 100 Algoritmo Recorrente para a Pesquisa Binária (tabela ordenada): Dados: numx ZZ e v[1..n], de elementos inteiros e com n DimMax Saída: falso numx v[1..n] e verdadeiro se i {1,..., n} : numx = v[i] Pesquisar(numx em v[i..j]): se j >= i então: se numx = v[meio] devolver meio; senão: se numx < v[meio] pesquisar(numx em v[i..meio 1]); senão: pesquisar(numx em v[meio + 1..j]) Implementação de Pesquisa binária: procedure pesquisa(num: integer; v : vector; dim : 0..DimMax; var posicao : 0..DimMax); function pesqb(esq, dir : integer): integer; { pesquisa bin aria num em v[esq..dir] } var meio : 0..DimMax; if esq <= dir { se ainda ha + elementos no vector } then meio := (esq+dir) div 2; if v[meio] = num then pesqb := meio else if v[meio] > num then pesqb := pesqb(esq, meio-1) else pesqb := pesqb(meio+1, dir); end else pesqb := 0; { fim pesqb } posicao := pesq(dim); { fim pesquisar } Note-se que, mais uma vez se recorreu ao encapsulamento da recorrência para evitar cópias desnecessárias. Deve notar-se ainda que, o procedimento principal de pesquisa é exactamente igual ao anterior, só mudando o módulo interior, que efectua (recorrentemente) a pesquisa propriamente dita.