Você está na página 1de 36

Métodos Simples de

Ordenação
ECOM03A – Análise de Algoritmos
Prof. João Paulo R. R. Leite
joaopaulo@unifei.edu.br
Universidade Federal de Itajubá
A eficiência no manuseio de um conjunto de dados pode
ser aumentada se eles forem dispostos em uma estrutura
que utilize um critério de ordem ou classificação.

Ordem, segundo o dicionário é


“Uma disposição metódica; um arranjo de coisas segundo
certas relações; uma disposição conveniente dos meios para
se obterem os fins.”

O problema da ordenação é, geralmente, uma etapa importante


na solução de um problema maior, de caráter mais prático. Seria
praticamente impossível, por exemplo, encontrar o significado
de uma palavra em um dicionário desordenado. No entanto, a
ordenação dificilmente será um fim em si mesma.
A ordenação também é importante para problemas
de caráter puramente computacional:
Banco de dados: Acelerando a busca por dados;
Agrupamento em categorias: Mantendo dados
relacionados próximos, facilitando, por exemplo, o
processamento de dados em blocos; Compressão de
dados a partir do agrupamento.
Computação Gráfica: Ordem de renderização.
Algoritmos: Alguns algoritmos precisam que os dados
estejam em ordem para que funcionem corretamente.
Exemplos: Busca binária, determinação da mediana,
Caminho mínimo em grafo (Dijkstra), Árvore geradora
mínima (Kruskal), e muitos outros.
De acordo com a sua definição, uma ordenação deve ser realizada de
acordo com certas relações, ou seja, algum critério, que defina quem
precede quem na ordem.

O critério da ordenação varia de acordo com a aplicação e o tipo de


dados, e é definido pelo usuário:
– um conjunto de números pode ser organizado de acordo com
sua ordem de grandeza em ordem crescente ou decrescente;
– os nomes em uma agenda de telefones podem estar ordenados
alfabeticamente pelo primeiro ou último nome.

Normalmente, o estabelecimento do critério é trivial, como no caso


dos números e caracteres. No entanto, para tipos de dados mais
complexos (tipos definidos pelo usuário, p. ex.), o critério passa a ser
mais complicado de ser obtido ou, pelo menos, menos óbvio.
– Como você organizaria, por exemplo, uma coleção de fotos?
Qual o critério adequado? Nome, data, local, cores?
Uma vez escolhido o critério, o segundo passo é a criação de um
algoritmo que o traduza em uma sequência de passos (instruções) que
levem à ordenação de um conjunto (vetor) de dados.
Nos algoritmos que estudaremos, o resultado da ordenação será a
reorganização de um dado vetor de entrada.

Existem diversos métodos já desenvolvidos, e alguns deles podem ser


considerados mais eficientes que outros. Mas como decidir qual é o
melhor?
Precisamos conhecer (ou calcular) a complexidade no pior caso
para cada método, mostrando, para cada um deles, a evolução da
quantidade de operações básicas realizadas (tempo) com relação
ao aumento da quantidade de dados a serem ordenados (n).
O método de comparação entre algoritmos precisa ser
independente da máquina, da linguagem de programação
utilizada, do tipo de estrutura de dados, do estilo de
programação, do compilador.

Por isso, ao comparar os métodos, é preciso fazer uma análise


de cada algoritmo e quantificar algumas operações:
– número de comparações realizadas;
– número de vezes que os dados são trocados de lugar.

Para ordenar um conjunto de dados, eles precisam ser


comparados e movidos de acordo com a necessidade, e a
eficiência do algoritmo é dada pela quantidade de vezes que
essas operações básicas são repetidas, em função do
tamanho da entrada (n).
A operação básica
Grande parte dos algoritmos de ordenação trabalha de forma
iterativa. Cada iteração aproxima o vetor a ser ordenado de seu
estado final.

Ao conjunto de operações realizados a cada iteração –


comparação + movimentação – daremos o nome de passo. Essa
será nossa operação básica.

A quantidade de operações básicas executadas pelo algoritmo


