3 Lista de Exercícios e algumas notas Estrutura de Dados I Recursividade simples Entrega no dia .... (veja no Blackboard) a
JM Josko, LCD Gonçalv Gonçalves es 6 de abril de 2016
NÃO É APOSTILA. NÃO NÃO SUBSTITUI A LEITURA DE LIVROS. NÃO DESOBRIGA AO ESTUDO DE LIVROS. É APENAS UMA LISTA LISTA DE EXERCÍCIOS! Apenas contém um quick start para começar a codicar, e tenta cobrir alguns detalhes omitidos nos livros.
1
SUMÁRIO
SUMÁRIO
Sumário 1 Regra Regrass para a entr entrega ega
3
2 Algu Algumas mas notas sobre recu recursão rsão
4
3 Implementação Implementação de métodos recursivos simples 5 3.1 Recu Recursão rsão mútua ou indire indireta ta . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 3.2 Probl Problemas emas com a recor recorrênci rênciaa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 3.2.1 3.2 .1 A pil pilha ha da máqu máquina ina . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 3.2.2 3.2 .2 Cas Casoo bas basee omi omitid tidoo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 3.2.3 3.2 .3 O(s O(s)) cas caso(s o(s)) bas basee exi existe ste(m) (m)mas mas a seq sequên uência cia de cha chamad madas as rec recurs ursiv ivas as não con conve verg rgee par paraa ele(s). Ou, o(s) caso(s) base existe(m) mas a sequência pode não convergir para ele(s). 9 3.2.4 Muito espaç espaçoo na área de pilha reque requerido rido . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3.2.5 Nú Número mero de recál recálculos culos exc excessiv essivos os . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 4 Exercises diversos (não é a lista) com arrays e strings strings 14 4.11 Co 4. Com m ar arra rays ys . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 4.22 Co 4. Com m st stri ring ngss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 5 Exercise Exercisess (a lista lista)) 21 5.1 Re Recur cursão são sim simple pless . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 5.2 Re Recur cursão são em arr array ayss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 5.33 Re 5. Recu curs rsão ão em Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2
1 Regras para a entrega
SUMÁRIO
1 Regras para a entrega 1. Os exercícios que não necessitam de rodar numa máquina: • são manuscritos (feitos à mão). • devem vir com o enunciado e com a numeração que aparece na lista (não precisa ser manuscrito); • devem estar na mesma sequência dos enunciados na lista. • a resposta deve vir após cada um dos enunciados. • exercícios com alternativas: devem ser entregues com a resolução/justicativa e não apenas com a alternativa marcada. • podem ser resolvidos com o método a sua escolha, exceto quando indicado ao contrário. Escolha sempre o método mais simples tal que responda à pergunta. • devem estar todos num mesmo pdf: a digitalização pode ser feita com celular ou assemelhado. O resultado nal deve ser legível. • devem conter o nome dos integrantes na primeira página de resolução (escrevam os nomes antes de digitalizar). 2. Os exercícios que necessitam de rodar numa máquina (códigos): • tipos, métodos estáticos de uso geral não relativos ao tipo e códigos de teste em arquivos separados. – tipos, abstratos ou não, em um único .java, sem main()s. – métodos estáticos, em um único .java, sem main()s. – códigos de teste, apenas o main(), • Testes de diversos métodos podem estar em um único main(), mencionando qual é o exercício: E1.22, E1.44, etc. Coloque um comentário antes da chamada do método. • todo o .java deve conter os nomes completos e RGMs dos integrantes • os códigos devem estar indentados (nem leio caso não estejam) 3. Todos os exercícios • entregar apenas os não resolvidos • devem ter os nomes completos e RGMs dos integrantes dos grupos num arquivo .txt separado na raiz do zip. Destaque o menor RGM; • Devem estar contidos em um único zip: 1 pdf para os exercicios manuscritos, além dos restantes 4. Regras gerais de orientação • grupos de 2 a 4 integrantes • tamanho máximo do zip entregue = 3MBytes. • Listas iguais, sem nenhum capricho, com resoluções que só quem fez entende (se entende), que subvertem alguma das regras: 0. Bom português nas justicativas. • um grupo mantém os mesmos integrantes durante todo o curso. O integrante de um grupo pertence apenas àquele grupo. No caso de integrantes promíscuos, 0 para os grupos (que são também promíscuos e portanto) envolvidos.
Para esta lista, entregar
• Seja n o número do integrante com o menor RGM do grupo (incluindo-se dígitos de controle e ans se for o caso). – Se n for par, entregar apenas os exercícios pares das Seções 5.2 e 5.3 (Ex: E5.32 é um exercicio par) – Se n for ímpar, entregar apenas os exercícios ímpares das Seções 5.2 e 5.3 (Ex: E5.11 é um exercicio ímpar)
3
2 Algumas notas sobre recursão
SUMÁRIO
2 Algumas notas sobre recursão A recursão1 não somente é uma ideia matemática elegante mas algo que faz inclusive parte da nossa linguagem. Recorrer signica denir algo parcialmente em termos desse algo, e é considerada uma técnica matemática poderosa, especialmente utilizada em demonstrações por indução. Os exemplos maissimples e familiares consistem de denições de “funções” matemáticas2 . Por exemplo, • E. 2.1 Fatorial: n! = n · (n − 1) · (n − 2) · (n − 3) · . . . · 1 n! =
�1
, n = 0 n · (n − 1)! , n > 0
• E. 2.2 Potência positiva (n ≥ 0) de x: xn = x · x · x · . . . · x (o x aparece n vezes) ou 1 se n = 0. n
x =
�1
n−1
x·x
, n = 0 , n > 0
(2.3) «
• E. 2.4 Sequência de Fibonacci: 1 , 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, . . . fibn =
�1
fibn−1 + fibn−2
, n = 0 ou n = 1 , n > 1
• E. 2.5 Série harmônica: h(n) = 1 + 1/2 + 1/3 + . . . + 1/n h(n) =
�0
h(n − 1) +
1 n
,n ≤ 0 , n > 0
A abordagem típica da recursão consiste na reaplicação da fórmula atéque o valor conhecido da fórmula seja encontrado. Por exemplo, para o fatorial 3! = 3 · 2! = 3 · 2 · 1! = 3 · 2 · 1 · 0! = 3 · 2 · 1 · 1
e para a sequência de Fibonacci fib3 = fib2 + fib1 = fib2 + 1 = fib1 + fib0 + 1 = 1 + 0 + 1 = 2 1
Sobre as diferenças entre recursão e recorrência: A raiz latina da palavra recursão é recursio, o que signica o ato ou processo de retornar para trás ou de voltar para trás. Recorrência vem de recurrere (Recurro, Recurrere, Recurri, Recursus) (de re- (”para trás, novamente”) + curro (”correr”). recursio é um substantivo e recorrer é um verbo (que pode ser substantivado por recorrência). Muitos dizem que recorrência, recursividade e recursão são coisas diferentes, computacionalmente falando, enquanto signicam exatamente o mesmo. Recurso é o substantivo para recorrer, em português. Através do Google (dei uma checada certa vez, phaz tempo) “recurrence trees” fornece cerca de 1k resultados (inclusive vindo de sites de Cornell e de outras universidades americanas) e “recursion trees” fornece cerca de 7k resultados, mostrando uma preferência pela palavra recursão no caso da representação de relações (recorrentes) por árvores. “recursion relations” fornece cerca de 73k resultados e “recurrence relations” cerca de 250k resultados, mostrando agora a preferência pela palavra recorrência. Enm, não há diferença. 2 Também conhecidas por “fórmulas“. A palavra ”função“ é incorreta mas culturalmente utilizada. Funções, no sentido estrito, são conjuntos de uplas ordenadas que podem ser estabelecidas através de uma fórmula.
4
3 Implementação de métodos recursivos simples
SUMÁRIO
e assim por diante. Em todas as denições anteriores, o ”objeto fórmula” dene seu próprio cálculo pela através da redução do argumento em 1 (ou em 1 e 2, como o caso da sequência de Fibonacci). Ou seja, o problema de tamanho n é reduzido para subproblemas de tamanho n-1 (ou etcs. Fibonacci). Colocado de forma simplista, um método é recursivo quando no corpo de sua denição existe uma ou mais chamadas a ele mesmo. Todo algoritmo recursivo (ou uma fórmula recursiva) tem duas partes: • Soluções Triviais (ou os casos base, ou as condições de parada da recursão): são dadas por denição, isto é, não necessitam de recursão para serem obtidas. São os critérios de parada da recursão. Quando não denidos de forma adequada, acarretam em estouros de pilha numa implementação ou mesmo deixam o programa em loop eterno. • Soluções Recursivas (ou passos de redução ou partes recursivas do algoritmo): são as partes do problema que recorrem a sua própria denição. Em geral: é desejável que o problema original seja reduzido a problemas menores e em número pequeno. Os argumentos usados como controle tem que convergir para as soluções triviais. Para os exemplos anteriores, o argumento n é decrementado a cada chamada. Em geral, a recorrência pode comparecer num método R também de forma mútua ou indireta. Na forma indireta, R chama métodos Ci , onde pelo menos 1 deles volta a chamar R.
3 Implementação de métodos recursivos simples As implementações mais simples de métodos recorrentes são aquelas obtidas pela simples transcrição da formulação matemática. Observe também que geralmente os casos base sempre comparecem no início do método para esses casos mais simples. De modo geral, os casos base podem estar distribuídos no corpo do método sendo que cada um deles marca o início de uma restrição incrementalmente maior do domínio dos argumentos. E. 3.1 Potência xn : Dada a relação de recorrência 2.3, considerando-se x e n inteiros positivos ou nulos 3 , o método praticamente escreve-se sozinho, bastando apenas transcrever a relação de recorrência in t pow( in t x, in t n) { if ( n = = 0 ) return 1; return x * p o w ( x , n - 1 ) ; }
Nesse exemplo, o caso base é n = 0 e a convergência para ele é assegurada já que a chamada recorrente incumbe-se de decrementar o argumento de controle n. Claro que se n < 0 quando da primeira chamada, o método entra em loop que encerra com um estouro de pilha de argumentos através da exceção StackOverflowError. Nos códigos apresentados, geralmente evita-se a escrita de elses após os returns dos casos base4 de modo a que o código não que poluído. « 3
Mais precisamente, 0 não é 1 e sim uma indeterminação. Para o exercício, a denição fornecida ca simples o bastante, embora incorreta. 4 O que deve ser considerada uma má prática de acordo com o manual dos gurus das boas práticas. 0
5
3.1 Recursão mútua ou indireta
SUMÁRIO
E. 3.2 Algoritmo de Euclides: Dada a relação de recorrência, considerando-se m e n inteiros positivos ou nulos,
mdc(m, n) =
�
m
mdc(n, m mod
, n = 0 n) , caso contrário
novamente basta transcrever a relação de recorrência in t mdc( in t m, in t n) { if ( n = = 0 ) return m; return m dc (n , m % n ); }
O caso base é n = 0. Quanto à convergência, ela é obtida pela análise de m % n. m % n converge para 0, que é o novo argumento n passado. Por maiores que sejam dois números m e n, por exemplo 1047 e 38, o resto da divisão é sempre menor que o menor deles. No exemplo, mdc(1047, 38)= mdc(38, 1047%38)= .... Etcs., pode ser provado que o algoritmo é O(log d), onde d é o número de dígitos do menor número (coloco a prova na próxima versão da lista). «
3.1 Recursão mútua ou indireta Recursão mútua ou indireta é uma forma de recorrência onde dois objetos denem suas características em termos um do outro. E. 3.3 O exemplo clássico de recursão mútua é a denição mútua de número par e ímpar, utilizando-se apenas subtrações. Def. 3.4 Número par: Se n vale 0 então é par. Caso não, n é par se n - 1 for ímpar.
«
Def. 3.5 Número ímpar: Se n vale 1 então é ímpar. Caso não, n é ímpar se n - 1 for par.
«
Estabelecemos as relações de recorrência, tomando o cuidado de nos certicarmos que convergem para o caso base. par(n) =
� verdadeiro
, n = 0 ímpar(n − 1) , caso contrário
ímpar(n) =
A codicação é imediata
6
� verdadeiro par(n − 1)
, n = 1 , caso contrário
3.2 Problemas com a recorrência
SUMÁRIO
public class MutualRecursionDemo01 { private MutualRecursionDemo01() { } public static boolean isEven( in t n) { return n = = 0 ? true : i sO dd ( n - 1 ); } public static boolean isOdd( in t n) { return n = = 1 ? true : i sE ve n (n – 1 ); } public static void main(String[] args) { System.out.println(isEven(3)); }
}
Em certos casos é possível que recorrências mútuas possam ser eliminadas fazendo-se a substituição direta de uma relação recorrente na outra. Ou simplesmente repensar-se os casos base. No último caso, por exemplo, public class MutualRecursionRemovedDemo01 { public class MutualRecursionRemovedDemo01 { private MutualRecursionRemovedDemo01() { } private MutualRecursionRemovedDemo01() { } public static boolean isEven( in t n) { if ( n = = 0 ) return true; if ( n = = 1 ) return false ; return isEven(n - 2); }
public static boolean isEven( in t n) { if ( n = = 0 ) return true; return !isEven(n - 1); }
ou
public static boolean isOdd( in t n) { if ( n = = 1 ) return true; if ( n = = 0 ) return false ; return isOdd(n - 2); }
public static boolean isOdd( in t n) { if ( n = = 1 ) return true; return !isOdd(n - 1); } public static void main(String[] args) { System.out.println(isEven(3)); }
public static void main(String[] args) { System.out.println(isEven(3)); }
}
}
no caso de 2 casos base serem utilizados ou apenas 1. Enm.
3.2 Problemas com a recorrência Mini-FAQ: Podemos pensar nas seguintes questões pertinentes: • Q: A recursão deixará meu código mais rápido? A: Não.
7
3.2 Problemas com a recorrência
SUMÁRIO
• Q: Então por que utilizar recursão? A: Algumas vezes os códigos cam bem mais simples! Em outras vezes, é meio impensável à partida resolver um problema sem usar recursão. • Q: Essa impensabilidade é vista neste texto? A: Não. Todos os códigos deste texto são inúteis. São apenas um meio para aprender-se os detalhes básicos (eventualmente alguns mais nos) sobre recursão. Em geral, métodos recursivos pecam em termos de eciência mas conseguem deixar o código compacto, claro, limpo e com as intenções de codicação explícitas. Embora tenham todas essas virtudes, diversos problemas em sua codicação são frequentes: o tempo de execução, tamanho necessário de área de pilha e possíveis recálculos desnecessários. Em relação aos recálculos, sendo o tempo de execução função do número de chamadas no código, no caso da sequência de Fibonacci é necessário um tempo de cálculo ligeiramente menor que, e da ordem de, O(2n ), ou seja, exponencial. Vejamos algumas considerações. 3.2.1 A pilha da máquina
Ao nível da máquina, existem apenas processos iterativosparaacargaeexecuçãodeinstruçõesdemáHeap quina. Todas as chamadas de métodos são imple(Objetos) mentadas utilizando-se uma pilha, também denominada de pilha de ativação. A pilha mantém o registro do endereço de retorno, argumentos, possiMemória Não velmente das ags e registradores da CPU e das vavariáveis locais utilizada riáveis locais do ponto de onde foi chamado o méargumentos Registro todo. A toda essa estrutura complexa da chamada método n estado da CPU BP (flags, ... de um método denomina-se de registro de ativação registradores) Pilha endereço de ou, simplesmente, de ativação. De modo bem transde ativação Registro retorno (PC) método 2 parente, toda a chamada de um método resulta na topo anterior PC (apontador base, Programa Registro carga de um registro de ativação na pilha; e todo BP) método 1 valor de o retorno de um método resulta na remoção do reretorno OS/VM gistro do topo da pilha, o qual possui o endereço no qual o código deve resumir a sua execução. Em baixo nível (assembly), CALLsfazemcomqueaCPU Figura 1: Esquema da pilha de ativação utilizada pelos empilhe o endereço de retorno (PC); e RETs com métodos que haja o desempilhamento e a execução do código resume-se no PC desempilhado. Quanto de memória ocupa um registro de ativação? Depende obviamente da quantidade de variáveis locais ao método que efetua a chamada, por exemplo. Para todos os ns práticos, diremos apenas que uma chamada requer espaço O(1) de pilha. A estrutura de um registro de ativação é também dependente da máquina e do compilador. Uma estrutura típica de registros de ativação é mostrada na Fig. 1 Segundo essa vizualização, a ideia de recursividade é útil apenas para o programador. Um método 8
3.2 Problemas com a recorrência
SUMÁRIO
recursivo, como qualquer outro, utiliza essa abordagem baseada em pilhas e em ativações, sendo que, em particular, uma ativação para o mesmo método é colocada na pilha; para a máquina, entretanto, isso não faz nenhuma diferença. A única diferença notória entre iteração e recursão ocorre quando os casos base de um método recursivo não são atingidos e haverá a gravação de uma dada quantidade de registros de ativação até que a área de pilha estoure; em contrapartida, para um método iterativo a situação análoga leva a um looping innito que é apenas interrompido com a intervenção do usuário. Em suma, um método recorrente tem que ser escrito de forma a convirja para os casos base (e que isso ocorra bem antes de um estouro); e métodos iterativos devem ter os critérios de nalização dos loops bem denidos. 3.2.2 Caso base omitido
No exemplo abaixo, o caso base não foi adicionado. Em princípio, o método deveria computar números de uma série harmônica public class HarmonicDemo01 { private HarmonicDemo01() { } public static double harmonic( in t n) { return h a rm on ic ( n – 1 ) + 1 .0 / n ; } public static void main(String[] args) { System.out.println(harmonic(3)); }
}
A partir da chamada em main(), o método encarrega-se de decrementar n para a próxima chamada a harmonic() mas nunca um caso base é atingido (porque não foi obviamente incluído no código). As diversas chamadas recorrentes são anotadas na pilha do Java e, em determinada altura, uma exceção de estouro de pilha, StackOverflowError, é emitida. 3.2.3 O(s) caso(s) base existe(m) mas a sequência de chamadas recursivas não converge para ele(s). Ou, o(s) caso(s) base existe(m) mas a sequência pode não convergir para ele(s).
Esse dois problemas tem como sintoma um estouro de pilha, embora a causa possa ser um problema com a denição deciente dos casos base ou com as reduções. Por exemplo,
9
3.2 Problemas com a recorrência
SUMÁRIO
public class HarmonicDemo02 { private HarmonicDemo02() { } public static double harmonic( in t n) { if ( n = = 1 ) return 1.0; return h a rm on ic ( n) + 1 .0 / n ; } public static void main(String[] args) { System.out.println(harmonic(3)); }
}
harmonic(3) é chamada, harmonic() tem o caso base mas nunca n é reduzido na chamada recorrente. Nesse caso, harmonic() é chamada recorrentemente com n = 3 até que um estouro de pilha aconteça.
Mesmo um caso base bem formado não evita que haja estouro de pilha. Por exemplo, o código abaixo public class FactorialDemo01 { private FactorialDemo01() { } public static double fact( in t n) { if ( n = = 1 ) return 1.0; return n * f ac t( n - 1 ); } public static void main(String[] args) { System.out.println(fact(0)); }
}
está bem escrito e determina o fatorial para qualquer número acima de n = 1. Entretanto, a sequência de chamadas recursivas para 0! nunca atinge o caso base. Mesmo sintoma (estouro de pilha), causa diferente: a falta de vericação do domínio dos argumentos, ou seja, a falta do précondicionamento. Para sanar-se isso, cria-se um segundo método (denominado de helper ou auxiliar) e coloca-se esse método à disposição do usuário: o método checa a précondição e o método recorrente é chamado apenas se os argumentos estiverem dentro do domínio. Ficaria public class FactorialDemo01 { private FactorialDemo01() { } public static double fact( in t n) { if ( n < 1 ) throw new IllegalArgumentException( "Fatoriais apenas para n >= 1.0" ); return _fact(n); } private static double _fact( in t n) { if ( n = = 1 ) return 1.0; return n * f ac t( n - 1 ); }
}
10
3.2 Problemas com a recorrência
SUMÁRIO
Obviamente private só faz sentido se os métodos _fact() e fact() estiverem escritos em uma classe separada. Sendo assim, o usuário caria restrito a utilizar fact(). 3.2.4 Muito espaço na área de pilha requerido
Tudo OK agora, casos base e passos de redução. Agora o usuário requer que seja calculado harmonic(5000) . Quando um método é chamado é carregado na área de pilha um registro: a informação necessária para o próximo método, outras informações e para onde o método chamado deve retornar. A cada chamada um registro é adicionado na pilha, a cada retorno um registro é removido da pilha. No caso de um método recorrente, se a profundidade da recorrência é grande (depende da área inicial de pilha do Java. No caso, algo aproximadamente 5000 ou maior), para o código abaixo ... public static double harmonic( in t n) { if ( n = = 1 ) return 1.0; return h a rm on ic (n - 1 ) + 1 .0 / n ; }
...
um estouro de pilha ocorrerá também! Note que harmonic(5000) chama harmonic(4999) que chama … que chama harmonic(2) que chama harmonic(1). São 4999 chamadas recorrentes antes do primeiro retorno. na área de pilha haverá cerca de 5000 registros (das chamadas recorrentes mais o da chamada harmonic(5000). Mesma causa, sintoma diferente e novamente um estouro de pilha: desta vez poor espaço insuciente (o mesmo StackOverflowError). Como orientação prática, um método que resolve um problema de tamanho n não pode usar O(n) de espaço de pilha. 3.2.5
Número de recálculos excessivos
A utilização de um método recorrente torna-se proibitiva quando existem recálculos ou redundâncias de operações envolvidas. No caso mais simples, recálculos são efetuados. Um dos métodos campeões de recálculos, tipicamente ilustrado como um uso de recorrência (e às vezes não é dito que é um mau uso) é o cálculo dos valores da sequência de Fibonacci. A implementação dessa ideia recursiva, um primor da ineciência, pode ser realizada pelo código abaixo A sequência de chamadas à função fib() foi esquematizada pela árvore à esquerda do código. Interprete-se como ” fib(6) chama fib(5) e fib(4); fib(5) chama fib(4) e fib(3)”, e assim por diante. Note que o cálculo de fib(6) envolve fib(5) e fib(4). A árvore de fib(4) é repetida 2 vezes. Por outro lado, a árvore de fib(2) está repetida 5 vezes! Esses são os recálculos mencionados. Vale salientar que o esforço computacional para o cálculo de fib(n) é o mesmo que o esforço de fib(n -1) somado àquele de b(n-2). Note também que 2 · fib(n − 1) > fib(n) = fib(n − 1) + fib(n − 2) > 2 · fib(n − 2)
11
3.2 Problemas com a recorrência
SUMÁRIO
public class FibonacciDemo01 {
fib (6) fib (5) fib (4) fib (3) fib (2) fib (1)
fib (3) fib (2)
fib (1)
fib (1)
fib (0)
private FibonacciDemo01() { }
fib (4)
fib (2) fib (1)
fib (0)
fib (3) fib (1)
fib (2) fib (1)
public static long fib( in t n) { if ( n = = 0 | | n = = 1 ) return 1; return f ib (n - 1 ) + f ib (n - 2 ); }
fib (2) fib (1)
fib (1)
fib (0)
fib (0)
public static void main(String[] args) { System.out.println(fib(6)); }
fib (0)
}
Figura 2: Representação gráca das chamadas recursivas feitas por fib(6) ou seja, fib(n) cresce exponencialmente com razão 5 menor que 2 (o esforço para o cálculo da sequência também!). Na prática, o número de chamadas recorrentes pode ser contado através do número de fib()s na árvore (na árvore, leia-se fib() ao invés de fib(), é responsável por 24 chamadas recorrentes. O número de vezes que fib(1)e fib(0) são chamados é 13, exatamente o número fib(6); ou seja, fib(n) requer fib(n) somas de 1. Ou seja, o número de vezes que 1 é somado cresce exponencialmente com a própria razão da sequência de Fibonacci! O tempo necessário para o cálculo recorrente da sequência de Fibonacci, em relação a uma versão iterativa, pode ser estimado ao executar-se o seguinte código. Deve ser suciente para demonstrar o efeito do recálculo excessivo. √ 5 Cresce com φ , sendo φ a famosa razão áurea φ = (1 + 5)/2 ≈ 1.62. Detalhes em mathworld.wolfram.com ou no wiki n
12
3.2 Problemas com a recorrência
SUMÁRIO
public class FibonacciDemo02 { public static long iterativeFib( in t n) { long r = 1 , r e s = 1 ; fo r ( in t i = 1 ; i < = n ; i + + ) { long q = r; r = res ; res = r + q; } return res; } public static long recursiveFib( in t n) { return n = = 0 | | n = = 1 ? 1 : r ec ur si ve Fi b (n - 1) + r ec ur si ve Fi b (n - 2 ); } public static void main(String[] args) { // Versão iterativa fo r ( in t i = 0; i <= 60; i ++) System.out.println(i + " " + iterativeFib(i)); // Versão recursiva fo r ( in t i = 0; i <= 60; i ++) System.out.println(i + " " + recursiveFib(i)); }
}
Os recálculos podem obviamente ser evitados memorizando-se os valores anteriores já calculados. Eventualemnte é uma melhoria inútil, já que o método iterativo continuaria sendo mais rápido.
13
4 Exercises diversos (não é a lista) com arrays e strings
SUMÁRIO
4 Exercises diversos (não é a lista) com arrays e strings Uma das partes mais difíceis da escrita de um método recorrente que envolva arrays é a escrita da frase que descreve o método. Para ilustrar passemos à escrita do selectionSort() recorrente.
4.1 Com arrays E. 4.1 [*] selectionSort() recursivo: Não é um bom exemplo de uso de recorrência: é um exercício apenas. A formulação do problema de ordenar um array recorrentemente tendo como base o Selection Sort foi já “passada” em aula. Como seria feita a formulação do problema? Qual é a frase inicial difícil de ser escrita?
”Considerando uma lista (de floats, por exemplo) list[] de tamanho n e desordenada, busca-se o menor elemento do array entre start e end (que delimitam a parte desordenada) e o trocamos com o elemento em start. Procede-se então à ordenação do array entre start + 1 e end.“ Ou seja, a maioria dos problemas que envolvem ordenação de arrays ou operações em arrays contém o índice inicial onde a operação deve ser realizada e o índice nal. Apesar de não serem sempre recorrentes, exceto talvez o quickSort(), diversos métodos utilitários de java.lang.String e de java.util.Arrays usam dois indices para delimitar a faixa de operação do método. No caso particular, o método teria como cabeçalho void selectionSort( float [] list, in t start, in t end)
e start e end serão os argumentos de controle da recorrência. Note que de pouco ou nada serve list.length. Teríamos portanto, sem o caso base void selectionSort( float [] list, in t start, in t e nd ) { / / F al ta o c as o b as e ... / / B u s c a p e l o m e no r e n tr e s t ar t e e nd in t imin = start; fo r ( in t i = s ta rt ; i <= e nd ; ++ i) if (list[i] < list[imin]) i mi n = i ; / / T r oc a d e l i st [ i mi n ] p or l is t [ s ta r t ] float t mp = li st [ st ar t] ; list[start] = list[imin]; l is t [i mi n ] = t mp ; / / P ro ce de a o s el ec ti on s or t e nt re s ta rt + 1 e e nd selectionSort(list, start + 1, end);
}
ou utilizam-se os métodos já ”passados“ em sala de aula
14
4.1 Com arrays
SUMÁRIO
void selectionSort( float [] list, in t start, in t e nd ) { / / F al ta o c as o b as e ... / / B u s c a p e l o m e no r e n tr e s t ar t e e nd in t imin = findMin(list, start, end); / / T r oc a d e l i st [ i mi n ] p or l is t [ s ta r t ] swap(list, start, imin); / / P ro ce de a o s el ec ti on s or t e nt re s ta rt + 1 e e nd selectionSort(list, start + 1, end); } void swap( float [] list, in t i, in t j) { float tmp = list[i]; list[i] = list[j]; list[j] = tmp; } in t findMin( float [] list, in t start, in t e nd ) { in t imin = start; fo r ( in t i = s ta rt ; i <= e nd ; ++ i) if (list[i] < list[imin]) i mi n = i ; return imin; }
Quem é o caso base? Note que é somado 1 a start e que aparentemente start convergirá até end. ”Na prática“, não é necessário ordenar um elemento (ou seja, quando start == end) e daí forma-se o caso base void selectionSort( float [] list, in t start, in t e nd ) { / / C a so b a se if (start == end) return ; / / B u s c a p e l o m e no r e n tr e s t ar t e e nd ... }
Para a resolução dos exercícios estaria bom até aqui. Note que entretanto o método recorrente não trata dos casos de list[] ser um null passado ou mesmo que start > end e outras situações para o tratamento de usuários distraídos e chamadas de outros métodos. Um tapa adicional pode ser efetuado e o código pode car mais prossional como public void selectionSort( float [] list, in t start, in t e nd ) { // Précondições if (list == null | | s t ar t < 0 | | e n d < 0 | | s t ar t > = l i st . le ng th || end >= list.length) return ; _selectionSort(list, start, end); } private void _selectionSort( float [] list, in t start, in t e nd ) { / / C as o b a se if (start >= end) return ; / / B u sc a p e lo m e no r e n tr e s t ar t e e nd ... }
15
4.2 Com strings
SUMÁRIO
onde o método selectionSort() é o método disponibilizado para o usuário e faz a vericação da précondição. Escolheu-se nada fazer no caso de list == null | | s ta rt < 0 | | e n d < 0 | | s ta rt > = l i st . le ng th | | e n d > = l i st . le ng th
mas poderia ser outra coisa. Deve ser especicada essa escolha no ”manual“ ou feita de acordo com o especicado). No caso de estar tudo OK, _selectionSort() é chamado. Note que o caso base foi mudado para start >= end para nada fazer se start > end. «
4.2 Com strings Tabelinha de métodos de Strings. Cortezia da Oracle. char
Retorna o caracter da posição index da String. IndexOutOfBoundsException se o índice é inválido (negativo ou ≥ length() )
charAt( int index)
int String
length() substring( int beginIndex)
String
substring( int beginIndex, int endIndex)
Retorna o comprimento da String. Retorna uma string entre a posição beginIndex (inclusive) até o último caracter. Se beginIndex for igual a length() , retorna uma string vazia. IndexOutOfBoundsException se beginIndex é inválido (negativo ou > length ()) Exemplos: "unhappy".substring(2) retorna "happy" "Harbison".substring(3) retorna "bison" "emptiness".substring(9) retorna "" Retorna uma substring da posição beginIndex (inclusive) até endIndex (exclusive) IndexOutOfBoundsException se beginIndex < 0 , ou endIndex > length() , ou beginIndex > endIndex Exemplos: "hamburger".substring(4, 8) retorna "urge" "smiles".substring(1, 5) retorna "mile"
E. 4.1 [null] Escreva um método recursivo int countChar(String s, char ch)
que conta quantos caracteres ch existem na String s. Retorna 0 caso s = null ou s é a String vazia. R:Se s tem tamanho 0 ou s = null, retorna 0. Caso contrário, retorna 1 se na posição 0 da String tem o ch + a contagem de caracteres a partir da posição 1 ( countChar(s.substring(1), ch)) . Ou, retorna 0 se na posição 0 da string não tem o ch + a contagem de caracteres a partir da posição 1 public static int countChar(String s, char c h) { if ( s = = null | | s . le ng th ( ) = = 0 ) return 0; if (s.charAt(0) == ch) return 1 + countChar(s.substring(1), ch); return countChar(s.substring(1), ch); }
Como o argumento s não converge para null, pode-se removê-lo do processo recursivo. Fica, dando aquele capricho, 16
4.2 Com strings
SUMÁRIO
public static int countChar(String s, char c h) { if ( s = = null ) return 0; return _countChar(s, ch); } private static int countChar(String s, char c h) { if (s.length() == 0 ) return 0; if (s.charAt(0) == ch) return 1 + countChar(s.substring(1), ch); return countChar(s.substring(1), ch); }
E. 4.2 [*] Um palíndromo6 é uma frase ou palavra que é igual se lida da esquerda para a esquerda ou da esquerda para a direita. Exemplos são "radar", "able was I ere I saw elba" e a própria string vazia "". Escreva um método recursivo que determine se uma String é um palíndromo ou não. R:Mais um makin’ o de exercise. Nos exemplos fornecidos existem ”não-letras”. Inicialmente escrevamos um método que decide se um caracter é uma letra e, por simplicidade, apenas minúsculas não acentuadas private static boolean isLetter( char c h ) { return 'a ' <= c h & & c h <= 'z '; }
Tudo feito na raça. Existe o método isLetter() em java.lang.Character, caso lhe interesse. Para escrever o método recursivo existem diversas formas: pode-se usar substring()s ou não. Façamos a versão “não”: o método irá requerer como argumentos de controle um índice para o nal da String eoutro para o começo; e mais um helper adicional. Despejo a ideia inicial no código boolean isPalindrome(String s) { if ( s = = null ) return true; return isPalindrome(s, 0, s.length() - 1); } boolean isPalindrome(String s, in t f, in t t) { ... / / C a so b a se if (!isLetter(s.charAt(f))) return isPalindrome(s, f+1, t ); if (!isLetter(s.charAt(t))) return i sP al in dr om e (s , f , t - 1 ); / / T e m os e m f e e m t d oi s c ar ac te re s ... } 6
Tem um montão no wiki: ”O Gal. Leno Roca, à porta da cidade, a portador relata fatal erro da tropa e dá dica da tropa a Coronel Lago”, ”Reverta é verbo, O vivo breve é, Sabe bem amá-lo o Lama, Me beba se é verbo vivo, O breve atrever.”, ”O saco no nosso nono caso”, ”Leben Sie mit Siegreits Rune. Deine Zier sei dies. Reize nie den Urstiergeist im Eisnebel”, ”Sumitis a vetitis; sitit is, sitit Eva, sitimus”, rsrsr.
17
4.2 Com strings
SUMÁRIO
onde foi assumido que null é um palíndromo (o que poderia ser false caso o enunciado pedisse. Ou não.). Caso os caracteres s.charAt(t) e s.charAt(f) sejam iguais, verica-se a String que vai de f+1 até t-1. Caso não, não é um palíndromo. Assim boolean isPalindrome(String s) { if ( s = = null ) return true; return isPalindrome(s, 0, s.length() - 1); } boolean isPalindrome(String s, in t f, in t t) { ... / / C a so b a se if (!isLetter(s.charAt(f))) return isPalindrome(s, f+1, t ); if (!isLetter(s.charAt(t))) return i sP al in dr om e (s , f , t - 1 ); / / T e mo s e m f e e m t d ua s l e tr as if (s.charAt(t) == s.charAt(f)) return isPalindrome(s, f+1, t -1); return false ; }
Para chegar-se nesse último teste é necessário que f >= t. E sendo assim a chamada fará com que f < t no caso de f == t. É o caso base! Note que naturalmente a String vazia é contemplada como palíndromo, sem nunca essa condição ter sido forçada/considerada! Finalmente public static boolean isPalindrome(String s) { if ( s = = null ) return true; return isPalindrome(s, 0, s.length() - 1); } private static boolean isPalindrome(String s, in t f, in t t) { / / C a so b a se if ( f > t ) return true; / / T r at a me n to d os c a so s d e " n ã o l e tr a " if (!isLetter(s.charAt(f))) return isPalindrome(s, f+1, t ); if (!isLetter(s.charAt(t))) return i sP al in dr om e (s , f , t - 1 ); / / T e mo s e m f e e m t d ua s l e tr as if (s.charAt(t) == s.charAt(f)) return isPalindrome(s, f+1, t -1); / / C as o b a se : s . c ha r At ( t ) ! = s . c ha r At ( f ) return false ; }
** FIM DO EXERCISE ** mas como a gente gosta de programar (sic), vamos ilustrar algumas diculdades e sutilezas da implementação da coisa iterativa. Praticamente transcrevendo-se da recursiva
18
4.2 Com strings
SUMÁRIO
public static boolean isPalindrome(String s) { if ( s = = null ) return true; return isPalindrome(s, 0, s.length() - 1); } private static boolean isPalindrome(String s, in t f, in t t) { while ( f < = t ) { / / T r at a me n to d o s c a s o s d e " n ão l e tr a " if (!isLetter(s.charAt(f))) { f++; continue ; } if (!isLetter(s.charAt(t))) { t--; continue ; } / / T e mo s e m f e e m t d ua s l e tr as if (s.charAt(t) != s.charAt(f)) return false ; / / C he ca m - s e a s p r óx i ma s d u as l e tr a s f++; t--; } return true; }
a qual também é um bom exemplo do porquê da existência de continue s7. O algoritmo iterativo anterior peca um pouco pela eciência: p.e. para s=",.;mariaairam. . .", ! isLetter(s.charAt(f)) é inicialmente avaliada 3 vezes; em seguida, a condição similar sobre t é vericada 4 vezes e também sobre f! Um polimento posterior no código trata esse problema boolean isPalindrome(String s, in t f, in t t) { while ( f < = t ) { / / T r at a me n to d os c a so s d e " n ã o l e tr a " while (f <= t && !isLetter(s.charAt(f))) f++; if ( f > t ) break ; while (f <= t && !isLetter(s.charAt(t))) t--; if ( f > t ) break ; / / T e mo s e m f e e m t d ua s l e t ra s if (s.charAt(t) != s.charAt(f)) return false ; / / C he ca m - s e a s p r óx i ma s d u as l e tr a s f++; t--;
} return true ; }
e agora a condição no while externo é desnecessária pois repete-se no primeiro while mais interno. Mais um passe, também substituindo-se o while externo por for e f++; t-- no corpo do for 7
Novamente, tal uso deve estar condenado no manual de boas práticas.
19
4.2 Com strings
SUMÁRIO
boolean isPalindrome(String s, in t f, in t t) { fo r ( ; ; f + + , t - - ) { / / V a rr e du r a e m f a té o f in a l d a S tr i ng , p u la n do n ão l e tr a s while (f <= t && !isLetter(s.charAt(f))) f++; if ( f > t ) break ; / / V a rr e du r a e m t a té o i n íc i o d a S t ri ng , p u la n do n ão l e tr a s while (f <= t && !isLetter(s.charAt(t))) t--; if ( f > t ) break ; / / T e mo s e m f e e m t d ua s l et ra s if (s.charAt(t) != s.charAt(f)) return false ;
} return true; }
Mais um tapa: remove-se o primeiro if, delegando-se essa vericação para o segundo (obviamente se f após o primeiro while, f <= t é falso para o segundo while e o código é interrompido em seguida)
> t
private static boolean isPalindrome(String s, in t f, in t t) { fo r ( ; ; f + + , t - - ) { / / V a rr e du r a e m f a té o f in a l d a S tr i ng , p u la n do n ão l e tr a s while (f <= t && !isLetter(s.charAt(f))) f++; / / V a rr e du r a e m t a té o i n íc i o d a S t ri ng , p u la n do n ão l e tr a s while (f <= t && !isLetter(s.charAt(t))) t--; if ( f > t ) break ; / / T e mo s e m f e e m t d ua s l et ra s if (s.charAt(t) != s.charAt(f)) return false ;
} return true; }
e paramos por aqui antes que sobre apenas uma linha de código. Eventualmente o break pode ser substituído por return true e o return true; do nal pode ser removido, caso o compilador não reclame.
20
5 Exercises (a lista)
SUMÁRIO
5 Exercises (a lista)
5.2 Recursão em arrays
5.1 Recursão simples
Para os exercises abaixo, com a exceção do primeiro, não usar loops.
Para os exercises abaixo, não usar loops. E. 5.1 (CCPS 109, Recursion Exercises)[null] creva o método recorrente
E. 5.5 Escreva um método que ordene o array de Es- modo recorrente com base no algoritmo Insertion Sort. «
void listNumbers(int from, int to)
E. 5.6 (CCPS 109, Recursion Exercises)[null] Esque mostra na consola os números de from a to. Es- creva o método recorrente creva uma versão que escreve em ordem crescente e int min(int[] a, int from, int to) outra em ordem decrescente. « que retorna o menor valor do array a entre os índices E. 5.2 (CCPS 109, Recursion Exercises)[null] Es- from e end. « creva o método recorrente E. 5.7 [null] Escreva o método recorrente int mul(int a, int b)
que calcula a multiplicação de dois inteiros positivos boolean linearSearch(int[] a, int from, int to, int x) a e b. As únicas operações aritméticas que podem ser utilizadas são a soma e a subtração. « que faz a busca linear entre as posições from e to do « E. 5.3 (CCPS 109, Recursion Exercises)[null] Es- array a pelo valor x. creva o método recorrente
5.3 Recursão em Strings
int sumOfDigits(int n)
que calcula a soma dos dígitos do inteiro positivo n. Por exemplo, para o argumento n = 12345, o método retorna 15. « E. 5.4 (UFOP) Considere um sistema numérico que não tenha a operação de adição implementada e que você disponha somente dois operadores (métodos): sucessor e predecessor. É meio óbvio dizer isso mas o sucessor e o predecessor seriam operações que somam 1 e subtraem 1, respectivamente. Então, pede-se para escrever uma método recursivoquecalculeasomadedoisnúmerosxeyatravés desses dois operadores: sucessor e predecessor. Sugestão meio óbvia: Escreva int succ(int n) e int pred(int n) e em seguida o método pedido que contém apenas chamadas a esses dois (além das óbvias a ele mesmo). «
Para os exercises de String abaixo pode-se usar apenas os métodos charAt(), length() e substring(), além da concatenação. Sem loops. Sugestãogenérica: Pense sempre que a string vazia pode ser um caso base. Obviamente se a própria String for um argumento de controle. E. 5.8 [null] Escreva um método recursivo void printString(s)
que imprime uma String recursivamente, caracter a caracter. « E. 5.9 (CCPS 109, Recursion Exercises)[*] Escreva o método recorrente
21
String mySubstring(String s, int from, int to)
5.3 Recursão em String s
SUMÁRIO
que funciona de modo similar ao método substring da classe String. Apenas usar os métodos charAt(), a concatenação + e length(). « E. 5.10 [null] Escreva um método recorrente void printStringReverse(String s)
que imprime uma String recursivamente e ao contrário, caracter a caracter. Apenas usar os métodos charAt(), a concatenação + e length(). « E. 5.11 (CCPS 109, Recursion Exercises)[null] creva o método
Es-
String repeat(String s, int n)
que retorna uma string montada a partir de n cópias da String s passada pelo argumento. Por exemplo, repeat("Hello", 3) retorna "HelloHelloHello". Caso n ≤ 0, o método retorna a string vazia "". « E. 5.12 (CCPS 109, Recursion Exercises)[null] creva o método recorrente
Es-
String disemvowel(String s)
que retorna uma string formada pela String s passada com as vogais removidas. Escreva, para auxiliar o método, boolean isVowel(char ch)
que indica se ch é uma vogal. Faça os algoritmos assumindo apenas vogais não acentuadas. « E. 5.13 [null] Escreva um método recursivo String rotateRight(String s, int n)
que roda a String s à direita n vezes. Por exemplo, caso rotateRight("Maria", 3) retorna "riaMa". « E. 5.14 (CCPS 109, Recursion Exercises)[null] creva o método recursivo
Es-
int binToDec(s)
que retorna a representação na base 10 de uma String que contém com um número binário, caracter a caracter. Por exemplo, binToDec("101011") retorna 43. Retorna 0 no caso de Stringvazia ou nula. « 22