Cap 3
Cap 3
Cap 3
®
2007-2 1
Capítulo 3
Análise de Algoritmos
de Pesquisa e Ordenação
1- Algoritmos de Pesquisa
A tarefa de pesquisar (ou buscar) está entre as tarefas mais freqüentemente
encontradas no dia a dia. Nesta seção analisaremos alguns algoritmos para recuperar
informação a partir de uma massa grande de informação previamente armazenada. A
informação geralmente é dividida em registros, onde cada registro possui uma chave para
ser usada na pesquisa. O objetivo da pesquisa é encontrar uma ou mais ocorrências de
registros com chaves iguais à chave de pesquisa. Neste caso, ocorre uma pesquisa com
sucesso; caso contrário a pesquisa é sem sucesso. Um conjunto de registros é chamado de
tabela ou arquivo. Geralmente, o termo “tabela” é associado a entidades de vida curta,
criadas na memória interna durante a execução de um programa. Já o termo “arquivo” é
geralmente associado a entidades de vida mais longa, armazenadas na memória externa.
1. O elemento é encontrado.
2. Todo o conjunto foi analisado, mas o elemento não foi encontrado.
i := 1;
enquanto (i <= N) e (Ai ≠ k) faça
i := i + 1;
se i > N então retorna 0
senão retorna i
Análise
haja uma ocorrência múltipla do elemento em questão). Quando i for igual a N+1, isto
significa que não foi encontrado o elemento. Evidentemente, o término das repetições é
garantido, porque, em cada passo, i é incrementado (positivamente) e, portanto,
certamente alcançará o limite N após um número finito de passos, caso não exista o
elemento desejado no vetor.
i := 1;
An+1 := k;
Enquanto Ai ≠ k faça
i := i + 1;
se i > N então retorna 0
senão retorna i
Análise
Uma busca pode ser mais eficiente se os dados estiverem ordenados. Suponha, por
exemplo, uma lista telefônica em que os nomes não estejam listados em ordem alfabética.
Uma coisa dessas não seria muito útil.
A seguir são apresentadas duas versões de um algoritmo que usa essa idéia, uma
iterativa e a outra recursiva. Eles utilizam duas variáveis-índices e e d e marcam,
respectivamente, os limites esquerdo e direito da região de A em que um elemento ainda
pode ser encontrado.
Análise
A cada iteração do algoritmo, o tamanho da tabela é dividido ao meio. Logo, o
número de vezes que ela é dividida ao meio é cerca de lgn. Entretanto, o custo para
manter a tabela ordenada é alto: a cada inserção na posição p da tabela implica no
deslocamento dos registros a partir da posição p para as posições seguintes.
Consequentemente, a pesquisa binária não deve ser usada em aplicações muito dinâmicas.
início
se (e == d)
se (k == A[e]) retorna e
senão retorna 0
senão
início
meio = (e + d) div 2;
se (k > A[meio]) retorna PesqBinaria(A, meio+1, d, k)
senão retorna PesqBinaria(A, e, meio, k)
fim-senão
fim PesqBinaria.
Análise
Encontrando a relação de recorrência associada ao algoritmo PesquisaBinária temos:
T(n) = T(n/2) + 1, T(1) = 1.
2 – Algoritmos de Ordenação
Os algoritmos de ordenação (ou classificação) constituem bons exemplos de como
resolver problemas utilizando computadores. As técnicas de ordenação permitem
apresentar um conjunto amplo de algoritmos distintos para resolver a mesma tarefa.
Dependendo da aplicação, cada algoritmo considerado possui uma vantagem particular
sobre os outros. Além disso, os algoritmos ilustram muitas regras básicas para a
manipulação de estrutura de dados.
Um método de ordenação é dito estável se a ordem relativa dos itens com chaves
iguais mantém-se inalterada pelo processo de ordenação. Por exemplo, se uma lista
alfabética de nomes de funcionários de uma empresa é ordenada pelo campo salário, então
um método estável produz uma lista em que os funcionários com mesmo salário aparecem
em ordem alfabética.
1
Para alguns métodos eficientes (HeapSort, ShellSort e QuickSort) esta análise teórica é
incompleta, pois muitas vezes envolve solução de problemas matemáticos que ainda estão
em aberto.
Análise de Algoritmos de Pesquisa e Ordenação Elton Silva
®
2007-2 5
ii) Para a ordenação de registros com chaves grandes, constrói-se um procedimento que
gere valores randômicos para gerar as várias chaves a serem inseridas no vetor. Para isto,
usa-se a função random (ou uma similar na linguagem escolhida) para gerar os números.
Cada chave deve ser formada pela concatenação de n caracteres obtidos da conversão de
cada número gerado. Para que as várias chaves tenham valores distintos usa-se o
procedimento randomize (ou correspondente) antes de iniciar a geração de uma chave.
obs.: o tamanho da estrutura dos itens a serem ordenados influencia diretamente no tempo
em relação ao número de movimentações realizadas na estrutura, uma vez que em muitas
linguagens ocorre a chamada deep copy nas atribuições.
iii) Coleta do tempo gasto para ordenar conjuntos com números crescentes de elementos,
por exemplo 100, 500, 1000, 2000, 3000, etc. Para obter o tempo, insere-se no programa
comandos de interface com o sistema operacional e obtém-se, por subtração, o valor
desejado.
2.1. BubleSortSort
Idéia básica do método
trocas
Seqüência inicial 44 55 12 42 94 18 06 67 06 ↔ 18
44 55 12 42 94 06 18 67 06 ↔ 94
44 55 12 42 06 94 18 67 06 ↔ 42
44 55 12 06 42 94 18 67 06 ↔ 12
44 55 06 12 42 94 18 67 06 ↔ 55
44 06 55 12 42 94 18 67 06 ↔ 44
06 44 55 12 42 94 18 67 18 ↔ 94
06 44 55 12 42 18 94 67 18 ↔ 42
06 44 55 12 18 42 94 67 12 ↔ 55
06 44 12 55 18 42 94 67 12 ↔ 44
06 12 44 55 18 42 94 67 67 ↔ 94
06 12 44 55 18 42 67 94 18 ↔ 55
06 12 44 18 55 42 67 94 18 ↔ 44
06 12 18 44 55 42 67 94 42 ↔ 55
06 12 18 44 42 55 67 94 42 ↔ 44
Seqüência final (ordenada) 06 12 18 42 44 55 67 94
As comparações nas chaves são realizadas no if da linha 5. Das linhas 4 a 7 são feitas n-i+1
comparações. No total, o número de comparações realizadas da linha 3 a linha 7 é
n
(n − 1) + 1 n2 − n
∑ (n − i + 1) = (n − 1) + (n − 2) + ... + 1 =
i =2 2
.(n − 1) =
2
= O(n 2 ).
O pior caso acontece quando o vetor se encontra inversamente ordenado, pois a cada
passo é feito o número máximo de movimentações associadas à linha 6. O número de
n
3n 2 − 3n
movimentações total M(n) é igual a 3.∑ (n − i + 1) = = O(n 2 ).
i=2 2
Estabilidade do método
Seqüência inicial 3 4 2 3* 1 1 ↔ 3*
3 4 2 1 3* 1↔2
3 4 1 2 3* 1↔4
3 1 4 2 3* 1↔3
1 3 4 2 3* 2↔4
1 3 2 4 3* 2↔3
1 2 3 4 3* 3* ↔ 4
Seqüência final (ordenada) 1 2 3 3* 4
É claro que este exemplo sozinho não prova que o algoritmo é estável. A prova dessa
propriedade vem do fato do BubbleSort trocar somente elementos de posições adjacentes
(o que não acontece, por exemplo, no método SelectionSort que veremos na seção a
seguir).
Análise de Desempenho
Tempo
Nº de Chaves Aleatório Ordenado Invertido (ms) C(n) M(n)
100 x 16 9801 2486
100 x 0 9801 0
100 x 16 9801 4950
400 x 31 159201 41701
400 x 31 159201 0
400 x 32 159201 79800
500 x 62 249001 62342
500 x 47 249001 0
500 x 63 249001 124750
600 x 79 358801 90762
600 x 63 358801 0
600 x 94 358801 179709
1000 x 218 998001 247909
1000 x 203 998001 0
1000 x 235 998001 499500
1500 x 547 2247001 562680
1500 x 531 2247001 0
1500 x 562 2247001 1124250
Análise de Algoritmos de Pesquisa e Ordenação Elton Silva
®
2007-2 8
2.2. SelectionSort
Idéia básica do método
Seqüência inicial 44 55 12 42 94 18 06 67 06 ↔ 44
06 55 12 42 94 18 44 67 12 ↔ 55
06 12 55 42 94 18 44 67 18 ↔ 55
06 12 18 42 94 55 44 67 42 ↔ 42
06 12 18 42 94 55 44 67 44 ↔ 94
06 12 18 42 44 55 94 67 55 ↔ 55
06 12 18 42 44 55 67 94 67 ↔ 67
Seqüência final (ordenada) 06 12 18 42 44 55 67 94
No algoritmo de Seleção Direta, a ordem inicial das chaves não interfere no desempenho do
mesmo, já que é necessário, a cada iteração do método, efetuar a comparação com os n-i
elementos do arranjo, para uma dada iteração i.
A cada iteração é realizada uma troca (linhas 6, 7, 8), que se constitui de 3 movimentações
no arranjo. Ou seja, a cada iteração são realizadas 3 movimentações. Assim, o número
total de movimentações M(n) = 3n-3. Portanto, a complexidade desse método em relação
ao número de movimentações realizadas é, no pior caso, no melhor caso e no caso
médio, O(n).
Instabilidade do método
Observe que, nesse exemplo, na primeira iteração do método Seleção Direta, haverá a
troca entre o elemento 0 e o elemento 4¹:
1ª iteração: [ 0 | 6 7 2 9 8 4² 1 3 4³ 9 4¹ ]
2ª iteração: [ 0 1 | 7 2 9 8 4² 6 3 4³ 9 4¹ ]
3ª iteração: [ 0 1 2 | 7 9 8 4² 6 3 4³ 9 4¹ ]
4ª iteração: [ 0 1 2 3 | 9 8 4² 6 7 4³ 9 4¹ ]
5ª iteração: [ 0 1 2 3 4² | 8 9 6 7 4³ 9 4¹ ]
Análise de Algoritmos de Pesquisa e Ordenação Elton Silva
®
2007-2 10
11ª iteração: [ 0 1 2 3 4² 4³ 4¹ 6 7 8 9 9 ]
Visto que a ordem relativa dos elementos de chaves iguais inicialmente apresentados (4¹,
4², 4³) foi alterada (para 4² 4³ 4¹), pode se concluir que o método é instável.
Análise de Desempenho
Através de experimentos realizados com estruturas pesadas, observou-se que não houve
diferença no tempo de ordenação entre as estruturas utilizadas. Isso se justifica pela
linguagem adotada (C++) para a implementação do método, pois a mesma trabalha com
ponteiros e, dessa forma, movimenta-se apenas as referências para os registros.
2.3. InsertionSort
Idéia básica do método
A classificação por inserção é parecida com a forma como organizamos as cartas de um
baralho. É caracterizada pelo princípio no qual os n dados a serem ordenados são divididos
em dois segmentos: um já ordenado e outro a ser ordenado. Num momento inicial, o
primeiro segmento é formado por apenas um elemento, que, portanto, pode ser
considerado como já ordenado. O segundo segmento é formado pelos n-1 restantes
elementos. A partir daí o processo se desenvolve em n-1 iterações, sendo que em cada
uma delas um elemento do segmento não ordenado é transferido para o primeiro
segmento, sendo inserido em sua posição correta em relação àqueles que lá já se
encontravam.
Seqüência inicial 44 55 12 42 94 18 06 67
44 55 12 42 94 18 06 67
44 55 12 42 94 18 06 67
12 44 55 42 94 18 06 67
12 42 44 55 94 18 06 67
12 42 44 55 94 18 06 67
12 18 42 44 55 94 06 67
06 12 18 42 44 55 94 67
Seqüência final (ordenada) 06 12 18 42 44 55 67 94
2
Utilizamos uma idéia semelhante no algoritmo de pesquisa linear com sentinela, só que a
sentinela ficava na posição n+1 ao invés da posição 0.
Análise de Algoritmos de Pesquisa e Ordenação Elton Silva
®
2007-2 12
Melhor Caso: ocorre quando a seqüência já está ordenada. Neste caso, o algoritmo
executa a comparação da linha (6) apenas uma vez a cada iteração. Logo, C(n) = n-1 para
todo o algoritmo (o PARA da linha 1 é executado n-1 vezes), e a ordem de complexidade
do algoritmo é O(n).
Pior Caso: ocorre quando a seqüência está em ordem decrescente. Neste caso, são feitas i
comparações na linha (6) até ser encontrada a posição correta de inserção do elemento
n
corrente. Logo, C(n) = ∑ i = 2+3+...+n = (n +n-2)/2, e a ordem de complexidade do
i =2
2
algoritmo é O(n2).
Melhor Caso: como no cálculo de C(n), o melhor caso de M(n) ocorre quando os
elementos do vetor já estão ordenados. Neste caso, as únicas movimentações feitas são as
das linhas (3), (4) e (11). A movimentação da linha (6) não é feita nenhuma vez, pois a
condição do ENQUANTO nunca é satisfeita. Como são feitas n-1 iterações no total (linha 1),
o número total de movimentações M(n) = 3n-3, ou seja, o algoritmo é O(n) em relação a
M(n).
Pior Caso: ocorre quando os elementos estão inversamente ordenados. Neste caso, são
feitas i-1 movimentações referentes à linha (8), acrescidas das 3 movimentações das linhas
(3), (4) e (11), o que totalizam i+2 movimentações das linhas (2) a (12). Logo, M(n) =
n
∑i + 2 =
i =2
4+5+...+(n+2) = (n2+5n-6)/2, e a ordem de complexidade associada do
algoritmo é O(n2).
Estabilidade do método
Análise de Desempenho
Existem duas variações deste método muito interessantes. Uma delas utiliza busca binária
na parte ordenada do vetor. Infelizmente este recurso não melhora notavelmente a função
de complexidade do algoritmo original, pois o custo é fortemente influenciado pelo numero
de trocas de posições no vetor, e mesmo com a busca binária, esse numero não é alterado.
A outra variação utiliza a estrutura de lista ligada ao invés de vetor. Isto melhora o
algoritmo pois não é necessário arredar os elementos (basta fazer um ajuste de ponteiros),
fazendo-se muitas trocas de posição. Infelizmente, com este recurso fica impossível utilizar
a pesquisa binária pois perde-se a noção de acesso direto a uma posição.
2.4. QuickSort
Idéia básica do método
Baseia-se numa idéia muito simples: partir o vetor em dois subvetores, de tal maneira que
todos os elementos do primeiro sejam menores ou iguais a todos os elementos do segundo.
Estabelecida a partição, o problema está resolvido, pois basta aplicar recursivamente a
mesma técnica a cada um dos subvetores, enquanto os subvetores tiverem dois ou mais
elementos.
Para fazer essa partição dos vetores (que é a base do QuickSort), começa-se por escolher
um elemento arbitrário no vetor, por exemplo, o do meio. Este elemento é o pivô. Depois,
percorre-se o vetor da esquerda para a direita, procurando um elemento que não seja
menor do que o pivô (tem de se encontrar, porque o pivô está no vetor e o pivô não é
menor do que ele próprio); e percorre-se também da direita para a esquerda, procurando
um elemento que não seja maior do que o pivô (tem de se encontrar, pela mesma razão).
Nessa altura trocam-se os dois elementos (aquele que não era menor do que o pivô, e que
tinha sido encontrado no percurso ascendente e aquele que não era maior, e tinha sido
Análise de Algoritmos de Pesquisa e Ordenação Elton Silva
®
2007-2 14
Seqüência inicial 44 55 12 42 94 18 06 67
06 55 12 42 94 18 44 67
06 18 12 42 94 55 44 67
06 12 18 42 94 55 44 67
06 12 18 42 94 55 44 67
06 12 18 42 44 55 94 67
Seqüência final (ordenada) 06 12 18 42 44 55 67 94
(1) {
(2) Sort(1,n)
(3) };
Análise do Algoritmo
O custo para percorrer o vetor comparando-se cada elemento com o pivô (linhas 5 e
6), é linear, ou seja, Θ(n).
Pior Caso: ocorre quando o algoritmo de particionamento (linhas 1 a 12) produz a cada
chamada um subproblema de tamanho n-1 e outro de tamanho 1. Daí podemos extrair a
seguinte relação de recorrência:
T(1) = Θ(1)
T(n) = T(n-1) + Θ(n)
Vale observar que o pior caso tem uma possibilidade muito remota de acontecer quando os
elementos forem aleatórios.
Melhor Caso: ocorre quando o particionamento divide o vetor a cada chamada em dois
subproblemas de tamanhos aproximadamente iguais a n/2, que produz a seguinte relação
de recorrência:
T(1) = Θ(1)
T(n) = 2T(n/2) + Θ(n)
Instabilidade do método
O QuickSort é instável, ou seja, a posição relativa de itens com chaves iguais é alterada
após a ordenação.
Análise de Desempenho
A bateria de testes com o QuickSort foi realizada em um computador Athlon XP2000, 256
Mb de memória RAM. O algoritmo foi implementado em linguagem Java. Foram utilizados
para ordenação vetores de strings[200] geradas com sorteio aleatório de caracteres. A
seguir apresentamos alguns dos resultados obtidos:
Análise de Algoritmos de Pesquisa e Ordenação Elton Silva
®
2007-2 16
VETOR ALEATÓRIO
1400
1200
1000
800
Tempo
600
400
200
0
-200 0 50000 100000 150000
Chaves
10000 overflow
20000 overflow
30000 overflow
50000 overflow
100000 overflow
120000 overflow
130000 overflow
800
700
600
500
Tempo
400
Seqüência1
300
200
100
0
-100 0 1000 2000 3000 4000
Chaves
É um método rápido para ordenar grandes entradas. A menor eficiência deste método com
certas distribuições de dados, resulta do QuickSort não considerar a ordem parcial dos
dados a ordenar. De fato, quando aplicado a conjunto de dados quase totalmente
ordenados, comporta-se de forma menos eficaz que a de outros algoritmos de ordenação,
como o método de inserção direta. Uma das formas de evitar o pior caso é escolher três
itens quaisquer do vetor e usar a mediana dos três como o pivô na partição.
Bibliografia
C.A.R. HOARE: Quicksort. Computer Journal, Vol. 5, 1, 10-15 (1962)
Carlos Augusto Santiago, Elayne Ferreira de Souza, Filipe Nunes Ribeiro, Silas Sallaume,
Inserção Direta, trabalho apresentado na disciplina CIC210, Departamento de Computação,
UFOP, novembro 2004.
Marcio Tadayuki Mine, Matheus de Souza Alves Silva, Valter Cezar Prado Júnior, Rodolfo
Dian de Oliveira Aguiar Soares, Lucas Reis Costa, Estudo sobre o Algoritmo SelectionSort,
trabalho apresentado na disciplina CIC210, Departamento de Computação, UFOP,
novembro 2004.
Rodrigo Reis, João Paulo de Melo Elias, QuickSort, trabalho apresentado na disciplina
CIC210, Departamento de Computação, UFOP, novembro 2004.
Análise de Algoritmos de Pesquisa e Ordenação Elton Silva
®
2007-2 19
Exercícios de Fixação
1. Desenvolva um algoritmo de busca binária que divide, a cada passo, o conjunto de
elementos em dois conjuntos, um de tamanho aproximadamente duas vezes maior que o
tamanho do outro. Compare a complexidade desse algoritmo com o algoritmo de busca
binária visto em sala.
2. Seja A = (a1, a2, ..., an) um vetor ordenado em ordem crescente, que armazena n
números inteiros. Considere o algoritmo de busca binária que, dado um valor inteiro x,
determina se existe i ∈ {1, 2, ..., n} tal que ai = x. Especifique um algoritmo recursivo de
busca ternária, baseado na partição do intervalo de busca em três partes “iguais”. Faça
uma análise comparativa dos algoritmos de busca binária e de busca ternária em termos de
suas complexidades relacionadas ao número de comparações e chamadas recursivas.
t Bin
t
AVL AVL
Bin
(inserção) (deleção)
9. Escreva um algoritmo para testar se um vetor já está ordenado (ordem crescente). Qual
a ordem de complexidade da sua solução em relação ao número de comparações realizadas
na estrutura?
11. Ordene o vetor de caracteres O R D E N A pelos métodos (a) bolha, (b) seleção e (c)
inserção. Mostre o estado do vetor passo a passo.
12. Faça o diagrama de execução do QuickSort para o seguinte vetor de inteiros: 44, 55,
12, 42, 94, 18, 06, 67.
BubbleSort
(1) ( ) A principal desvantagem do BubbleSort é que se o vetor já estiver ordenado, M(n) é
máximo.
(2) ( ) Se alterarmos a condição da linha 4 para "se A[j] <= A[j-1]" o algoritmo deixa de
ser estável.
(3) ( ) Se alterarmos a condição da linha 4 para "se A[j] > A[j-1]" o vetor fica
inversamente ordenado após a ordenação.
(4) ( ) Podemos dizer que o algoritmo BubbleSort funciona como um tipo de seleção
direta, só que dá mais trabalho para colocar cada elemento em sua posição correta em
cada passo da ordenação.
SelectionSort
(1) ( ) Se estiver utilizando uma linguagem que faz deep copy (ex.: Pascal), o método não
é recomendável para ordenar arquivos com registros pesados (registros com grande
quantidade de informação).
(2) ( ) A complexidade do algoritmo em relação a C(n) é quadrática para vetores
previamente ordenados, aleatórios ou em ordem decrescente.
(3) ( ) O algoritmo nunca altera a posição relativa de itens com chaves iguais durante a
ordenação.
(4) ( ) É possível reduzir M(n) quando os itens já estiverem previamente ordenados.
(5) ( ) Se o vetor possui todas as chaves iguais, o SelectionSort possui M(n) igual ao
BubbleSort.
InsertionSort
(1) ( ) O uso de sentinela ajuda a simplificar o teste da condição associada à linha (6) para
inserir A[i] na sequência destino.
(2) ( ) O algoritmo tem complexidade linear para C(n) e M(n) quando os itens já estão
ordenados.
(3) ( ) Se usarmos pesquisa ternária para encontrar o local adequado na parte já ordenada
do vetor consegue-se reduzir C(n).
(4) ( ) Para um vetor inversamente ordenado, a variação do algoritmo Inserção Binária
piora M(n).
(5) ( ) O deslocamento de seqüências de elementos na linha (6) é uma das principais
vantagens do InsertionSort.
QuickSort
(1) ( ) O pior caso do QuickSort acontece quando, a cada passo, o pivô é o menor ou
maior elemento da partição.
(2) ( ) O melhor caso do QuickSort acontece quando a cada passo, o pivô escolhido está
na posição central do vetor.
(3) ( ) Em linguagens mais antigas, onde o emprego de recursividade não é permitido, não
é possível implementar o método QuickSort.
(4) ( ) Uma maneira de evitar o pior caso do algoritmo é escolher ao acaso uma pequena
amostra do arranjo e usar a mediana dessa amostra como pivô na partição sendo
ordenada.
(5) ( ) O QuickSort é O(nlgn) quando todos os elementos do vetor possuem chaves iguais.
Análise de Algoritmos de Pesquisa e Ordenação Elton Silva
®
2007-2 22
15. Sabe-se que a cota inferior para o problema de ordenação é n.lgn. José A. Pressado e
João Sá Bido dizem ter desenvolvido, cada um, um algoritmo que é capaz de ordenar
qualquer conjunto de n elementos com complexidade O(n3/2) e O(n2/3) respectivamente.
Você compraria esses algoritmos? Justifique a sua resposta cuidadosamente.
18. Faça uma análise de complexidade de melhor caso e pior caso relacionada ao número
de comparações C(n) e trocas T(n) da seguinte implementação do BubbleSort.