pode variar para vetores com o mesmo tamanho n, de acordo
com a ordem em que se encontram os dados do vetor no início
da operação. Estamos mais interessados no pior caso, ou seja, o
limite máximo de operações para um vetor de tamanho n.
Desempenho vs. Simplicidade
Os métodos de ordenação podem ser classificados em:
– Métodos simples: São adequados para pequenas quantidades
de dados e requerem na ordem de O(n2) operações básicas.
Utilizados quando o foco está na simplicidade. São de fácil compreensão
e demonstram claramente os princípios de ordenação por comparação.

– Métodos eficientes: São adequados para grandes quantidades


de dados e requerem O(n logn) operações básicas.
Realizam menos comparações e trocas, mas suas implementações são
mais complicadas e difíceis de compreender.
Os algoritmos de ordenação podem também ser classificados de
acordo com o princípio que utilizam para realizar a ordenação.
As famílias de métodos são:

Ordenação por troca (Bubble Sort, Quick Sort)


Ordenação por Inserção (Insertion Sort, Shell Sort)
Ordenação por Seleção (Selection Sort, Heap Sort)
Ordenação por Intercalação (Merge Sort)
Ordenação por Distribuição (Bucket Sort)

Falaremos mais sobre eles durante o curso.


Os métodos baseados em trocas realizam a
ordenação comparando pares de elementos de um
vetor e trocando-os de posição caso estejam foram
de ordem no par (comparação + troca).

Bubble Sort
– Simples entendimento e fácil escrita.
– É o mais antigo método de ordenação e um dos
menos eficientes, pois exige a realização de muitas
comparações e trocas.
– Faz com que os maiores valores sejam “empurrados”
para o final do vetor através de trocas sucessivas.
• Como uma bolha (bubble), que percorre o comprimento do
vetor até encontrar seu lugar definitivo.
Bubble Sort
Algoritmo realiza varreduras no vetor a cada iteração, trocando
pares adjacentes de elementos sempre que um elemento for
menor que seu anterior.

Após a primeira iteração, o maior elemento estará posicionado


na última posição do vetor: sua posição definitiva. Da mesma
forma, após cada uma das iterações seguintes, o maior
elemento entre os restantes encontrará seu lugar no vetor.
Portanto, após a i-ésima iteração, os i maiores elementos já
estão ordenados e em suas posições definitivas.

A cada iteração, o maior elemento é inserido na posição


ordenada e removido da posição desordenada.
Função em C++ para ordenação de um vetor de inteiros utilizando Bubble Sort.
Repare que não é preciso ir
até o final do vetor todas as
vezes (n-1-i). A cada
iteração (i), o maior dos
elementos fica em sua
posição definitiva e
“encurta” a parte do vetor
que ainda precisa ser
ordenada.

Função em C++ para ordenação de um vetor de inteiros utilizando Bubble Sort.


No exemplo, ordenamos o vetor {7,2,8,5,4}:

Os elementos em azul e vermelho estão sendo comparados (azul é o j e vermelho o j+1).


As setas indicam se eles são trocados ou não.
Elementos verdes são os que já chegaram em suas posições definitivas e compõem a
parte do vetor já ordenada.
Bubble Sort
As etapas do algoritmo são:
– Quando i = 0, os elementos vizinhos são comparados:
• (v[0],v[1]), (v[1],v[2]), (v[2],v[3])...
• São realizadas (tam - 1) comparações;
• Para cada par, são trocados os valores se (v[j+1] < v[j]);
• Ao final, o maior elemento da lista está na posição v[tam - 1].
– Quando i = 1, são realizadas as mesmas comparações e
trocas, terminando com o elemento de segundo maior
valor na posição v[tam - 2];
– o processo termina com i = tam - 1, em que o menor
elemento será armazenado na posição v[0].
Considerando uma entrada de tamanho n:
– O algoritmo fará, na primeira varredura, n-1 comparações,
depois n-2, depois n-3, e assim por diante. Na última, fará
apenas 1 comparação.
– Para o número total de comparações, temos a soma (n-1)
+ (n-2) + (n-3) + ... + 1, que é a soma de uma progressão
aritmética (PA) com (n-1) termos, em que o primeiro é
(n-1), e o último é 1.

𝑎1 +𝑎𝑁 ∗𝑁 ( 𝑛−1)+1 ∗(𝑛−1) 𝑛∗(𝑛−1) 𝑛2 −𝑛


𝑆= = = =
2 2 2 2

