Capítulo 3.2 Algoritmos Iterativos vs. Recursivos
Algoritmos e Estruturas de Dados Dados
2007-2008 , Gladys Castillo
Algoritmos Iterativos Um algoritmo iterativo usa estruturas de repetição (ciclos) para resolver problemas Exemplo: A função factorial static fact fa factoria ctor oria orial ial (i nt static sta tic int intt factorial in fact fa facto fac ctor toria oria rial ialll (int (int (i nt n) n) {{ factorial fa ctt = int int fact fact fac = 1; 1; for (int for i=2 i+ int for fo r (int (int (i int nt i=2; i=2;;; i<=n, i=2 i<=n, i++) i++) i++) +) for fact fa ctt ** i;i; fact = = fact fac fact return return retu rn fact; fact; fac t;
Um ciclo ciclo é usado para para calcular o factorial de um número
}}
Algoritmos e Estruturas de Dados, Dados,
2007-2008 , Gladys Castillo
2
1
Algoritmos Recursivos Um algoritmo recursivo possui a característica de invocar-se a sim próprio repetidamente até que certa condição seja satisfeita Exemplo: A função factorial
static static int int factorial factorial (int (int n) n) {{ if (n if if == 0) 0) if (n == return return 1; 1; else else return factorial return nn * factorial(n-1); factorial(n-1); factorial }} Algoritmos e Estruturas de Dados,
Uma função recorrente inclui uma instrução de decisão que decide se parar o continuar a recorrência.
2007-2008 , Gladys Castillo
3
Árvore de Recorrência Cálculo de 4! 4! = 4 x 3! = 4 x 6 = 24
4!
4! = 4 x 3!
3!
3! = 3 x 2!
2!
2! = 2 x 1!
1!
1! = 1 x 0!
6 = ! 3
2 = ! 2
1 = ! 1
Desenhos extraídos de http://www.lcc.uma.es/~lopez/modular/recursion/ transp_recursion.pdf Autor: Pablo Lópes
1 = ! 0
Algoritmos e Estruturas de Dados,
0! 2007-2008 , Gladys Castillo
4
2
Caixa de Execução Cálculo de 4!
O valor de 4! é calculado no regresso das sucessivas invocações da função recursiva, pelo que 4! = 1 x 1 x 2 x 3 x 4 = 24
4! = 24
factorial(4) = 4 x factorial(3) =4x6 factorial(3) = 3 x factorial(2) =3x2 Cada caixa representa o espaço de execução de uma nova chamada da função. Em total são efectuadas 5 chamadas e 4 multiplicações
factorial(2) = 2 x factorial(1) =2x1 factorial(1) = 1 x factorial(0) =1x1
Memória utilizada: 4 bytes x 5 Cada chamada gera uma cópia do valor do parâmetro n pelo que não é eficiente em termos da utilização da memória Algoritmos e Estruturas de Dados,
factorial(0) = 1
2007-2008 , Gladys Castillo
5
Desenho de Algoritmos Recursivos Ideia básica: diminuir sucessivamente o problema em um problema menor, até que o tamanho do problema reduzido permita resolve-lo de forma directa, sem recorrer a si mesmo. Neste caso, diz-se que o algoritmo atingiu o critério de paragem. Devemos considerar as seguintes questões:
Definir o caso base:
Qual é o caso base do problema que pode ser resolvido sem quaisquer chamadas recursivas?
Definir formula recursiva:
Como definir o problema em termos do mesmo problema de menor dimensão? A diminuição da dimensão do problema em cada chamada recursiva garante que seja sempre atingido o caso base e por consequência o critério de paragem? Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
6
3
Sucessão de Fibonacci Esta sucessão foi estudada pelo matemático Leonardo Fibonacci (1170-1250), quando conjecturava acerca do crescimento de uma população de coelhos.
F(n) =
F(1)= F(2) =1 F(n-1) + F(n-2), se n> 2
Nº de pares 1, 1, 2, 3, 5, 8, 13, 21, 34, …
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
7
Sucessão de Fibonacci Algoritmo Iterativo vs. Recursivo versão recursiva
versão iterativa
static static int int fibonacci fibonacci (int (int n) n) {{ int int f, f, f1, f1, f2; f2; if (n<=2) if if return 1; if (n<=2) return return 1; f1 f1 = = 1; 1; f2 f2 = = 1; 1; for (int int for for int i=3; i=3; i<=n, i<=n, i++){ i++){ for (int ff = = f1 f1 + + f2; f2; f1 f1 = = f2; f2;
static static int int fibonacci fibonacci (int (int n) n) {{ if (n if if <= 2) 2) if (n <= return return 1; 1; else else return return fibonacci fibonacci (n-1) (n-1) + fibonacci fibonacci (n-2) (n-2) ;; }}
f2 f2 = = f; f;
O algoritmo recursivo é mais elegante e parecido com a definição da sucessão, mas muito menos eficiente do que a sua versão iterativa
}} return return f;f; }} Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
8
4
Árvore de Recorrência Cálculo do Fibonacci de 6
A solução recursiva é ineficiente pois para calcular o Fibonacci de 6, calcula repetidamente valores intermédios: 2 vezes F(4), 3 vezes F(3), 5 vezes F(2) e 3 vezes o F(1) F(6) = 8
hhhhh Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
9
Coeficiente Binomial e Triângulo de Pascal Triângulo de Pascal
O triângulo de Pascal é um triângulo numérico infinito formado pelos co eficientes dos binómios de Newton (a+b) n C (n, k )
=
n! (n
−
k )! k !
onde n representa o nº da linha e k o nº da coluna para obtermos os coeficientes do desenvolvimento do binómio (a+b)2, basta olharmos a 3ª linha do triângulo para escrevermos: (a+b)2 = 1 a2 + 2ab + 1b2
Cada elemento, com excepção dos elementos terminais da cada linha é calculado através da soma dos valores da linha anterior, ou seja: C(n,0) = C(n, n) = 1 C(n, k) = C(n-1, k-1) + C (n-1, k) , Algoritmos e Estruturas de Dados,
se 0
2007-2008 , Gladys Castillo
10
5
Cálculo do Coeficiente Binomial
Algoritmo Recursivo
Trabalho para casa:
static coefBinomial static int int coefBinomial coefBinomial (int n, n, int k) k) {{ coefBinomial (int if (k if if n) return return 0; 0; if (k >> n) if (k if if = 00 |||| kk == 1) 1) return return 1; 1; if (k =
1. Imprimir o triângulo de Pascal usando o algoritmo recursivo
return return coefBinomial coefBinomial (n-1, (n-1, k-1) k-1) + coefBinomial coefBinomial (n-1, (n-1, k) k) ;; }}
2. Implementar a versão iterativa para o cálculo do coeficiente binomial
Esta implementação é computacionalmente ineficiente para valores elevados de k e n. Devido à dupla invocação recursiva a árvore de recorrência cresce rapidamente, repetindo o cálculo de certos valores (o mesmo que acontecia com os números Fibonacci).
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
11
Invertir Número Algoritmo Iterativo vs. Recursivo versão iterativa
versão recursiva
static n){ static inverterNumero static void void inverterNumero(int inverterNumero(int inverterNumero n){ inverterNumero while while (n (n >0){ >0){ System.out.print(n%10); System.out.print(n%10); nn /=10; /=10;
static inverterNumero n){ static static void inverterNumero(int inverterNumero(int inverterNumero n){ static void System.out.print(n%10); System.out.print(n%10); if (n if if 10 >> 0) 0) if (n // 10 inverterNumero(n inverterNumero inverterNumero(n /10); inverterNumero /10);
}}
}} }}
}}
Trabalho para casa:
O algoritmo recursivo é mais elegante e mais fácil
Escrever a árvore de recorrência para a versão recursiva
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
12
6
Tipos de Recursividade Existem três tipos de algoritmos recursivos: 1. Recursividade directa:
a função tem uma chamada explicita a si própria 2. Recursividade indirecta:
a função F chama a função G que por sua vez chama a função F 3. Recursividade tail:
o valor de retorno é o valor de retorno da chamada recursiva (não existem operações pendentes)
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
13
Torres de Hanoi Edouard Lucas, 1883
Deslocar todos os discos da Torre A para a Torre C, um de cada vez Utilizar a Torre B como auxiliar Nunca colocar nenhum disco, sobre outro menor
Início Torre A
Torre B
Torre C
Torre A
Torre B
Torre C
Fim
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
14
7
Torres de Hanoi
Algoritmo Recursivo
Mover n discos de A para C usando B como auxiliar mover n-1 discos de A para B usando C como auxiliar mover 1 disco de A para C mover n-1 discos de B para C usando A como auxiliar
recursão mover n -1 discos de A para B usando C como auxiliar A
B
C
mover 1 disco de A para C
A
B
C
A
B
C
mover n discos de A para C usando B como auxililar
recursão mover n-1 discos de B para C usando A como auxililar A
Algoritmos e Estruturas de Dados,
B
C
2007-2008 , Gladys Castillo
15
Torres de Hanoi
Algoritmo Recursivo em Java static void torresHanoi (int n, int origem, int auxiliar, int destino) { if (n if == 1){ moverDisco (origem, destino); } else { torresHanoi (n-1, origem, destino, auxiliar); moverDisco (origem, destino); torresHanoi (n-1, auxiliar, origem, destino); } } static void moverDisco (int origem, int destino){ System.out.println (“desde : “ + origem + “-- para: ” + destino); }
O número de movimentos simples necessários para mover n discos é 2n-1 Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
16
8
Ordenação por Separação O algoritmo Quicksort Algoritmo recursivo:
fim
ini
escolher um elemento p como pivô da sequência e dividi-la em duas partes: na parte esquerda: colocam-se os valores menores do que o pivô p na parte direita: colocam-se os valores maiores do que o pivô p Logo é aplicado este mesmo procedimento de forma recursiva com a parte esquerda e direita da sequência. O algoritmo termina ao se atingir uma sequência de um elemento ⇒ a sequência está ordenada
dividir numero[i]
Algoritmos e Estruturas de Dados,
aplicar Quicksort medio
numero[i]>p
aplicar Quicksort
Escolha do pivô:
O 1º elemento da sequência O elemento do médio e depois colocar o pivó por forma a que ini, medio e fim fiquem por ordem crescente
2007-2008 , Gladys Castillo
17
O Algoritmo Quicksort static void quickSort (int int [ ]] seq, int ini, int fim) { if (seq.length == 1 || ini > fim) return; if if (seq.length == 2) { /* sequência com 2 elementos */ if if (seq[ini] > seq[fim]) trocar trocar(seq, ini, fim); if trocar return; return; } int medio = (ini + fim) /2;
/* calcular o índice do pivô no meio */
/* colocar o pivô por forma a que ini, medio e fim fiquem por ordem crescente */
colocarPivo (seq, ini, medio, fim); if (seq.length == 3) return; if return // se sequência com 3 elementos então já está ordenada /* dividir a sequencia: na parte esquerda colocam-se os valores menores do que o pivô na parte direita, os valores maiores do que o pivô */
medio = dividirSeq dividirSeq (seq, ini, medio, fim); quickSort (seq, ini, medio-1); // invocação recursiva para a parte esquerda da sequência quickSort (seq, medio+1, fim); // invocação recursiva para a parte direita da sequência } Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
18
9
O Algoritmo Quicksort
Métodos: trocar e colocarPivot private static void trocar trocar (int[ ] seq, int ind1, int ind2){ int temp = seq[ind1]; seq[ind1] = seq[ind2]; seq[ind2] = seq[temp]; } private static void colocarPivo (int[ ] seq, int ini, int medio, int fim){ /* colocar o pivô por forma a que ini , medio e fim fiquem por ordem crescente */ if (seq[ini] > seq[medio]) trocar(seq, ini, medio); if trocar if (seq[ini] > seq[fim]) trocar trocar(seq, ini, fim); if if (seq[medio] > seq[fim]) trocar(seq, medio, fim); if trocar }
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
19
O Algoritmo Quicksort Método: dividirSeq (I)
private static int dividirSeq ( int[ ] seq, int ini, int medio, int fim ){ int indEsq = ini + 1; int indDir = fim - 1; while (indEsq < indDir) { // 1º. procurar elementos na parte esquerda maiores do que pivô while (indEsq < medio){ if (seq[indEsq] > seq[medio]) break; if break indEsq++; } // 2º. procurar elementos na parte direita menores do que pivô
while (indDir > medio){ if (seq[indDir] if < seq[medio]) break; break indDir--; } Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
20
10
O Algoritmo Quicksort Método: dividirSeq (II)
// 3º. trocar elementos segundo 3 casos Caso I: existe um elemento na if (indEsq != medio && indDir != medio){ parte esquerda maior do que pivô e um elemento na parte direita trocar (seq, indEsq, indDir); trocar menor do que pivô então trocar os indEsq++; indDir--; elementos } else if if (indEsq if == medio && indDir != medio){ trocar trocar (seq, medio,indDir); Caso II: existe um elemento na parte direita menor do que pivô, medio++; então trocar o elemento de índice if (medio != indDir) trocar(seq, trocar medio,indDir); if trocar indDir com o pivô e deslocar o indEsq = medio; // a parte esquerda já está OK pivô uma posição à direita } else if (indEsq if != medio && indDir == medio){ if trocar(seq, medio,indEsq); trocar Caso III: existe um elemento na medio --; parte esquerda maior do que pivô, if (medio != indEsq) trocar trocar (seq, medio, indEsq); então trocar o elemento de índice if trocar indEsq com o pivô e deslocar o indDir= medio; } // a parte direita já está Ok pivô uma posição à esquerda } // fecho while return medio; } Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
21
Exemplo do Algoritmo Quicksort (VER Programação avançada
medio = 4
ini = 0 209
330 25
15
42
2
usando C, António Rocha, pag 189) indEsq = 2
fim = 9
32 8
55
145
42 55 25
15
indDir = 7 145
Trocar 145 com 8
Como 209, 42 e 145 não estão ordenados ⇒ Trocar 209 com 42 e 209 com 145 medio = 4 ini = 0 42
330 25
15
145
2
32
42 55 25 fim = 9 8
55
209
pivô 145: na parte esquerda colocam-se os valores menores do que o pivô e na parte direita, os valores maiores do que o pivô
executar dividirSeq(seq, 0, 4, 9) 42
330
25
15
145
2
32
8
55
209
42 55 25
25
15
8
2
32
15
8
145
15
8
145
32
145
2
32
8
330
145
330 209
2
330 209
indDir = 7 32
2
330 209
indDir = 7
medio = 6 15
8
2
145
Trocar 145 com 332 42
55
330 209
medio = 5
indEsq = 6
Trocar 330 com 55
42
8
Trocar 145 com 8, medio ++, trocar 32 com 145
indDir = 8
indEsq = 1
15
indEsq = 5 42 55 25
32
medio ++
Trocar 2 com 145
42 55 25
2
209
55 25
15
32
330 209
medio = 7 8
2
32
145
330 209
Chamar quickSort (seq, 0, 6) quickSort(seq, 8, 9)
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
22
11
Ordenação Quicksort Complexidade
Este algoritmo é considerado uns dos melhores algoritmos de ordenação, pois apesar de, no pior caso, ser um algoritmo quadrático, no caso médio tem uma complexidade linear logarítmica de comparações, ou seja de ordem O(n log 2 n) nº de comparações
pior caso: ordem quadrático caso meio: ordem linear logarítmica
WC(n) AC(n)
≈
≈
n2
1.4 n . log2 (n)
Outro algoritmo recursivo de ordenação de complexidade linear logarítmica também muito popular é o algoritmo MergeSort (ordenação por fusão). Pode ler sobre este algoritmo no livro “ Programação avançada usando C” de Antonio Rocha, pag 184
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
23
Recursividade vs. Iteração Todo problema que se resolve recursivamente pode ser resolvido iterativamente Mas como regra geral:
os algoritmos recursivos são mais simples e elegantes
se os problemas são de natureza recursivas são mais fáceis de serem implementados em linguagens de programação de alto nível
os algoritmos recursivos são menos eficientes
possuem código mais claro (legível) e mais compacto do que os correspondentes iterativos
consomem mais recursos devido à invocação sucessiva da função recursiva: gasta-se mais tempo na execução do algoritmo e muito mais memória. Porém pode valer a pena sacrificar a eficiência em beneficio da clareza
os algoritmos recursivos são mais difíceis de ser depurados
Especialmente quando for alta a profundidade da recursão, ou seja, o número máximo de chamadas simultâneas Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
24
12
Recursividade vs. Iteração Conclusão
Quando devemos usar algoritmos recursivos?
Usar se:
Os problemas são de natureza recursiva. Neste caso, a solução recursiva é a mais natural, elegante e fácil de implementar
A solução recursiva não leva a uma excessiva repetição do cálculo dos mesmos valores
A solução iterativa equivalente é muito complexa (ex: problema das Torres de Hanoi)
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
25
Bibliografia
C. Thomas Wu: An introduction to Object-Oriented Programming with Java, third edition. Chapter 15: Recursive Algorithms slides online: http://highered.mcgraw-hill.com/sites/0072518847/student_view0/chapter15/powerpoint.html
Rosália Rodrigues: Introdução à Análise e Desenvolvimento de Algoritmos. Capítulo 3. Recorrência on-line em: http://www2.mat.ua.pt/rosalia/cadeiras/ADA/Cap3Recorrencia.pdf
António Adrego da Rocha: Programação Avançada usando C. Tecnologias da Informação. FCA Editora de Informática, 2006 Capítulo 1. Recursividade, Capítulo 6.6. Algoritmos de ordenação recursivos Nota: Algumas das figuras usadas nos acetatos foram extraídas destas fontes Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
26
13
Caixa de Execução Cálculo de 4! true
O valor de palindromo(“somos”) é calculado no regresso das sucessivas invocações da função recursiva, pelo que palindromo( “somos” ) = (‘s’ == ‘s’) && (‘o’ == ‘o’) && true = true palindromo( “somos” ) = (‘s’ == ‘s’) && palindromo( “omo” ) = true && e u r t
palindromo( “omo” ) = (‘o’ == ‘o’) && palindromo( “m” ) = true && e u r t
palindromo( “m” ) = true
Algoritmos e Estruturas de Dados,
2007-2008 , Gladys Castillo
27
14