Você precisa organizar etiquetas de preço em ordem crescente. Procura a menor de todas e a coloca no início. Depois, procura a menor entre as que sobraram e a coloca em segundo, e assim por diante.
Qual parte da sequência já está pronta depois de cada busca? Essa parte ainda pode mudar?
Faça a primeira busca em [5, 2, 4]. Quantas comparações são necessárias para ter certeza de que achou o menor?
Conferir depois de tentar
São 2 comparações: o 5 com o 2 (o 2 vira o menor até agora) e o 2 com o 4. Para ter certeza com n elementos, são sempre n − 1 comparações, porque cada um dos outros precisa ser olhado uma vez.
Como funciona
O selection sort divide o vetor em duas partes: à esquerda, a parte pronta; à direita, a parte que ainda falta. Em cada etapa:
Seleciona. Percorre toda a parte que falta e anota a posição do menor valor.
Troca. Leva esse menor para o início da parte que falta, com uma única troca.
Avança. A parte pronta cresce uma posição.
Cada etapa procura em um trecho menor (o contorno) e faz no máximo uma troca, indicada pela seta laranja. Na etapa 4, o menor já está no lugar, e nenhuma troca é feita.
O que fica garantido a cada etapa
Depois de k etapas, as k primeiras posições têm os k menores valores, já no lugar definitivo. Essa parte nunca mais muda. É o oposto do insertion sort, cuja parte ordenada ainda pode ser empurrada.
Há um preço: para ter certeza de que achou o menor, a etapa precisa olhar todos os elementos que faltam. Não existe atalho, nem quando a lista já chega ordenada. Com 5 elementos, são sempre 4 + 3 + 2 + 1 = 10 comparações. Esse total, n(n−1)/2, é igual ao número de pares possíveis de elementos, mas cuidado: nem todo par é comparado. Cada elemento é comparado com o menor encontrado até aquele momento.
O ponto forte: poucas escritas
Cada etapa faz no máximo uma troca. Para n elementos, são no máximo n − 1 trocas, não importa quão bagunçada esteja a lista. O bubble e o insertion fazem um movimento para cada par fora de ordem, o que pode chegar a centenas de milhares de movimentos em uma lista pequena.
Medido nesta aula com a mesma lista aleatória de 1 000 números. A barra do selection sort quase não aparece: são 1 984 escritas, contra quase meio milhão do bubble sort.
Dá para conferir isso com uma conta. Cada troca do selection sort escreve em 2 posições do vetor, e são no máximo n − 1 trocas: no máximo 2(n − 1) escritas. O bubble sort escreve 2 vezes o número de inversões, e o insertion sort escreve o número de inversões mais n − 1. Como cada troca do selection sort conserta pelo menos uma inversão, ele nunca escreve mais que os outros dois.
Isso importa de verdade em dois casos:
Memória que se desgasta: memórias flash e EEPROM, comuns em microcontroladores, suportam um número limitado de escritas por posição. Escrever 250 vezes menos significa um equipamento que dura mais.
Registros enormes: se cada elemento é uma struct de vários kilobytes, cada movimento custa caro. Poucas trocas compensam muitas comparações baratas.
Na prática
Laboratório de ordenação
Informe de 2 a 8 inteiros entre −99 e 99, separados por vírgula. Teste um vetor já ordenado e um decrescente: o número de comparações muda? E o de trocas?
Candidato e menor atual
Posição final garantida
Implementação em C
Funções: copie para um programa que tenha main
void selection_sort(int v[], int n) {
for (int i = 0; i < n - 1; i++) { // v[0..i-1] já está pronto
int menor = i; // POSIÇÃO do menor, não o valor
for (int j = i + 1; j < n; j++) {
if (v[j] < v[menor]) menor = j;
}
if (menor != i) { // no máximo uma troca por etapa
int temp = v[i];
v[i] = v[menor];
v[menor] = temp;
}
}
}
Para usar: declare int v[] = {64, 25, 12, 22, 11}; e chame selection_sort(v, 5);. O resultado é [11, 12, 22, 25, 64], com as 10 comparações e 3 trocas da figura.
Armadilhas frequentes
Guardar o valor do menor em vez da posição: no fim da busca, não se sabe onde ele estava, e a troca fica impossível. O exercício 2 mostra o estrago.
Trocar dentro do laço interno, sempre que encontrar um menor, multiplica as trocas e destrói a principal vantagem do algoritmo.
Achar que uma lista já ordenada termina mais rápido. Não termina: as comparações são sempre as mesmas.
Exemplo real: o pódio da corrida
Uma corrida de rua tem milhares de atletas, mas a organização precisa anunciar agora apenas os três primeiros. Ordenar todo mundo seria desperdício.
O selection sort tem uma propriedade rara: depois de k etapas, as k primeiras posições já estão prontas e definitivas. Então basta executar 3 etapas e parar:
Programa completo: compile e execute
#include <stdio.h>
typedef struct {
char nome[20];
float tempo; // em segundos: menor é melhor
} Atleta;
// Deixa os k melhores tempos em a[0..k-1], em ordem. O resto fica sem ordem.
void podio(Atleta a[], int n, int k) {
if (k > n) k = n;
for (int i = 0; i < k; i++) { // só k etapas
int melhor = i;
for (int j = i + 1; j < n; j++) {
if (a[j].tempo < a[melhor].tempo) melhor = j;
}
if (melhor != i) {
Atleta temp = a[i];
a[i] = a[melhor];
a[melhor] = temp;
}
}
}
int main(void) {
Atleta a[] = {
{"Lia", 12.8f}, {"Rui", 11.9f}, {"Gil", 13.4f}, {"Bel", 11.5f},
{"Teo", 12.1f}, {"Ivo", 12.0f}, {"Mel", 13.0f}, {"Noa", 12.4f}
};
int n = sizeof a / sizeof a[0];
podio(a, n, 3);
for (int i = 0; i < 3; i++) {
printf("%d. %s %.1f s\n", i + 1, a[i].nome, a[i].tempo);
}
return 0;
}
1. Bel 11.5 s
2. Rui 11.9 s
3. Ivo 12.0 s
Cada etapa percorre a lista uma vez, então o pódio custa só 3 passagens pela lista. Nos testes desta aula, o pódio de 1 milhão de atletas saiu em 10 ms. Ordenar o milhão inteiro com o selection sort levaria, pela regra do quadrado (20 vezes mais dados, 400 vezes mais tempo), cerca de 2,7 s × 400 ≈ 18 minutos, uma estimativa.
O bubble sort também tem essa propriedade, só que ao contrário: depois de k passagens, os k maiores estão prontos no fim. O insertion sort não tem: só no final se sabe quem está em primeiro.
Estabilidade: a troca de longe
O selection sort não é estável: elementos empatados podem trocar de ordem. O motivo é a troca distante. Quando o menor, ou o maior em um ranking, vem do fim do vetor, ele troca de lugar com o primeiro da parte que falta, e esse primeiro é jogado lá para trás, passando por cima de quem estava no meio.
Ranking por nota, da maior para a menor. A troca de longe joga Ana para depois de Caio, mesmo com notas iguais. Deslocando em vez de trocar, a ordem de inscrição é mantida.
A versão estável, à direita da figura, retira o escolhido e desloca os elementos do meio uma posição, como no insertion sort:
Funções: copie para um programa que tenha main
typedef struct {
char nome[20];
float nota;
} Aluno;
// Ranking estável: maior nota primeiro; empatados mantêm a ordem original.
void selection_estavel(Aluno v[], int n) {
for (int i = 0; i < n - 1; i++) {
int melhor = i;
for (int j = i + 1; j < n; j++)
if (v[j].nota > v[melhor].nota) melhor = j; // > estrito: o primeiro dos empatados
Aluno escolhido = v[melhor];
for (int k = melhor; k > i; k--) v[k] = v[k - 1]; // desliza, em vez de trocar
v[i] = escolhido;
}
}
Com a entrada da figura (Ana 8.5, Caio 8.5, Bia 9.0), o resultado é Bia, Ana, Caio.
O preço é perder justamente a vantagem de poucas escritas: os deslocamentos voltam a ser tantos quanto no insertion sort. Se você precisa de estabilidade, use o insertion sort de uma vez.
Quanto custa na prática?
Medimos as três ordenações da revisão com as mesmas listas de 50 000 números:
Tempo e escritas medidos para 50 000 elementos
Entrada
Selection
Insertion
Bubble
Já ordenada
2,7 s
< 0,1 ms
< 0,1 ms
Aleatória
2,7 s
0,21 s
2,9 s
Ordem decrescente
2,7 s
0,43 s
1,4 s
Escritas no vetor (aleatória)
~100 mil
626 milhões
1,25 bilhão
Mesmo tempo nos três casos medidos. O selection sort levou 2,7 s nas três entradas, inclusive com a lista já ordenada, que os outros dois resolvem em uma passagem. O número de comparações é exatamente o mesmo, 1,25 bilhão, em qualquer entrada. O tempo ainda pode variar um pouco com a máquina e os dados.
Escritas: o menor número entre os três. Cerca de 100 mil escritas contra centenas de milhões. Entre estas três implementações, ele nunca escreve mais que as outras e costuma escrever muito menos em listas bagunçadas. Se escrever for o gargalo, ele é o melhor dos três, embora não seja o mínimo absoluto que um algoritmo qualquer poderia atingir.
Parar cedo é a carta na manga. Os 3 primeiros de 1 milhão saíram em 10 ms, como no pódio.
Comparação com as outras ordenações da revisão
Comportamento das três ordenações elementares
Algoritmo
Lista já ordenada
Lista bagunçada
Movimentos de dados
Estável
Bubble (com parada)
uma passagem, rápido
quadrático, lento
muitos: uma troca por inversão
Sim
Insertion
uma passagem, rápido
quadrático; nas medições, o mais rápido dos três
um deslocamento por inversão, mais barato que uma troca
Sim
Selection
quadrático, mesmo ordenada
quadrático, lento
poucos: no máximo n − 1 trocas
Não
Qual usar?
Insertion sort
Listas pequenas, quase ordenadas ou que recebem dados aos poucos. É o padrão entre os três.
Selection sort
Quando escrever é caro, os registros são enormes ou você só precisa dos k primeiros.
Bubble sort
Didático: ensina inversões, invariantes (garantias que valem a cada passagem) e estabilidade. Em produção, perde para o insertion sort.
Exercícios progressivos
Use a figura do passo a passo como modelo: marque o trecho onde a etapa procura, o menor encontrado e a troca. A dica não entrega a resposta: ela indica a pergunta certa a fazer.
1. A prateleira de preços Básico
Um repositor organiza as etiquetas [7, 3, 9, 1] com o selection sort. Mostre o vetor depois da etapa 1 e depois da etapa 2. Em alguma delas a troca não acontece?
Ver uma dica
Na etapa 1, procure em todas as 4 posições. Na etapa 2, a posição 0 já está pronta: procure só nas posições 1, 2 e 3. Se o menor encontrado já estiver na primeira posição do trecho, o que a implementação faz?
Conferir a resposta
Etapa 1: o menor de [7, 3, 9, 1] é o 1, na posição 3. Ele troca com o 7, e o vetor fica [1, 3, 9, 7].
Etapa 2: o menor entre as posições 1 a 3 (3, 9, 7) é o 3, que já está na posição 1. O teste menor != i é falso, então nenhuma troca acontece, e o vetor continua [1, 3, 9, 7].
A etapa 3 ainda vai trocar o 9 com o 7. No total, são 2 trocas para 4 elementos, menos que o máximo de 3.
2. A nota que sumiu Básico
Um colega achou mais simples guardar o valor do menor, em vez da posição:
Trecho: coloque dentro do main de um programa
for (int i = 0; i < n - 1; i++) {
int menor = v[i];
for (int j = i + 1; j < n; j++)
if (v[j] < menor) menor = v[j];
v[i] = menor;
}
Com as notas [3, 1, 2], o resultado foi [1, 1, 2]: o 3 sumiu, e o 1 apareceu duas vezes. Explique o que aconteceu.
Ver uma dica
Uma troca tem duas metades: o menor vai para a posição i, e o valor que estava em i vai para onde o menor estava. Este código faz qual metade? Para fazer a outra, que informação falta?
Conferir a resposta
Na primeira etapa, o código descobre que o menor vale 1 e escreve 1 em v[0], por cima do 3. Ele faz só metade da troca: o 1 é copiado para a frente, mas o 3 nunca vai para a posição onde o 1 estava, porque o código não sabe que posição é essa. O 3 é perdido, e o 1 fica duplicado.
Guardar a posição (menor = j) resolve. Com ela, dá para ler o valor (v[menor]) e também saber onde colocar o elemento que sai da posição i. Esse bug é traiçoeiro porque o resultado parece ordenado.
3. A lista que quase nunca muda Intermediário
Um sistema de estoque reordena a lista de 1 000 produtos a cada hora, mas na maioria das vezes nada mudou e a lista já está ordenada. Quantas comparações o selection sort faz nesse caso? E o insertion sort? Qual dos dois você escolheria para esse sistema?
Ver uma dica
No selection sort, quantos elementos a primeira etapa olha? E a segunda? Algo nessa conta depende de a lista estar ordenada? No insertion sort, quantas comparações cada inserção faz quando o elemento já está no lugar certo?
Conferir a resposta
Selection sort: 999 + 998 + … + 1 = 499 500 comparações, exatamente como em uma lista bagunçada. Cada etapa precisa olhar tudo o que falta para ter certeza de qual é o menor, e o algoritmo não tem como “perceber” que a lista já estava em ordem.
Insertion sort: cada elemento é comparado só com o vizinho da esquerda, que já é menor, e fica onde está. São 999 comparações, 500 vezes menos.
Para esse sistema, a escolha é o insertion sort. Nas medições da aula, com 50 mil elementos já ordenados, o selection levou 2,7 s, e o insertion terminou em menos de 0,1 ms.
4. Empate no ranking Intermediário
Uma escola usa o selection sort para montar o ranking por nota, da maior para a menor, procurando o maior a cada etapa. Os alunos estão em ordem de inscrição: Ana 8.5, Caio 8.5, Bia 9.0. Qual é o ranking final? O regulamento diz que, em empate, quem se inscreveu antes fica à frente. Ele foi respeitado?
Ver uma dica
Na etapa 1, quem tem a maior nota, e em que posição está? Com quem ele troca? Depois da troca, onde Ana foi parar em relação a Caio? Compare com a figura da estabilidade.
Conferir a resposta
Etapa 1: a maior nota é a de Bia, 9.0, na posição 2. Ela troca com Ana, que estava na posição 0, e o vetor fica Bia, Caio, Ana. Etapa 2: entre Caio e Ana, ambos com 8.5, o maior encontrado primeiro é Caio, que já está na posição 1. Nada muda.
Ranking final: Bia, Caio, Ana. O regulamento não foi respeitado: Ana se inscreveu antes, mas ficou atrás de Caio. A troca de longe jogou Ana para o fim. Para corrigir, use a versão estável (deslocando) ou o insertion sort, que é estável e mais rápido.
5. Selection sort recursivo Avançado
Reescreva o selection sort de forma recursiva, usando a receita da aula de recursão: “para ordenar n elementos, leve o maior para a última posição e ordene os n − 1 primeiros”. Escreva void selection_rec(int v[], int n) e teste com [5, 2, 8, 1, 9, 3]. Depois responda: por que, na prática, essa versão é pior que a com laços?
Repare que esta é a versão “espelhada” do selection sort da aula: em vez de levar o menor para o início, ela leva o maior para o fim. O resultado é o mesmo.
Ver uma dica
Aplique as três perguntas. Qual é o menor caso, que já está ordenado sem fazer nada? Como o problema fica menor a cada chamada? Para a segunda parte, lembre o que aconteceu com soma(50000) na aula de recursão.
Conferir a resposta
Funções: copie para um programa que tenha main
void selection_rec(int v[], int n) {
if (n <= 1) return; // 0 ou 1 elemento: já ordenado
int maior = 0;
for (int j = 1; j < n; j++) {
if (v[j] > v[maior]) maior = j;
}
if (maior != n - 1) { // o maior vai para o fim
int temp = v[maior];
v[maior] = v[n - 1];
v[n - 1] = temp;
}
selection_rec(v, n - 1); // ordena o resto
}
Para [5, 2, 8, 1, 9, 3], o resultado é [1, 2, 3, 5, 8, 9].
A versão funciona e é uma ótima forma de enxergar a ideia, mas faz uma chamada recursiva por elemento. Executada de forma direta, com 50 mil elementos, seriam 50 mil chamadas abertas ao mesmo tempo na pilha, e na aula de recursão soma(50000) estourou a pilha no ambiente de teste. Como a chamada recursiva é a última coisa que a função faz, alguns compiladores com otimização a transformam em um laço e eliminam esse risco, mas não dá para contar com isso. A versão com laços faz o mesmo trabalho sem depender do compilador. Recursão é a ferramenta certa quando o problema se divide em partes, como nas pastas ou na busca binária, e não quando só diminui de um em um.
./desafio --nivel=hacker
Desafio hacker: o robô do armazém
Tipo: opcional
Dificuldade: ★★★★★
Tema: permutações e ciclos
Limite: até 100 000 prateleiras
Um robô organiza n caixas em prateleiras numeradas de 0 a n − 1. A prateleira i guarda a caixa de etiqueta p[i], e todas as etiquetas de 0 a n − 1 aparecem uma vez (uma lista assim se chama permutação). Cada troca de caixas entre duas prateleiras gasta energia. Quantas trocas são necessárias, no mínimo, para que a prateleira i guarde a caixa i?
Escreva int min_trocas(const int p[], int n), passando por cada prateleira um número fixo de vezes, sem simular trocas.
Explique por que não dá para fazer com menos trocas.
Mostre que o selection sort desta aula faz exatamente esse número de trocas.
Entradap = {1, 0, 3, 2}
Saída2
Entradap = {4, 0, 3, 1, 2}
Saída4
Aquecimento: desenhe à mão p = {1, 2, 0}, com uma seta de cada prateleira para onde a caixa dela deveria ir. Depois troque as caixas das prateleiras 0 e 1, desenhe de novo e compare. A prova do item 2 é a parte mais difícil e é opcional: escrever min_trocas já vale o desafio.
Dica 1: desenhe setas
Desenhe cada prateleira como um círculo e uma seta da prateleira i para a prateleira p[i], onde a caixa dela deveria estar. Cada prateleira tem exatamente uma seta saindo e uma chegando. Siga as setas a partir de qualquer prateleira: o que acontece?
Dica 2: o armazém arrumado
No armazém arrumado, cada prateleira aponta para si mesma: são n “voltinhas” de tamanho 1. Agora pense no que uma troca faz com as voltas: ela pode quebrar uma volta em duas? Pode juntar duas em uma?
Dica 3: e o selection sort?
Na etapa i do selection sort, o menor valor que falta é exatamente a caixa i. Onde ela está? O que a troca faz com a prateleira i e com o ciclo dela?
Solução comentada
Ideia. Seguindo as setas, você sempre volta ao ponto de partida: as prateleiras se agrupam em ciclos separados.
À esquerda, uma única volta passando pelas 5 prateleiras. À direita, duas voltas independentes de 2 prateleiras. Cada volta com L prateleiras precisa de L − 1 trocas.
Em um ciclo de L prateleiras, se cada troca levar uma caixa direto ao seu destino, ela acerta pelo menos uma caixa, e a última troca acerta duas de uma vez: são L − 1 trocas. No aquecimento, {1, 2, 0} é um ciclo de 3, e trocar as prateleiras 0 e 1 acerta a caixa 1, deixando um ciclo de 1 e um de 2. Somando todos os ciclos:
mínimo de trocas = n − (número de ciclos)
Funções: copie para um programa que tenha main
#include <stdlib.h>
// p é uma permutação de 0..n-1. Retorna -1 se faltar memória.
// Usa n bytes extras de memória para marcar as prateleiras visitadas.
int min_trocas(const int p[], int n) {
if (n == 0) return 0; // nada a organizar (e evita calloc(0, 1))
char *visitado = calloc(n, 1); // calloc zera a memória
if (visitado == NULL) return -1;
int ciclos = 0;
for (int i = 0; i < n; i++) {
if (visitado[i]) continue; // já pertence a um ciclo contado
ciclos++;
for (int j = i; !visitado[j]; j = p[j]) // percorre o ciclo de i
visitado[j] = 1;
}
free(visitado);
return n - ciclos;
}
Cada prateleira é visitada uma única vez, então o tempo é proporcional a n: nos testes, 100 mil prateleiras levaram cerca de 1 milissegundo.
Por que não dá para fazer com menos. Trocar duas prateleiras do mesmo ciclo o divide em dois (+1 ciclo). Trocar prateleiras de ciclos diferentes junta os dois (−1 ciclo). Cada troca aumenta o número de ciclos em no máximo 1. O armazém arrumado tem n ciclos, então, partindo de c ciclos, são necessárias pelo menos n − c trocas.
O selection sort atinge esse mínimo. Na etapa i, o menor valor que falta é exatamente a caixa i. O algoritmo troca a prateleira i com a prateleira onde está a caixa i. As duas estão no mesmo ciclo, porque a seta dessa prateleira aponta justamente para i. A troca destaca a prateleira i como um ciclo de tamanho 1: exatamente +1 ciclo por troca. Quando a caixa i já está no lugar, não há troca. Resultado: exatamente n − c trocas, o mínimo possível.
Verificação: a fórmula foi conferida em todas as permutações com até 6 elementos (873 casos), comparando com uma busca que testa todas as sequências de trocas possíveis. O selection sort desta aula fez o número mínimo de trocas em todos os casos.
Generalização: se as etiquetas forem valores distintos quaisquer, e não 0..n−1, ordene uma cópia para descobrir onde cada valor deve ficar e aplique a mesma contagem de ciclos.
Cuidado com valores repetidos. Tudo isso depende de cada caixa ter um único destino. Com valores repetidos, o selection sort pode trocar mais que o necessário. Em [2, 1, 1], ele faz 2 trocas, mas trocar só a primeira e a última posições já ordena com 1.
Revisão rápida
A cada etapa, seleciona a posição do menor que falta e faz no máximo uma troca.
As primeiras posições ficam prontas e definitivas: dá para parar cedo e obter só o pódio.
As comparações são sempre as mesmas, mesmo com a lista já ordenada: nas medições, 2,7 s para 50 mil nas três entradas testadas.
Faz pouquíssimas escritas: nunca escreve mais que os outros dois algoritmos. Com valores distintos, faz o mínimo possível de trocas: ideal quando escrever é caro.
Não é estável por causa da troca de longe. Precisa de estabilidade? Use o insertion sort.