Como o algoritmo realiza n2/2 comparações (e possíveis trocas)


para uma entrada de tamanho n, podemos afirmar que sua
complexidade é O(n2).
Ordenação por Seleção
Buscam, a cada iteração, o maior (ou menor) elemento
do vetor e o colocam na sua posição definitiva.

Selection Sort
Talvez o mais simples e intuitivo dos algoritmos de
ordenação, o Selection sort se apóia em sucessivas iterações
que trocam o elemento de menor valor com o elemento do
vetor que se encontra na sua posição definitiva.
1. Pesquisa sequencial seleciona o menor elemento do vetor;
2. Coloca-o na posição que lhe corresponde, trocando-o de
posição com o elemento que ali se encontra.
Os passos para sua execução são os seguintes:

1. Identificar o menor elemento do vetor através de uma


pesquisa sequencial;
2. Trocar com o elemento da primeira posição do vetor;
3. Reexamina os elementos restantes no vetor para achar o
segundo menor elemento;
4. O processo se repete até que todos os elementos do
vetor sejam ordenados.
Função em C++ para ordenação de um vetor de inteiros utilizando Selection Sort.
Não é necessário repetir n
vezes, uma vez que, ao
colocar o penúltimo
elemento em seu lugar, o
último também encontra
automaticamente seu lugar.

Função em C++ para ordenação de um vetor de inteiros utilizando Selection Sort.


Repare no exemplo, com o
vetor inicialmente com
{7, 2, 8, 5, 4}

O algoritmo divide o vetor


em uma parte já ordenada
(verde) e outra ainda a ser
ordenada. Quando o menor
elemento é encontrado, ele
é trocado com o elemento
da posição i (em vermelho),
que é a primeira posição do
vetor com elementos ainda
desordenados.
Considerando uma entrada de tamanho n:
– A primeira iteração compara o 1º elemento com os
n - 1 demais: n - 1 comparações
– A segunda iteração compara o 2º elemento com os
n – 2 demais: n – 2 comparações
– A (n-1)º iteração compara o (n-1)º elemento com o
último: 1 comparação
– Total de comparações: (n-1) + (n-2) + (n-3) + ... + 1
– Calculando a soma da PA:
(n-1 + 1)*(n-1) / 2 = (n2 – n)/2

Portanto, sua complexidade é também O(n2).


No entanto, o algoritmo realiza muito menos trocas do
que o Bubble Sort, o que justifica seu desempenho
levemente melhor.
As duas curvas possuem comportamento quadrático (parábolas), mas a curva do
Selection Sort cresce mais lentamente, devido ao menor número de operações
necessárias para ordenar um mesmo vetor de tamanho n.
Ordenação por Inserção
Insertion Sort

Este método de ordenação baseia-se na inserção de


elementos em uma fila ordenada, da mesma maneira que
muitas pessoas fazem para ordenar cartas de baralho
durante um jogo.

Iniciamos com a mão esquerda vazia e as cartas


viradas para baixo, na mesa. Em seguida, retiramos
uma carta de cada vez da mesa e a inserimos na
posição correta na mão esquerda. Para encontrar a
posição correta, a comparamos com cada uma das
cartas que já estão na mão, da direita para a
esquerda.
Traduzindo para o universo da programação, o
algoritmo tomará cada elemento de um vetor não
ordenado e o inserirá exatamente no lugar devido
em outro vetor, sempre ordenado.

Diferente dos métodos anteriores, Bubble e


Selection Sort, neste caso não há troca de
elementos. Haverá sim um deslocamento de dados,
cada vez que for necessário inserir um elemento no
meio do vetor ordenado.
Mas serão necessários dois vetores? Não!

A ordenação por inserção é caracterizada pelo princípio


no qual os n dados a serem ordenados são divididos em
dois segmentos dentro de um mesmo vetor:
1. Um segmento já ordenado;
2. Outro segmento a ser ordenado;

No início, o primeiro segmento é formado por um único


elemento (que, portanto, já pode ser considerado
ordenado), enquanto o segundo segmento é formado
pelos (n – 1) elementos restantes.
Veja como funciona, antes que formalizemos o algoritmo:

Em um mesmo vetor, temos a parte sombreada, que contém os elementos já


