Você está na página 1de 7

Algoritmos de Ordenação

Carlos Torrão João Martins Maria Couceiro


55381 55385 53964

Resumo: Neste artigo são apresentados vários algoritmos de ordenação: bubble sort,
mergesort, quicksort, hyperquicksort, rank sort, counting sort e radix sort. É feita uma descrição
do seu funcionamento em série e em paralelo, fazendo-se referência a vantagens e
desvantagens e problemas resultantes do seu uso. Concluímos que, na grande maioria dos
casos, a implementação paralela dos algoritmos produz melhores resultados a nível de
complexidade temporal que em série.

1 Introdução x2, onde na posição x2 ficará o maior dos


A ordenação é um dos aspectos números que se encontra nas posições x1 e
fundamentais das ciências computacionais. x2. Este processo é repetido de forma
Torna-se, então, importante reduzir ao análoga até chegar à comparação entre as
máximo a complexidade temporal dos posições xn-1 e xn. Este processo anterior de
algoritmos que lidam com este problema. comparações e trocas será considerado
As melhores ordenações em série uma fase do algoritmo, sendo o algoritmo
normalmente demoram O(n log n), tempo bubble sort constituído (na pior das
que tende a agravar com o aumento do hipóteses) por n-1 fases semelhantes à
número de elementos. Deste modo, foram anterior.
desenvolvidas versões para funcionamento
em paralelo destes algoritmos, cujo 2.2 Paralelo
objectivo é diminuir consideravelmente o
tempo de execução dos mesmos. Após esta breve descrição do algoritmo
Neste texto vamos abordar vários sequencial, a questão que se impõe é a
algoritmos de ordenação. Relativamente seguinte: será que é possível uma versão
aos que realizam operações de paralela eficiente deste algoritmo?
comparação e troca, descrevemos o A resposta a esta pergunta é afirmativa.
bubble sort, quicksort e hyperquicksort, Através da técnica de pipelining é possível
mergesort, odd-even mergesort e bitonic melhorar o desempenho do algoritmo, em
mergesort. Também falamos sobre o rank comparação com a versão sequencial. Isto
sort e o counting sort, que não recorrem a deve-se ao facto de ser possível serem
este tipo de operações. No que diz respeito executadas várias fases em simultâneo na
a algoritmos que obtêm uma performance versão com pipelining.
muito superior quando paralelizados, É possível tal método, pois como cada fase
vamos descrever o radix sort. só faz uma comparação por cada par de
posições, quando a primeira fase se
encontra na comparação entre as posições
2 Bubble Sort x2 e x3, a segunda fase já pode (sem
problema algum) fazer a comparação entre
2.1 Sequencial as posições x0 e x1. A terceira fase, terá
início quando a segunda fase estiver a
O algoritmo de ordenação bubble sort usa comparar as posições x2 e x3, e a primeira
uma estratégia de “comparação e troca”, fase, por sua vez, se encontrar na
que é aplicada em várias iterações sobre comparação das posições x4 e x5. (Ver
os dados a ser ordenados. Passo a explicar Figura 1)
os passos do algoritmo sequencial. Existe ainda uma variação do algoritmo
Partindo do principio que se deseja ordenar bubble sort, cujo nome é odd-even
(de forma crescente) um vector de números (transposition) sort, que consiste em duas
nas seguintes posições: x0, x1, …, xn-1, xn. O fases, a fase even (par) e a fase odd
algoritmo começa por comparar o número (ímpar). Na fase even (par), comparam-se
que se encontra na posição x0 com o da (e se necessário trocam-se) as posições
posição x1, e troca (quando necessário) os pares com a posição seguinte a cada uma
números de maneira a ficar o maior dos delas. Na fase odd (ímpar), a ideia é
dois na posição x1. De seguida, repete o análoga à anterior, mas para as posições
mesmo procedimento para as posições x1 e ímpares. (Ver Figura 2)
Figura 1 – Bubble sort com pipelining

Figura 2 – Odd-Even transposition sort em paralelo

