1. Comparação Experimental de
Algoritmos de Ordenação
Jéssica Valeska da Silva
Lenon Fachiano Silva
Prof. Dr. Danilo Medeiros Eler
Análise e Projeto de Algoritmo
PPG Ciência da Computação
2015
2.
3. 1 Introdução
Este trabalho foi realizado na disciplina de Análise e Projeto de Algoritmos do
Programa de Pós-Graduação em Ciência da Computação da UNESP.
1.1 Objetivo
O presente trabalho tem por objetivo apresentar a análise de algoritmos de or-
denação, comparando resultados experimentais com a análise assintótica destes mesmos
procedimentos.
1.2 Metodologia
1.2.1 Materiais
Para realização da análise experimental, utilizou-se um notebook LG P420-G.BE41P1
(5100), sistema operacional Windows 8.1 Pro 64 bits, processador Intel(R) Core(TM) i3-
2310M CPU @ 2.10 GHz e 4 Gb de memória RAM. Os algoritmos foram codificados em
linguagem Java, contando com apoio do NetBeans IDE 8.0.2.
1.2.2 Realização do Experimento
Uma vez codificados os algoritmos, mediu-se o intervalo de tempo necessário para
cada um realizar o procedimento de ordenação. Como entrada para o método, utilizou-se
um vetor do tipo inteiro (𝑖𝑛𝑡[ ] 𝑣𝑒𝑐𝑡𝑜𝑟). Contudo, alguns algoritmos executavam muito
rápido o procedimento, dificultando a captura do intervalo de tempo. Desta maneira,
optou-se por realizar a tarefa 10 vezes para uma mesma entrada. Assim, o vetor de entrada
foi copiado dez vezes, sendo cada um ordenado uma vez e seus tempos somados.
Foram utilizados como entrada vetores de ordenados em ordem crescente, decres-
cente e aleatórios, sendo os dois primeiros gerados por um função da classe 𝑉 𝑒𝑐𝑡𝑜𝑟𝐺𝑒𝑛𝑒𝑟𝑎𝑡𝑜𝑟
e o terceiro copiado de um arquivo, a fim de garantir que os mesmos valores fossem utili-
zados em todas as medidas. O comprimento dos vetores foram: 5000, 10000, 20000, 25000
e 30000 elementos.
Por fim, verificou-se o tempo de execução para todos os métodos, utilizando cada
combinação de orientação de vetor e comprimento como entrada. Contudo, a fim de evitar
certos erros derivados do sistema, o procedimento foi repetido dez vezes para cada entrada.
4. Como saída, foi gerado um arquivo 𝑐𝑠𝑣 com os dados brutos. Os gráficos para análise foram
gerados por meio do software LibreOffice Calc 4.4.
No capítulo 2 são apresentados os resultados dos experimentos e a comparação
com a análise assintótica. Por fim, no capítulo 3, é apresentada um discussão sobre os
resultados obtidos.
5. 2 Experimento e Análise dos Algoritmos de
Ordenação
2.1 BubbleSort
O algoritmo BubbleSort baseia-se em troca para realizar a ordenação de elementos.
Compara dois elementos; caso o segundo seja menor que o primeiro, troca suas posições.
É necessário percorrer o vetor várias vezes, sempre comparando elementos adjacentes. Em
𝐴𝑙𝑔𝑜𝑟𝑖𝑡ℎ𝑚 1 é possível observar um pseudcódigo desse algoritmo.
Algorithm 1 BubbleSort
1: function BubbleSort(V, N)
2: for 𝑗 < −1 : 𝑁 − 1 do
3: for 𝑖 < −1 : 𝑁 − 1 do
4: if 𝐴[𝑖] > 𝐴[𝑖 + 1] then
5: 𝑎𝑢𝑥 < − 𝐴[𝑖]
6: 𝐴[𝑖] < − 𝐴[𝑖 + 1]
7: 𝐴[𝑖 + 1] < − 𝑎𝑢𝑥
8: end if
9: end for
10: end for
11: end function
Pode-se observar que o método é dominado por dois laços encadeados da ordem
de 𝑛 cada um, sendo 𝑛 o número de elementos do vetor de entrada. Desta maneira,
em seu pior caso, o algoritmo apresenta complexidade da ordem 𝑂(𝑛2
). Contudo, em seu
melhor caso (vetor de entrada em ordem ascendente), a instrução interna não é executada,
tornando o BubbleSort 𝑂(𝑛).
Uma interessante alteração que pode ser realizada neste algoritmo é a verificar
se o vetor já está ordenado. As instruções acrescida possuem complexidade 𝑂(1), não
afetando a ordem de complexidade do programa e evitando que sejam realizadas iterações
desnecessárias.
Na Figura 1 é possível observar que a modificação no algoritmo não alterar o
número de trocas de elementos no vetores. Contudo, como observado nas figuras 2 e 3,
o tempo de execução para o algoritmo alterado é menor. Essa característica é verificada
para qualquer tipo de vetor de entrada, mas mais notável no de ordem ascendente.
6. (a) BubbleSort original (b) BubbleSort melhorado
Figura 1 – Movimento de registros
Figura 2 – Tempo de execução do algoritmo BubbleSort
Figura 3 – Tempo de execução para o algoritmo BubbleSort Melhorado
7. 2.2 QuickSort
O algoritmo QuickSort utiliza a ideia de dividir e conquistar para trocar elementos
distantes. Nessa abordagem, um vetor é dividido em dois menores que serão ordenados
independentemente e combinados para produzir o resultado final. Existem basicamente
três passos: eleja um elemento do vetor como pivô; reordene os elementos de tal maneira
que todos os que estiverem a esquerda do pivô sejam menores que ele e o que estiverem
à direita sejam maiores (após este passo o pivô está na posição correta); remaneje cada
subvetor de maneira independente.
Em 𝐴𝑙𝑔𝑜𝑟𝑖𝑡ℎ𝑚 2 é possível observar um pseudocódigo do QuickSort. Vale observar
que a definição do elemento pivô e feita na 𝑓 𝑢𝑛𝑐𝑡𝑖𝑜𝑛 𝑃 𝑎𝑟𝑡𝑖𝑐𝑎𝑜.
Algorithm 2 QuickSort
1: function QuickSort(V, p, r)
2: if 𝑝 < 𝑟 then
3: 𝑞 < −𝑃 𝑎𝑟𝑡𝑖𝑐𝑎𝑜(𝑉, 𝑝, 𝑟)
4: 𝑄𝑢𝑖𝑐𝑘𝑆𝑜𝑟𝑡(𝑉, 𝑝, 𝑞 − 1)
5: 𝑄𝑢𝑖𝑐𝑘𝑆𝑜𝑟𝑡(𝑉, 𝑞 + 1, 𝑟)
6: end if
7: end function
8:
9: function Particao(V,p,r)
10: 𝑥 < −𝑉 [𝑝]
11: 𝑢𝑝 < −𝑟
12: 𝑑𝑜𝑤𝑛 < −𝑝
13: while down < p do
14: while V[down] <= x do
15: 𝑑𝑜𝑤𝑛 < −𝑑𝑜𝑤𝑛 + 1
16: end while
17: while V[up] > x do
18: 𝑢𝑝 < −𝑢𝑝 − 1
19: end while
20: if down < p then
21: 𝑡𝑟𝑜𝑐𝑎(𝑉 [𝑑𝑜𝑤𝑛], 𝑉 [𝑢𝑝])
22: end if
23: end while
24: 𝑉 [𝑝] < −𝑉 [𝑢𝑝]
25: 𝑉 [𝑢𝑝] < −𝑥
26: return up
27: end function
Seguindo o Teorema mestre da complexidade de algoritmos, tem-se que a recursão
desse algoritmo pode representada por
𝑇(𝑛) = 2𝑇(𝑛/2) + 𝑛 (2.1)
8. Desta maneira, pelo segundo caso, obtém-se:
𝑐1 * 𝑛𝑙𝑜𝑔2
2 ≤ 𝑛 ≤ 𝑐2 * 𝑛𝑙𝑜𝑔2
2 (2.2)
Tomando 𝑐1 = 𝑐2 = 1, têm-se uma afirmação verdadeira. Logo, a complexidade do
QuickSort é dado por
Θ(𝑛 * 𝑙𝑜𝑔𝑛) (2.3)
Contudo, vale ressaltar que no pior caso desse algoritmo (vetor ordenado), ele
apresenta comportamento próximo de Θ(𝑛2
) . Isso deve-se ao fato de que a cada divisão
são criadas uma partição com 1 elemento e outra com 𝑛 − 1, gerando uma recorrência
como descrito na Equação (2.4).
𝑇(𝑛) = 𝑇(𝑛 − 1) + 𝑛 (2.4)
(a) QuickSort: primeiro elemento como pivô (b) QuickSort: elemento central como pivô
Figura 4 – Movimento de registros
A importância da escolha do pivô pode ser observada na Figura 4 . Em 4a, que
utiliza o primeiro elemento do vetor para esta tarefa, o número de movimentos de registros
é muito superior ao de 4b, que utiliza o elemento central, considerando um vetor como
valores aleatórios como entrada. Contudo, no pior caso, apesar de próximos, a primeira
abordagem realiza um número menor de movimentos.
Porém, isso não implica diretamente em um tempo superior de execução. Obser-
vando as Figuras 5 e 6, nota-se que utilizando o primeiro elemento como pivô, o algoritmo
QuickSort registrou tempo de excução próximo de zero, enquanto a abordagem que faz
uso do elemento central apresentou um tempo superior. Contudo, convém ressaltar que
para vetores já ordenados, a segunda apresentou tempo de execução idêntico, enquanto a
primeira foi superior em ambos casos.
9. Figura 5 – Tempo de execução do algoritmo QuickSort: primeiro elemento como pivô
Figura 6 – Tempo de execução para o algoritmo QuickSort: elemento central como pivô.
2.3 InsertionSort
InsertionSort (Inserção Simples) é um algoritmo que consiste basicamente em orde-
nar um conjunto inserindo seus elementos em um subconjunto já ordenado. Em 𝐴𝑙𝑔𝑜𝑟𝑖𝑡ℎ𝑚
3, é possível um pseudocódigo referente a esta abordagem.
(𝑛 − 1) + (𝑛 − 2) + (𝑛 − 3) + ... + 2 + 1 = (𝑛 − 1) *
𝑛
2
(2.5)
A complexidade deste algoritmo é dominada pelos dois 𝑓 𝑜𝑟. O conjunto de instru-
ções referentes a essas estruturas de repetição, cria um somatório de comparações como o
visto na equação (2.5). Assim, pode-se observar que no pior caso esse algoritmo é 𝑂(𝑛2
).
10. Algorithm 3 InsertionSort
1: function InsertionSort(V, N)
2: for 𝑘 < −1; 𝑘 < 𝑛; 𝑘 + + do
3: 𝑦 < − 𝑉 [𝑘]
4: for 𝑖 < −𝑘 − 1 : 𝑖 >= 0 𝐴𝑁 𝐷 𝑉 [𝑖] > 𝑦 do
5: 𝑉 [𝑖 + 1] < − 𝑉 [𝑖]
6: end for
7: 𝑉 [𝑖 + 1] < − 𝑦
8: end for
9: end function
Contudo, para um vetor já ordenado, a complexidade vai ser reduzida a percorrer os
𝑛 elementos do vetor, sendo dominado assintoticamente por 𝑂(𝑛)(LEISERSON et al.,
2002).
Figura 7 – Número de movimentos de registro do algoritmo InsertionSort
Figura 8 – Tempo de execução do algoritmo InsertionSort
Na Figura 7, observa-se que que o número de movimentos para o vetor ascendente
é nulo. Já para vetores em ordem reversa e com elementos aleatórios, esses valores são
11. relativamente elevados. Convém ainda ressaltar que isso reflete no tempo de execução,
como visto na Figura 8.
2.4 ShellSort
Este algoritmo pode ser considerado uma melhoria do InsertionSort, permitindo
trocas entre elementos distantes entre si. Consiste basicamente em dividir a entrada em
k-subconjuntos e aplicar a Inserção Simples a cada um, sendo que k é reduzido sucessiva-
mente. Em 𝐴𝑙𝑔𝑜𝑟𝑖𝑡ℎ𝑚 4 é possível observar um pseudocódigo deste algoritmo.
Algorithm 4 ShellSort
1: function ShellSort(V, N, increments, numinc)
2: for 𝑖 = 0; 𝑖 < 𝑛𝑢𝑚𝑖𝑛𝑐; 𝑖 + + do
3: 𝑠𝑝𝑎𝑛 < − 𝑖𝑛𝑐𝑟𝑒𝑚𝑒𝑛𝑡𝑠[𝑖]
4: for 𝑗 < −𝑠𝑝𝑎𝑛; 𝑗 < 𝑁; 𝑗 + + do
5: 𝑦 < − 𝑉 [𝑗]
6: end for
7: 𝑉 [𝑘 + 𝑠𝑝𝑎𝑛] < − 𝑦
8: end for
9: end function
A complexidade deste algoritmo é um problema aberto. Na verdade, sua com-
plexidade depende da sequência de 𝑔𝑎𝑝 e ninguém ainda foi capaz de analisar seu có-
digo(ZIVIANI, 2007). Contudo, podem ser inferidos os teoremas 2.4.1 e 2.4.2(LANG,
2010).
Figura 9 – Número de movimentos de registro do algoritmo ShellSort
12. Teorema 2.4.1 Para a sequência de incrementos 1, 3, 7, 15, 31, 63, 127, ..., 2 𝑘
− 1, o
algoritmo ShellSort necessita de 𝑂(𝑛 *
√
𝑛) passos para ordenar um vetor que possui 𝑛
elementos.
Teorema 2.4.2 Para a sequência de incrementos 1, 2, 3, 4, 6, 8, 9, 12, 16, ..., 2 𝑝
3 𝑞
, o
algoritmo ShellSort necessita de 𝑂(𝑛 * 𝑙𝑜𝑔(𝑛)2
) passos para ordenar um vetor que possui
𝑛 elementos.
Na figura 9 é possível observar como o número de movimento de registros em um
vetor ordenado é consideravelmente menor que nos demais casos. Já na figura 10, nota-se
que o tempo de execução do algoritmo para o vetor de valores aleatórios e muitos superior
aos demais casos.
Figura 10 – Tempo de execução do algoritmo ShellSort
2.5 SelectionSort
Este algoritmo, também chamado de Seleção Simples, possui como ideia básica
selecionar um elemento e colocá-lo em sua posição correta. Para tal, seleciona o menor
item do vetor e troca-o de lugar com o da primeira posição, repetindo isto para todos
os demais elementos 𝑛 − 1 elementos restantes. Em 𝐴𝑙𝑔𝑜𝑟𝑡ℎ𝑚 5, é possível observar um
pseudocódigo deste algoritmo.
O algoritmo SelectionSort compara, a cada iteração, um elemento com todos os
demais não ordenados, visando a encontrar o menor. Desta maneira, na primeira iteração
são comparados 𝑛 − 1 elementos, em seguida 𝑛 − 2 e assim por diante(ZIVIANI, 2007).
Assim, obtém-se o somatório expresso na Equação 2.6. Logo, pode-se concluir que esta
abordagem é dominada assintoticamente por 𝑂(𝑛2
), não existindo melhora caso o vetor
este ordenado ou em ordem inversa.
13. Algorithm 5 SelectionSort
1: function SelectionSort(V, N)
2: for 𝑖 < − 0 : 𝑁 − 1 do
3: 𝑚𝑒𝑛𝑜𝑟 < − 𝑉 [𝑖]
4: 𝑖𝑛𝑑𝑒𝑥 < − 𝑖
5: for 𝑗 < − 𝑖 + 1 : 𝑁 do
6: if 𝑉 [𝑗] < 𝑚𝑒𝑛𝑜𝑟 then
7: 𝑚𝑒𝑛𝑜𝑟 < − 𝑉 [𝑗]
8: 𝑖𝑛𝑑𝑒𝑥 < − 𝑗
9: end if
10: end for
11: 𝑉 [𝑖𝑛𝑑𝑒𝑥] < − 𝑉 [𝑖]
12: 𝑉 [𝑖] < − 𝑚𝑒𝑛𝑜𝑟
13: end for
14: end function
(𝑛 − 1) + (𝑛 − 2) + (𝑛 − 1) + ... + 2 + 1 = 𝑛 *
𝑛 − 1
2
=
𝑛2
2
−
𝑛
2
(2.6)
Figura 11 – Número de movimentos de registro do algoritmo SelectionSort
Na figura 11, nota-se que independente da orientação do vetor, nas execuções foram
realizados o mesmo número de movimentação de registros. Já na figura 12, percebe-se que,
apesar da mesma complexidade em todos os casos, o vetor em ordem reversa necessitou
de muito mais tempo para realizar a operação de ordenação.
2.6 HeapSort
O algoritmo HeapSort utiliza o mesmo princípio do SelectionSort para realizar
a ordenação. Contudo, faz uso de uma estrutura de dados específica: o ℎ𝑒𝑎𝑝. Com a
utilização dessa estrutura, o custo para recuperar o menor elemento é drasticamente
reduzido (ZIVIANI, 2007).
14. Figura 12 – Tempo de execução do algoritmo SelectionSort
Essa abordagem consiste basicamente em construir um ℎ𝑒𝑎𝑝 𝑚á𝑥𝑖𝑚𝑜, trocar o
primeiro elemento com o último, diminuir o tamanho do ℎ𝑒𝑎𝑝 e rearranjar o ℎ𝑒𝑎𝑝 𝑚á𝑥𝑖𝑚𝑜.
Isso é repetido para os 𝑛 elementos .
Mantendo as propriedades de ℎ𝑒𝑎𝑝 𝑚á𝑥𝑖𝑚𝑎, pode-se retirar sucessivamente ele-
mentos da raiz do ℎ𝑒𝑎𝑝 na ordem desejada. Vale lembrar, ainda, que isso garante uma
ordenação em ordem crescente. Caso sejam mantidas as propriedades de ℎ𝑒𝑎𝑝 𝑚í𝑛𝑖𝑚𝑎,
obter-se-á a ordem decrescente.
O custo do HeapSort é devido ao procedimento de criar o ℎ𝑒𝑎𝑝 𝑚á𝑥𝑖𝑚𝑜: 𝑂(𝑙𝑜𝑔𝑛).
Como o procedimento é repetido para o 𝑛 elementos, tem-se que esta abordagem é domi-
nada assintoticamente por 𝑂(𝑛 * 𝑙𝑜𝑔𝑛).
Figura 13 – Número de movimentos de registro do algoritmo HeapSort
15. Na figura 13, nota-se que o número de movimento de registros é próxima para
qualquer ordem de vetor, porém alto. Na figura 14, nota-se que nos testes os vetores alea-
tórios tiveram um tempo que oscilou em relação aos demais. Ora todos ficaram próximos,
ora o aleatório levava um tempo superior, para o mesmo comprimento de vetor.
Figura 14 – Tempo de execução do algoritmo HeapSort
2.7 MergeSort
O MergeSort utiliza a abordagem dividir e conquistar. Nele um vetor é dividido
em duas partes, recursivamente. Em seguida, cada metade é ordenada e ambas são inter-
caladas formando o vetor ordenado.
O pseudocódigo está em 𝐴𝑙𝑔𝑜𝑟𝑖𝑡ℎ𝑚 6. Na linha 3, é encontrado o meio do vetor;
nas linhas 4 e 5 são realizadas as chamadas recursivas com as metades do vetor; na linha
6 ocorre a intercalação.
Algorithm 6 MergeSort
1: function MergeSort(V, e, d)
2: if 𝑒 < 𝑑 then
3: 𝑚𝑒𝑖𝑜 < − (𝑒 + 𝑑)/2
4: 𝑀 𝑒𝑟𝑔𝑒𝑆𝑜𝑟𝑡(𝑉, 𝑒, 𝑚𝑒𝑖𝑜)
5: 𝑀 𝑒𝑟𝑔𝑒𝑆𝑜𝑟𝑡(𝑉, 𝑚𝑒𝑖𝑜 + 1, 𝑑)
6: 𝑀 𝑒𝑟𝑔𝑒(𝑉, 𝑒, 𝑞, 𝑑)
7: end if
8: end function
Quanto à complexidade deste algortimo, a recursão é dada por
𝑇(𝑛) = 2𝑇(
𝑛
2
) + 𝑛 (2.7)
16. Pelo segundo caso:
𝑐1 * 𝑛𝑙𝑜𝑔2
2 ≤ 𝑛 ≤ 𝑐2 * 𝑛𝑙𝑜𝑔2
2 (2.8)
Tomando 𝑐1 = 𝑐2 = 1, têm-se uma afirmação verdadeira. Logo, a complexidade do
MergeSort é dado por
Θ(𝑛 * 𝑙𝑜𝑔𝑛) (2.9)
Nos testes realizados, notou-se que independentemente da ordenação do vetor são
realizados o mesmo número de movimentações de registros (figura 15). Quanto ao custo
necessário para realizar o procedimento, a partir de um 1000 elementos, foi necessário o
mesmo tempo para os vetores em ordem crescente e decrescente. Já o vetor em ordem
aleatória foi mais custoso a partir deste ponto (figura 16).
Figura 15 – Número de movimentos de registro do algoritmo MergeSort
Figura 16 – Tempo de execução do algoritmo MergeSort
17. 3 Discussões
Nas figuras 17, 18 e 19 são apresentadas comparações entre todas as técnicas tes-
tadas, em ordem crescente, descrescente e alatória, respectivamente. Nota-se facilmente
como alguns métodos mudam sua eficiência em relação aos demais dependendo da quan-
tidade de elementos e a ordem do vetor.
Para vetores em ordem crescente, destacam-se as implementações do algoritmo
QuickSort. Neste que é seu pior caso, ele necessita de um tempo muito superior aos
demais. Utilizando o elemento central como pivô, o tempo é reduzido quase que pela
metade, mesmo assim ficando acima dos outros.
Já para os vetores em ordem decrescente, os 𝑂(𝑛2
) ficam evidentes. As implemen-
tações do BubbleSort são as que exigem maior tempo. Destacam-se o ShellSort, MergeSort
e HeapSort, que mantêm-se próximos ao eixo x.
Finelmente, para entradas aleatórias, as implementações do BubbleSort dispara-
ram como as mais custosas. Já o QuickSort utilizando o elemento central como pivô, o
InsertionSort e o SelectionSort apresentaram desempenho parecido, destacando-se essa
implementação do QuickSort, que a partir de 20000 elementos passa a ser mais custoso
que os outros 2. Já o MergeSort, o HeapSort, o ShellSort e o QuickSort utilizando o
primeiro elemento como pivô permanecem juntos independe do comprimento do vetor.
Figura 17 – Comparação do tempo de execução de todos algoritmos testados: vetor cres-
cente
18. Figura 18 – Comparação do tempo de execução de todos algoritmos testados: vetor de-
crescente
Figura 19 – Comparação do tempo de execução de todos algoritmos testados: vetor alea-
tório
Tomando vetores independente de orientação, os melhores foram HeapSort, Shell-
Sort, MergeSort. Na figura 20, é possível observar uma comparação entre estes métodos.
Nota-se que para vetores ordenados diretamente e inversamente, o ShellSort é melhor, en-
quanto os demais são mais lentos, porém próximos entre si. Já para uma entrada alaetória,
o HeapSort é o mais rápido, apesar de desempenho semelhante aos demais.
19. Entre os algoritmos 𝑂(𝑛2
), o InsertionSort apresentou o melhor desempenho para
qualquer tamanho de vetor em ordem aleatória.
(a) Vetor crescente (b) Vetor decrescente
(c) Vetor aleatório
Figura 20 – Comparação entre os métodos HeapSort, ShellSort e MergeSort
Cabe ainda um comentário quanto ao QuickSort, que para vetores em ordem ale-
atória apresentou desempenho semelhante ao HeapSort. Sua abordagem recursiva exige
mais memória que outros algoritmos quando implementado. Assim, para a realização dos
testes, fez-se uso de uma estrutura de dados extra. Uma pilha permitiu a execução de
uma versão iterativa deste algoritmo.
20.
21. Referências
LANG, H. W. Shellsort. Consultado em 30/04/2015. 2010. Disponível em:
<http://www.iti.fh-flensburg.de/lang/algorithmen/sortieren/shell/shellen.htm>. 11
LEISERSON, C. et al. Algoritmos: teoria e prática. [S.l.]: CAMPUS - RJ, 2002. ISBN
9788535209266. 10
ZIVIANI, N. Projeto de algoritmos: com implementações em Java e C++. [S.l.]:
Thomson Learning, 2007. ISBN 9788522105250. 11, 12, 13