Ordenação (sort)
Esta aula aborda a ordenação em Go, explorando desde a função sort.Slice para ordenações rápidas até interfaces de ordenação para controle total, além de busca binária e a diferença entre algoritmos estáveis e instáveis.
Nesta aula, vamos mergulhar no pacote sort da biblioteca padrão de Go, uma ferramenta essencial para organizar dados de forma eficiente. Você aprenderá a ordenar slices de tipos básicos e personalizados, realizar buscas binárias e entender as nuances de estabilidade que afetam o comportamento da ordenação.
Dominar a ordenação é fundamental para muitos algoritmos e operações do dia a dia. O pacote sort fornece funções prontas e interfaces que permitem ordenar praticamente qualquer tipo de dado, com desempenho otimizado e sintaxe limpa.
sort.Slice
A função sort.Slice é a maneira mais simples de ordenar um slice em Go. Ela recebe o slice e uma função de comparação que define a ordem. Essa função deve retornar true se o elemento na posição i deve vir antes do elemento na posição j.
Um dos grandes benefícios do sort.Slice é que ele funciona com qualquer tipo de slice, sem exigir que o tipo implemente uma interface específica. Isso torna a ordenação rápida e direta, especialmente para tipos básicos como inteiros, strings ou estruturas personalizadas.
Vamos ver um exemplo prático ordenando um slice de inteiros e um slice de structs:
package main
import (
"fmt"
"sort"
)
type Pessoa struct {
Nome string
Idade int
}
func main() {
// Ordenando inteiros
numeros := []int{5, 2, 8, 1, 9}
sort.Slice(numeros, func(i, j int) bool {
return numeros[i] < numeros[j]
})
fmt.Println("Números ordenados:", numeros) // [1 2 5 8 9]
// Ordenando structs por idade
pessoas := []Pessoa{
{"Alice", 30},
{"Bob", 25},
{"Carol", 35},
}
sort.Slice(pessoas, func(i, j int) bool {
return pessoas[i].Idade < pessoas[j].Idade
})
fmt.Println("Pessoas ordenadas por idade:", pessoas)
}
A função de comparação captura as variáveis do slice por referência, permitindo acessar os elementos diretamente. Isso é conveniente, mas requer cuidado com a captura de variáveis em loops, pois a função pode ser chamada em qualquer momento durante a ordenação.
Interfaces de ordenação
Para tipos que precisam ser ordenados frequentemente, Go oferece a interface sort.Interface. Ela exige a implementação de três métodos: Len(), Less(i, j int) bool e Swap(i, j int). Ao implementar essa interface, você pode usar sort.Sort para ordenar o seu tipo.
Essa abordagem é mais verbosa, mas oferece mais controle e reutilização. Ela também permite que você defina a ordenação como parte do tipo, tornando o código mais expressivo.
Vejamos como implementar a interface para o tipo Pessoa e ordenar por nome:
type PorNome []Pessoa
func (p PorNome) Len() int { return len(p) }
func (p PorNome) Less(i, j int) bool { return p[i].Nome < p[j].Nome }
func (p PorNome) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func main() {
pessoas := []Pessoa{
{"Alice", 30},
{"Bob", 25},
{"Carol", 35},
}
sort.Sort(PorNome(pessoas))
fmt.Println("Pessoas ordenadas por nome:", pessoas)
}
Além de sort.Sort, o pacote fornece sort.Stable, que preserva a ordem original de elementos iguais (veremos mais adiante). Também existem atalhos para tipos básicos: sort.Ints, sort.Float64s, sort.Strings.
Busca
O pacote sort também oferece funções de busca binária para slices ordenados. A função sort.Search retorna o menor índice i no qual a função passada retorna true. Para slices de tipos básicos, existem atalhos como sort.SearchInts, sort.SearchFloat64s e sort.SearchStrings.
É importante que o slice esteja ordenado para que a busca binária funcione corretamente. O retorno é o índice onde o elemento deveria ser inserido para manter a ordem.
Exemplo de busca em um slice de inteiros:
numeros := []int{1, 3, 5, 7, 9}
procurado := 5
indice := sort.SearchInts(numeros, procurado)
if indice < len(numeros) && numeros[indice] == procurado {
fmt.Printf("Encontrado %d no índice %d\n", procurado, indice)
} else {
fmt.Printf("%d não encontrado, deveria ser inserido em %d\n", procurado, indice)
}
Para busca personalizada, você pode usar sort.Search com uma função que define a condição. Por exemplo, buscar a primeira pessoa com idade maior que 30 em um slice ordenado por idade:
idades := []int{25, 30, 35, 40}
indice := sort.Search(len(idades), func(i int) bool { return idades[i] > 30 })
fmt.Println("Primeira idade > 30 no índice:", indice) // 2
Estável vs instável
A estabilidade de um algoritmo de ordenação refere-se à manutenção da ordem relativa de elementos com chaves iguais. Um algoritmo estável garante que, se dois elementos têm a mesma chave, a ordem original é preservada. Algoritmos instáveis podem trocá-los.
Em Go, sort.Slice e sort.Sort usam um algoritmo instável (quicksort variante), enquanto sort.SliceStable e sort.Stable usam um algoritmo estável (merge sort). A estabilidade é importante quando você ordena por múltiplos critérios em sequência.
Por exemplo, se você ordenar uma lista de pessoas primeiro por nome e depois por idade, com um algoritmo estável, a ordenação por idade preservará a ordem por nome para pessoas com a mesma idade. Com um algoritmo instável, isso pode não acontecer.
A escolha entre estável e instável envolve um trade-off: algoritmos estáveis costumam ser um pouco mais lentos e usar mais memória, mas garantem a preservação da ordem. Para a maioria dos casos, a versão instável é suficiente, mas quando a ordem relativa importa, use a estável.
Exemplo demonstrando a diferença:
type Item struct {
Categoria string
Valor int
}
itens := []Item{
{"A", 1},
{"B", 1},
{"A", 2},
}
// Ordenar por valor (instável)
sort.Slice(itens, func(i, j int) bool { return itens[i].Valor < itens[j].Valor })
fmt.Println("Instável:", itens) // Pode trocar a ordem dos itens com valor 1
itens2 := []Item{
{"A", 1},
{"B", 1},
{"A", 2},
}
// Ordenar por valor (estável)
sort.SliceStable(itens2, func(i, j int) bool { return itens2[i].Valor < itens2[j].Valor })
fmt.Println("Estável:", itens2) // Mantém a ordem original: A, B, A
Boas práticas
Ao trabalhar com ordenação em Go, considere as seguintes recomendações:
- Use
sort.Slicepara ordenações rápidas e pontuais, especialmente em funções pequenas. - Implemente
sort.Interfacequando o tipo for usado em múltiplos lugares ou quando você quiser encapsular a lógica de ordenação. - Prefira
sort.SliceStablequando a ordem de elementos iguais for relevante. - Para buscar em slices ordenados, use as funções de busca binária para eficiência O(log n).
- Lembre-se de que a ordenação modifica o slice original; se precisar preservar a ordem original, faça uma cópia antes.
Referências
- Documentação oficial do pacote sort
- Go Blog: Slices
- Go Wiki: Slice Tricks
- GeeksforGeeks: Stable vs Unstable Sorting
- Wikipedia: Sorting algorithm
- YourBasic: How to sort in Go
Exercícios
- Exercício 1: Escreva uma função que receba um slice de strings e o ordene em ordem alfabética decrescente usando
sort.Slice.✓ Resposta:func ordenarDecrescente(s []string) { sort.Slice(s, func(i, j int) bool { return s[i] > s[j] }) } - Exercício 2: Implemente a interface
sort.Interfacepara um tipoPessoaque ordene por idade de forma crescente e, em caso de empate, por nome.✓ Resposta:type PorIdadeNome []Pessoa func (p PorIdadeNome) Len() int { return len(p) } func (p PorIdadeNome) Less(i, j int) bool { if p[i].Idade != p[j].Idade { return p[i].Idade < p[j].Idade } return p[i].Nome < p[j].Nome } func (p PorIdadeNome) Swap(i, j int) { p[i], p[j] = p[j], p[i] } - Exercício 3: Dado um slice ordenado de inteiros, use
sort.SearchIntspara encontrar o índice de um valor específico. Se não existir, retorne o índice onde ele deveria ser inserido.✓ Resposta:func buscarIndice(s []int, alvo int) int { indice := sort.SearchInts(s, alvo) return indice } - Exercício 4: Explique quando usar
sort.SliceStableem vez desort.Slicee dê um exemplo prático.✓ Resposta: Usesort.SliceStablequando a ordem relativa de elementos iguais for importante. Por exemplo, ao ordenar uma lista de alunos por nota, mas mantendo a ordem alfabética original para notas iguais.sort.SliceStable(alunos, func(i, j int) bool { return alunos[i].Nota < alunos[j].Nota }) - Exercício 5: Implemente uma função que, dado um slice de structs com campo
NomeeIdade, ordene por idade de forma estável usandosort.SliceStablee depois por nome de forma estável, para garantir que a ordem por idade seja preservada quando os nomes forem iguais.✓ Resposta:func ordenarPessoas(pessoas []Pessoa) { // Primeiro por nome (estável) sort.SliceStable(pessoas, func(i, j int) bool { return pessoas[i].Nome < pessoas[j].Nome }) // Depois por idade (estável) – preserva a ordem por nome para idades iguais sort.SliceStable(pessoas, func(i, j int) bool { return pessoas[i].Idade < pessoas[j].Idade }) }
Observações finais
A ordenação é uma operação fundamental que aparece em muitos problemas. O pacote sort de Go é poderoso e flexível, permitindo ordenar praticamente qualquer tipo de dado com facilidade. Lembre-se de que a estabilidade pode ser crucial em certos cenários, e a busca binária é uma ferramenta valiosa para consultas rápidas em dados ordenados.
Pratique os conceitos com os exercícios e explore a documentação oficial para mais detalhes. Em projetos reais, você frequentemente encontrará a necessidade de ordenar dados, e dominar essas técnicas fará de você um programador Go mais eficiente.