Como é de prever, esta solução em Inicialmente, durante todo o processo da


sequencial não traz benefícios nenhuns em divisão da lista, não é feita qualquer
relação ao bubble sort (sequencial), mas o computação sobre os elementos e sua
mesmo já não se pode afirmar da versão ordem na lista, e a primeira lista de quatro
paralela desta, que é bastante mais elementos é apenas separada em metade,
eficiente. Esta solução paralela tem ficando duas listas, sendo de seguida o
complexidade O(n), enquanto que o método repetido, ficando desta vez todos
algoritmo bubble sort na sua versão os elementos separados.
sequencial, apresenta uma complexidade É na fase da junção (merge) que o
de O(n2). algoritmo ordena todos os elementos da
forma pretendida. Neste caso, em que
3 Merge Sort queremos ordenar de forma crescente, o
algoritmo pega nos primeiros dois
elementos e junta-os numa lista, ficando o
3.1 Sequencial menor valor na primeira posição e o maior
na segunda posição. Para os dois últimos
O algoritmo merge sort consiste em ir elementos que sobram, utiliza o mesmo
separando em metades uma lista de método. Este processo é agora repetido
elementos, até ficar com todos os para as duas listas criadas anteriormente,
elementos separados. Esta técnica tem o juntando os elementos de cada uma
objectivo de “dividir-para-conquistar”. Após dessas duas listas numa única lista, ficando
estes elementos estarem separados, o essa lista ordenada de forma crescente.
algoritmo procede à sua junção (merge),
retornando no final uma única lista com
todos os elementos ordenados.
3.2 Paralelo
Vamos considerar a título de exemplo, uma
lista de quatro elementos sem ordem A versão simples paralela, baseia-se em
alguma, e que se querer ordenar de forma atribuir a cada processador uma lista de
crescente todos os seus elementos.
Figura 3 – Merge sort sequencial

A lista final ordenada, E, constituídas pelos


elementos: e1, e2, ..., e2n, é obtida através
das seguintes regras (1 ≤ i ≤ n-1):
• e2i = min{ ci+1, di }
• e2i+1 = max{ ci+1, di }
Basicamente, esta lista E é obtida pela
comparação das posições pares e ímpares
das listas C e D. O primeiro elemento da
lista E, e1, é dado por c1, e o mesmo
acontece com o ultimo elemento, e2n, que é
dado por dn. Isto deve-se ao facto, de em c1
estar o elemento menor das listas C e D, e
de em dn estar o maior elemento das listas
C e D.

3.4 Análise das versões


