Ordenação
Vamos entender alguns dos algoritmos mais comuns de ordenação e como usá-los na prática.
Last updated
Vamos entender alguns dos algoritmos mais comuns de ordenação e como usá-los na prática.
Last updated
Todas as animações desta página são temporárias e vão mudar a medida que eu tiver tempo para tal.
Ordenar elementos em um vetor é um problema bastante comum com inúmeras aplicações na nossa vida. Até antes dos computadores existirem, as pessoas já desempenhavam métodos para ordenar itens com eficiência.
Alguns dos métodos de ordenação apresentados abaixo vão te parecer muito intuitivos enquanto outros podem ser mais difíceis de entender, mas a ideia é apenas entender como eles funcionam para compreender como tais métodos evoluíram ao longo do tempo e também como existem uma diversidade incrível de formas de resolver o mesmo problema, cada uma com suas vantagens e desvantagens.
Vamos começar com os métodos mais simples e partir para métodos mais sofisticados, cada um com sua complexidade de tempo e espaço e uma breve explicação e implementação.
Em todas as funções apresentadas estamos usando os métodos mais gerais possíveis, podendo ser aplicados a qualquer tipo de estrutura de dados. Para isso, usamos templates na linguagem C++ e também uma função comp
facilmente customizável para qualquer necessidade de ordenação. Falamos mais sobre esta função e suas propriedades na seção Customização de critérios.
O InsertionSort é um método de ordenação que parte de um princípio muito simples. Começando a partir do segundo elemento, ele vai verificando se o elemento selecionado é menor do que o elemento anterior e, se for, empurra ele e se coloca no lugar. repetindo esse processo até achar um elemento menor ou chegar à primeira posição. repetindo esse processo para todos os números até processar o último.
Esse método costuma ser bastante intuitivo pois é possível nos imaginarmos ordenando dessa maneira na vida real, sendo também um método bem fácil de implementar. Infelizmente, sua complexidade de pior caso é proibitiva para vetores maiores, mas para vetores pequenos, é uma excelente escolha.
Propriedade
Complexidades
Tempo (melhor caso)
O(n)
Tempo (pior caso)
O(n²)
Espaço
O(1)
O SelectionSort é um método também bastante simples, se valendo da ideia de que os menores elementos sempre estarão na frente. Com isso, para a primeira leva, ele vai ver qual dos elementos é o menor, e quando ele acabar de percorrer todo o mundo e poder ter acesso a essa informação, ele vai e coloca o maior na frente. Com o menor na frente, agora ele pode procurar o segundo menor, só que ele só precisa ver do segundo pra frente porque ele já sabe que o primeiro já é o menor e assim por diante.
Também é um algoritmo simples de se implementar e não necessita de um vetor auxiliar, mas o fato de ele sempre fazer todas as suas instruções independente do vetor já estar ordenado ou não o torna um algoritmo bem lento para vetores grandes.
Propriedade
Complexidades
Tempo
O(n²)
Espaço
O(1)
O BubbleSort é outro bem simples cuja ideia é ir percorrendo o vetor de par em par, sempre trocando se eles estiverem na ordem errada. Esse processo é repetido n - 1
vezes, onde n
é o número de elementos no vetor e é garantido que o vetor esteja ordenado quando o algoritmo terminar (isso porque cada etapa arrasta o último elemento pra última posição do momento).
Este algoritmo tem todas as características do InsertionSort, então qual método é melhor fica a critério do leitor. De qual dos dois métodos você mais gostou?
Propriedade
Complexidades
Tempo (melhor caso)
O(n)
Tempo (pior caso)
O(n²)
Espaço
O(1)
O MergeSort é um algoritmo mais sofisticado de ordenação, que usa uma ideia recursiva bastante interessante com estas etapas:
Divide o vetor em duas metades
Ordena cada uma das duas metades
Junta as duas metades ordenadas num processo chamado "merge" (daí vem o nome do algoritmo)
Se você tem duas metades ordenadas, então há uma maneira esquematizada de fazer este merge que consiste de você começar pelos elementos iniciais, comparar estes elementos e escolher o menor, seguindo esta abordagem até ter escolhido todos os elementos.
A pergunta que fica é: como ordenar as duas metades que você dividiu? Ora, com MergeSort, claro!
Este algoritmo é extremamente eficiente, com uma ideia um pouco menos intuitiva, mas que faz bastante sentido num ponto de vista de dividir para conquistar. É um método que gasta bastante memória e não é fácil de implementar a princípio, mas que com certeza vale a pena conhecer a fundo, pois essa abordagem para resolver problemas é amplamente utilizada, não apenas com ordenação.
Propriedades
Complexidade
Tempo
O(n log n)
Espaço
O(n)
O QuickSort é o mais eficiente de todos os métodos de ordenação por comparação conhecidos e é o mais amplamente utilizado nos dias de hoje. A ideia do QuickSort é escolher um pivô e ordenar todos os elementos entre menores e maiores que o pivô, fazendo isso recursivamente até que o vetor esteja ordenado. O pivô escolhido normalmente é o último elemento e o GIF abaixo esquematiza a primeira execução de função particao()
, destacando a escolha do pivô e as comparações e trocas feitas nesta execução.
Tem uma ideia bem parecida com o MergeSort com mesmas complexidades de tempo, mas com uma baita economia de espaço. É o algoritmo de escolha em grande parte das implementações dos métodos sort()
em diversas bibliotecas.
Propriedades
Complexidade
Tempo (melhor caso)
O(n log n)
Tempo (pior caso)
O(n²) (muito raro)
Espaço
O(1)
O Counting sort é bastante eficiente, mas só serve para um caso particular onde sabemos que os elementos possuem valores dentro de um determinado intervalo finito. A ideia por trás disso é contar quantos de cada elemento tem e após isso usar as contagens para reconstruir o vetor ordenado.
Este é o mais eficiente de todos os algoritmos que foram apresentados, mas ele só serve para casos muito específicos, ou seja, não é facilmente aplicável. Entretanto, é extremamente útil entender esse algoritmo porque alguns outros problemas podem ser resolvidos com muito menos espaço utilizando esta abordagem.
Ao contrário dos outros algoritmos, onde pudemos aplicar a função generalizada de comparação, aqui estamos nos atendo a ordenar do menor para o maior.
Propriedades
Complexidade
Tempo
O(n)
Espaço
O(m), onde m é o tamanho do intervalo de elementos
Em C, é possível usar a função qsort()
para ordenar um vetor por QuickSort. Mais informações sobre a função podem ser encontradas na documentação.
Em C++, pode-se usar a função sort()
para ordenar um vetor ou um vector
. Mais informações sobre a função podem ser encontradas na documentação.
Em JavaScript, pode-se usar a função sort()
para ordenar um vetor. Mais informações sobre a função podem ser encontradas na documentação.
Em Python, pode-se usar tanto a função sort()
quanto a função sorted()
onde a primeira ordena o vetor e a segunda retorna uma cópia do vetor ordenado, sem alterar o vetor original. Mais informações sobre ambas as funções podem ser encontradas na documentação.
Até aqui, você percebeu que nas implementações usadas, eu usei um método chamado comp()
como método de comparação entre elementos e não usei o operador de menor, que é o que normalmente se utiliza quando exemplificados o algoritmo? A intenção disso é para discorrer um pouco sobre como podemos customizar nossa função de comparação para podemos comparar quaisquer aspectos que desejarmos dos elementos dos vetores que estamos ordenando.
Com a comparação relativa, comparamos dois elementos do vetor e indicamos qual dos elementos deve vir na frente. Com comparações o bastante, podemos ordenar o vetor de uma maneira esquemática (basicamente como mostrado em quase todos os métodos que vimos na seção anterior).
Nossa função comp(a, b)
recebe dois elementos de mesmo tipo a
e b
retorna as seguintes respostas:
< 0
: caso a
tenha que estar na frente de b
no vetor ordenado
= 0
: caso os elementos tenham o mesmo valor e, portanto, não é possível comparar ambos os valores
> 0
: caso b
tenha que estar na frente de a
no vetor ordenado
As implementações de sort()
em diversas bibliotecas têm suporte a uma função comp()
personalizada que segue estas normas, o que torna o processo de ordenar qualquer tipo de elemento muito mais tranquilo.
Na linguagem C++, o método comp()
retorna um booleano onde true
cobre o caso dos números negativos e false
cobre os demais.
Comparar dois inteiros e retornar número negativo se a
estiver na frente de b
na ordenação, número positivo caso contrário e 0
se os dois números são iguais.
Comparar dois Alunos
, ordenando-os primeiro por nota (da maior nota pra menor) e se os dois alunos têm mesma nota, apresentar em ordem alfabética. Repare que aqui começamos trabalhamos com dois casos: caso em que as notas são iguais e, neste caso, temos que desempatar pelo nome; e em que as notas são diferentes, então comparar pela nota já nos trás um resultado conclusivo.
Com a comparação por chave, associamos cada elemento com um outro valor que é usado para definir a ordem de cada número. Por exemplo, podemos usar como chave o próprio valor, se quisermos ordenar números em ordem crescente ou podemos usar como chave o tamanho de uma string se quisermos ordenar palavras por tamanho.
No exemplo abaixo, queremos ordenar uma estrutura de dados Aluno
pelo nome.