ordenados, o elemento atual, pintado de preto, e os elementos ainda
desordenados, deixados em branco. A cada iteração, tomamos o primeiro
elemento do segmento desordenado e o inserimos na posição adequada no
segmento ordenado do vetor, movimentando alguns elementos para a direita e
mantendo a ordenação.
A partir da configuração inicial, desenrolam-se (n – 1) iterações.
Em cada uma delas um elemento do segmento não-ordenado é
transferido para o segmento ordenado, sendo inserido em sua
posição correta em relação àqueles que já foram transferidos.

Repare que, quando se acha a posição de um elemento no


segmento ordenado, todos os elementos à sua direita precisam
ser movidos uma posição para a direita, para abrir um espaço. É
isso que o tornará ineficiente quando for necessário ordenar
conjuntos grandes de elementos.
Função em C++ para ordenação de um vetor de inteiros utilizando Insertion Sort.
Iteração é parada quando o
elemento é menor ou igual
ao elemento a ser inserido,
ou quando j = 0. Neste
último caso, o elemento
seria inserido na primeira
posição do vetor ordenado.

Função em C++ para ordenação de um vetor de inteiros utilizando Insertion Sort.


Ordenação por Inserção
A cada comparação realizada entre o elemento a ser
inserido e os que já se encontram ordenados, podemos
obter um dos dois resultados:

1) O elemento a ser inserido é menor do que aquele


com que se está comparando: Nesse caso, este último é
movido uma posição para a direita, deixando vaga a
posição que ocupava.

2) O elemento a ser inserido é maior ou igual àquele


com que se está comparando: Nesse caso, fazemos a
inserção do elemento na posição vaga, que corresponde
à sua posição correta no segmento já ordenado.
No exemplo abaixo, o vetor {25, 60, 4, 32, 12, 45} é
ordenado utilizando o algoritmo do Insertion Sort.

25 60 4 32 12 45

25 60 4 32 12 45

4 25 60 32 12 45

4 25 32 60 12 45

4 12 25 32 60 45

4 12 25 32 45 60
A operação dominante deste tipo de ordenação é a
comparação e, principalmente, a movimentação de
elementos, visando “abrir espaço” para a inserção de um
elemento em sua posição ordenada.

O laço externo será repetido n – 1 vezes, partindo do


segundo elemento até o último. O laço interno, por sua vez,
irá variar, começando sempre em j = i – 1 e terminando, no
máximo, em j = 0.

Para o melhor caso, quando o vetor está previamente ordenado, todos os


testes (vetor[j] > aux) retornarão false, e não será necessário mudar
ninguém de lugar. Resulta em uma única atribuição para cada iteração do
laço externo, em vetor[i] = aux, totalizando (n – 1) operações: O(n).
O pior caso para este algoritmo é quando o vetor está em ordem
inversa, e cada inserção precisa mover em uma posição todos os
itens previamente ordenados (vetor[j] > aux == true em todas
iterações). O laço interno sempre fará i – 1 iterações, resultando
também em uma complexidade quadrática - O(n2) - de acordo
com o somatório abaixo:


n −1
i =1
i −1 Que gera a sequência:
0, 1, 2, 3, ..., n - 2

Calculando a soma da P.A., temos que:


Σ = (0 + n-2) (n-1)/2 = (n2 – 3n – 2)/2
Σ = O(n2)
Novamente, percebemos o comportamento quadrático (parábolas) das três curvas,
uma vez que os algoritmos têm complexidade O(n2). No entanto, Insertion Sort
possui uma curva menos íngreme e é, geralmente, mais eficiente que os demais.
Algumas observações:

Existem vários algoritmos para ordenação de dados, que variam


muito com relação a implementação e eficiência.

Um método de implementação simples de ser escrito pode ser apenas um


pouco menos eficiente do que um mais elaborado. É preciso analisar cada
caso e escolher cuidadosamente qual algoritmo utilizar em cada situação.

O uso de um algoritmo mais elaborado para a ordenação de um


conjunto pequeno de dados pode ser uma escolha ruim.
Um método mais simples pode ser útil, por ser implementado mais
rapidamente e, às vezes, até mesmo por apresentar melhor desempenho
em conjuntos de dados pequenos.

Você também pode gostar