O merge sort sequencial tem uma
complexidade temporal O(n.logn). O que é
bastante bom para um algoritmo de
ordenação sequencial.
O odd-even merge sort em comparação
com a versão sequencial é bastante
superior, pois apresenta uma complexidade
temporal O(log2n) com n processadores.
elementos, na parte inicial da divisão da
lista a ordenar.
Esta versão tem o problema de em 4 Quicksort
algumas fases do algoritmo ser necessária
bastante comunicação entre O algoritmo Quicksort, inventado por
processadores. Mas por outro lado, esta C.A.R. Hoare, é um algoritmo recursivo que
versão torna-se bastante leve a nível tem como base a comparação de valores
computacional para cada um dos para ordenar uma lista desordenada. Mais
processadores envolvidos, pois cada especificamente, quando é passada uma
processador é responsável por uma lista lista de números, o algoritmo selecciona
pequena. um número dessa lista, tornando-o pivot.
Após isso, a lista é partida em duas sub-
listas: uma contendo números inferiores ao
3.3 Odd-Even merge sort pivot, e outra com os números superiores.
Chama-se depois a si mesma
recursivamente para ordenar as duas sub-
Este algoritmo tem como objectivo ordenar
listas. A função retorna a concatenação da
duas listas ordenadas, numa só lista
primeira lista, pivot e segunda lista.
ordenada. Isto apresenta potencial, para
ser invocado de maneira recursiva. Na
parte de junção das listas no algoritmo
merge sort. 4.1 Quicksort Paralelo
Considerando a lista A = a1, a2, ..., an e a
lista B = b1, b2, ..., bn. O algoritmo começa Existem algumas vantagens na utilização
por criar uma lista C, constituída pelas deste algoritmo, no que toca a
posições (índices) ímpares de ambas as paralelização de algoritmos. Primeiro,
listas, ou seja, C = a1, a3, ..., an-1, b1, b3, ..., porque é considerado dos algoritmos mais
bn-1. Existe o processo análogo para as rápidos no que trata a ordenação através
posições (índices) pares, sendo criado a de comparação de valores. Em segundo
lista D = a2, a4, ..., an, b2, b4, ..., bn. Para lugar, porque como é um algoritmo que se
maior facilidade de percepção futura, os chama a si próprio recursivamente, tem
elementos da lista C estarão referenciados concorrência natural, sendo que essas
da seguinte forma: c1, c2, ..., cn, e o mesmo chamadas podem executar-se
para os elementos da lista D. independentemente.
Em relação ao desenvolvimento deste dos valores, para garantir maior divisão de
algoritmo, inicialmente os valores trabalho pelos processos.
encontram-se distribuídos ao longo dos No entanto, poderia ser feito um melhor
processos. Escolhemos então um pivot de balanceamento do tamanho da lista pelos
cada um dos processos e divulgamo-lo. processos, se em vez de escolher um
Após isso, cada processo divide os seus número arbitrário da lista para ser pivot,
números em duas listas: aqueles que são escolhêssemos um valor mais perto do
menores ou iguais ao pivot, e aqueles que valor mediano da lista ordenada. Este
são maiores do que o pivot (que ponto é a motivação por trás do próximo
chamaremos aqui de “lista menor” e “lista algoritmo em paralelo: hyperquicksort.
maior” respectivamente). Cada processo na
parte de cima da lista de processos envia a 4.2 Hyperquicksort
sua “lista menor” para um processo
concorrente na parte inferior da lista de Este algoritmo, inventado por Wagar,
processos e recebe uma “lista maior” em começa onde o quicksort acaba.
retorno. Neste momento, os processos na Considerando que os valores continuam a
parte superior da lista de processos têm mover-se de processos para outros
valores maiores do que o pivot, e os processos, teremos então um pivot para
processos na parte inferior da lista de dividir os números em dois grupos: a parte
processos têm valores menores ou iguais superior e a parte inferior. Devido à lista de
ao pivot. A partir daqui os processos elementos de cada processo estar
dividem-se em dois grupos e o algoritmo é ordenada, o processo responsável por
chamado de novo. Em cada grupo de fornecer o pivot pode usar o mediano da
processos é distribuído um pivot. Os sua lista como pivot. Este pivot é então,
processos dividem a sua lista e trocam mais próximo do verdadeiro mediano da
valores com processos concorrentes. Após inteira lista desordenada do que
log p recursões, cada processo tem uma escolhendo um valor arbitrário.
lista desordenada de valores, que são Os próximos três passos do hyperquicksort
diferentes dos valores possuídos pelos são os mesmos do que o quicksort
outros processos. Cada processo pode paralelo.
agora ordenar a lista que controla usando Após a execução desses passos, cada
quicksort sequencial. processo tem uma sub-lista ordenada
própria e uma sub-lista ordenada que
Em relação à complexidade, se tivermos p recebe de um outro processo. Esse
processadores, podemos dividir a lista de n processo vai então juntar as duas listas,
elementos em p sub-listas em O(n). Após para que todos os elementos que controla
isso, a sua ordenação será em estejam ordenados. É importante que os
O((n/p)log(n/p)). processos terminem esta fase com listas
ordenadas, porque quando o algoritmo se
Uma das vantagens deste algoritmo em chamar de novo, dois processos precisarão
relação a outros algoritmos em paralelo é o de escolher o elemento mediano das suas
facto de não ser necessária a sua listas como pivot.
sincronização. Um processo é criado por O hyperquicksort assume que o número de
cada sub-lista gerada, sendo que esse processos é um expoente de 2. Se
processo só trabalha com essa lista, não imaginarmos a lista de processos como um
comunicando com os outros processos. hypercube, poderemos modelar as
O tempo de execução do algoritmo começa comunicações, de modo a que estas sejam
quando o primeiro processo começa a sua sempre feitas entre pares de processos
execução, e termina quando o ultimo adjacentes.
processo termina a sua execução. É por
isto que é importante assegurar que todos
os processos tenham a mesma carga de
trabalho, para que terminem todos por volta
do mesmo tempo. Neste algoritmo, a carga
de trabalho é relacionada com o número de
elementos controlados pelos processos.
Este algoritmo tem como ponto negativo Figura 4: Neste exemplo há 8 processos,
que neste caso faz um mau trabalho a fazendo log p = 3 vezes separar-e-juntar
balancear o tamanho das listas. Tem que
se conseguir um pivot perto da mediana
Há a referir também a eficiência deste um vector com n números, a[0] … a[n-1],
algoritmo, onde para p processadores, a[0] começa por ser comparado a todos os
serão ordenados n elementos, onde n >> p. outros elementos, sendo a soma total dos
No início do algoritmo, cada processo não menores que ele guardados em rank. Este
tem mais do que n/p valores. A esperada valor é o seu índice no vector ordenado b,
complexidade temporal do passo inicial do onde é colocado. A mesma operação é
quicksort é Θ[(n/p)log(n/p)]. Assumindo que repetida para todos os elementos do vector
cada processo guarda n/2p valores e a.
transmite n/2p valores em cada passo de Segue-se uma possível implementação
separar-e-juntar, o numero esperado de para este algoritmo, tendo em conta a
comparações necessárias para juntar as existência de valores duplicados:
duas listas numa única lista ordenada é
n/p. O numero de comparações feitas for(i=0; i < n; i++){
durante o algoritmo todo é Θ[(n/p) rank = 0;
for(j=0; j < n; j++){
(logn+logp)], para log p iterações.
if(a[i] < a[j] ||
Assumindo que cada processo passa (a[i] == a[j] && j < i))
metade dos seus valores em cada iteração, rank++;
o tempo necessário para enviar e receber b[rank] = a[i];
n/2p valores ordenados para e de outro }
processo, é Θ(n/p).
A complexidade do tempo sequencial do A nível de complexidade, este algoritmo é
quicksort é n.log n. O overhead da pouco eficiente quando executado em
comunicação do hyperquicksort é p vezes a modo sequencial, uma vez que faz n-1
complexidade da comunicação, ou comparações n vezes, ficando o algoritmo
Θ(n.logp). com n(n-1) iterações, o que resulta numa
complexidade temporal de O(n2).
Para finalizar, o hyperquicksort tem duas Como é necessário existir um acesso
desvantagens que limitam as suas partilhado ao vector, este algoritmo é mais
vantagens. Primeiro, o número esperado indicado para sistemas de memória
de vezes que um valor é passada de um partilhada. O cálculo da posição final de
processo para outro é (logp)/2. Este cada número no vector ordenado é dividido
overhead da comunicação limita a por n processadores, que a determinam em
escalabilidade do algoritmo em paralelo. paralelo, ficando assim o algoritmo com
Poderíamos reduzir este overhead se uma complexidade temporal de O(n). No
conseguíssemos encontrar uma maneira entanto, é possível implementá-lo em
de enviar as chaves directamente para os sistemas de memória distribuída. Neste
seus destinos finais. caso, um processo central (master) distribui
Em segundo lugar, a maneira como os pelos outros processadores (slaves) n/p
pivots são escolhidos podem levar a certos elementos do vector, sendo p o número
problemas no balanceamento da carga de total de processadores. Cada processador
trabalho entre processos. Se fica responsável por calcular o rank de
conseguíssemos ter exemplares de todos cada um dos seus n/p elementos. No final,
os processos, poderíamos ter uma melhor os cálculos são reportados ao processo
divisão das listas de elementos pelos central, que constrói o vector ordenado.
processos. Esta distribuição só compensa quando n é
suficientemente grande, devido ao custo
5 Rank Sort das operações de comunicação. Uma outra
limitação do uso deste algoritmo neste tipo
O rank sort, também conhecido como de sistemas prende-se com a utilização de
enumeration sort, é um exemplo de um uma grande quantidade de memória, uma
algoritmo de ordenação estável, isto é, os vez que cada processador precisa de uma
registos aparecem na sequência ordenada cópia do vector desordenado e de um
pela mesma ordem que estão na sequência vector do tamanho deste último para poder
original, mantendo assim a sua posição posicionar os elementos repetidos
relativa. ordenados na posição correspondente.
O seu modo de funcionamento consiste em Utilizando n2 processadores e uma
determinar a quantidade de números estrutura em árvore, a complexidade
menores que um determinado número, temporal diminui para O(log n). Cada
sendo assim calculada a sua posição (rank) processador executa a comparação de um
na lista ordenada. Tomando como exemplo elemento com outro, somando-se os
resultados de cada comparação cada vez c[i] = 0;
que se muda de nível na árvore, como se for(i = 1; i <= m; i++)
c[a[i]]++;
pode ver na figura 5. for(i = 2; i <= m; i++)
c[i] = c[i] + c[i-1];
for(i = 1; i <= n; i++){
b[c[a[i]]] = a[i];
c[a[i]]--;
}

Este código tem complexidade temporal de


O(n + m), mas caso m = O(n), O algoritmo
corre em O(n).
Na paralelização do counting sort, usa-se a
versão paralela do prefix sum, que requer
um tempo de O(log n) para n-1
processadores. Para atingir este tempo,
partimos recursivamente o vector c ao meio
Figura 5: Computação do cálculo do rank e somamos as duas metades também
em paralelo. recursivamente, estando associada a esta
computação uma árvore binária completa,
Como a utilização de n2 processadores (e em que cada nó interno contém a soma
até mesmo de n processadores, quando n das suas folhas descendentes. A
é muito grande) se torna proibitiva, é ordenação final pode ser feita em O(n/p),
comum agrupar números e distribuí-los caso tenhamos n < p processadores,
pelos processadores disponíveis. Partindo dividindo-se o vector em p sub-vectores,
do princípio que temos m grupos, seriam cada um com n/p elementos, ou então
apenas necessários n/m processadores O(1), caso tenhamos n processadores, em
para calcular a posição no vector ordenado que cada um executa o corpo do ciclo.
em cada grupo de números (sem que este
cálculo seja paralelizado). No entanto, o 7 Radix Sort
número de operações em cada
processador aumentaria m vezes. O radix sort é um dos algoritmos de
ordenação mais utilizados, devido à sua
6 Counting Sort rapidez e simplicidade de implementação.
Parte do pressuposto que os dígitos
Outro exemplo de um algoritmo estável é o representam valores e que a sua posição,
counting sort. Sendo uma optimização do que está ordenada do menos significativo
rank sort para o caso em que todos os para o mais significativo, indica o seu peso
números a ordenar são inteiros, reduz a relativo. Imaginando que um número tem b
complexidade temporal da execução em bits (ou dígitos, caso se esteja a falar de
série de O(n2) para O(n). notação decimal), na sua versão mais
Tal como no rank sort, a ideia fundamental simples, o algoritmo ordena os números em
é determinar, para cada número i, a b passagens, sendo que em cada n-ésima
quantidade de elementos menores que ele. passagem se ordena pelo n-ésimo bit
Os vectores a e b, ambos com dimensão n, menos significativo. Este número de
contêm, respectivamente, os números passagens pode ser reduzido de b para b/r
desordenados e ordenados. Existe também se se considerar blocos de r dígitos em vez
um vector c, de dimensão m, que contém de um único bit. A complexidade temporal
um elemento para cada valor possível de a, vai depender do algoritmo de ordenação
por exemplo, de 1 até m. usado, que tem de ser estável, de modo a
Para calcular a quantidade de elementos preservar a ordem relativa dos elementos.
inferior a um dado número, utiliza-se o O counting sort é um algoritmo de
método denominado prefix sum, que ordenação muito usado pelo radix sort, cuja
consiste na soma parcial de todas as complexidade temporal em modo
posições de um dado vector. sequencial é O(n + m), sendo que n
Segue-se uma possível implementação corresponde ao número de elementos
para este algoritmo, tendo em conta a inteiros, estando cada um deles entre, por
existência de valores duplicados: exemplo, 1 e m. Com números de
dimensão r, ficamos com um intervalo de 1
for(i = 1; i <= m; i++) a 2r-1 e b/r fases do algoritmo. Assim,
obtém-se uma complexidade temporal em
modo sequencial de O( b/r ( n + 2r )), mas 9 Referências
se b e r forem constantes, a complexidade
baixa para O(n).
Podemos paralelizar o radix sort usando 1. Michael Quinn, Parallel Programming,
um algoritmo de ordenação paralelo, como McGrawHill, 2003
o counting sort descrito na secção anterior,
atingindo-se assim um tempo de O(log n) 2. Barry Wilkinson and Michael Allen,
com n processadores e b e r constantes. Parallel Programming: Techniques and
No entanto, em sistemas de memória Applications Using Networked Workstations
distribuída, para grandes quantidades de and Parallel Computers, Prentice Hall, 2005
dados, o tempo gasto em comunicação é
demasiado elevado e o balanceamento da 3. Philippas Tsigas and Yi Zhang, A
carga é muito irregular, pois não se sabe à Simple, Fast Parallel Implementation of
partida o formato de todos os elementos a Quicksort and its Performance Evaluation
ordenar. Consequentemente, foram on SUN Enterprise 10000
propostas várias formas de dividir a carga
entre os vários processadores, com o 4. N. M. Amato, R. Iyer, S. Sundaresan, Y.
objectivo de diminuir estes problemas. Uma Wu, A Comparison of Parallel Sorting
dessas propostas é denominada de “Load Algorithms on Different Architectures, 1998
Balanced Parallel Radix Sort” e consiste
em atribuir a cada processador 5. S. Lee, M. Jeon, D. Kim, A. Sohn,
exactamente o mesmo número de Partitioned Parallel Radix Sort, 2002
elementos a ordenar, eliminando assim o
problema da distribuição de carga, mas 6. A. Sohn, Y. Kodama, Load Balanced
gastando ainda muito tempo a redistribuir Parallel Radix Sort, 1998
os elementos pelos processadores. O 7.
“Partitioned Parallel Radix Sort” veio http://beowulf.csail.mit.edu/18.337/book/Lec
solucionar o problema de ture_03-Parallel_Prefix.pdf, 2006
comunicação desta proposta, uma vez
que elimina a redistribuição global de
elementos, ao ordená-los,
inicialmente, pelos seus g bits mais
significativos e, depois, distribuir
grupos destes elementos pelos
processadores disponíveis, onde são
ordenados pelos seus b – g bits menos
significativos, reduzindo assim a troca
de mensagens entre processadores.

8 Conclusão
Durante a elaboração deste artigo,
pudemos concluir que a execução em
paralelo dos algoritmos estudados aumenta
consideravelmente a eficiência temporal da
ordenação de elementos. Há que referir
que existem outros que não foram aqui
abordados, pois não os considerámos tão
relevantes quanto os aqui expostos.
Foi ainda possível observar que,
relativamente à investigação nesta área, há
uma grande preocupação em optimizar
certos aspectos dos algoritmos existentes
de modo a reduzir os overheads
introduzidos pela comunicação e
distribuição de carga pouco equilibrada,
que continuam a limitar o seu
funcionamento.

Você também pode gostar