Uma fila de fichas numeradas está fora de ordem. A única operação permitida é comparar duas fichas vizinhas e trocá-las de lugar.
Repetindo comparações da esquerda para a direita, qual número chega obrigatoriamente ao fim da fila? Quantas vezes é preciso repetir?
Experimente com as fichas 5, 2, 4 e 1 antes de continuar. Acompanhe o 5.
Como funciona
O bubble sort percorre o vetor comparando cada par de vizinhos, v[j] e v[j+1]. Se o da esquerda for maior, troca os dois. Um percurso completo, do início ao fim, é uma passagem. Durante a passagem, o maior valor encontrado é “carregado” pelas trocas, como uma bolha subindo na água, até o fim da parte ainda desorganizada.
Cada linha mostra o par comparado (amarelo) antes da decisão. Ao final de cada passagem, mais um valor fica definitivamente no lugar (verde), e a passagem seguinte pode parar uma posição antes.
O que fica garantido a cada passagem
Depois da 1ª passagem, o maior valor está na última posição. Depois da 2ª, os dois maiores estão nas duas últimas, e assim por diante. Após k passagens, as k últimas posições estão prontas e não mudam mais. Com n − 1 passagens, o vetor inteiro está ordenado.
Uma garantia como essa, que continua verdadeira ao final de cada repetição de um laço, se chama invariante. Saber o invariante de um algoritmo é o que permite prever o estado do vetor em qualquer ponto da execução.
Parada antecipada: se uma passagem inteira terminar sem nenhuma troca, todos os vizinhos já estão em ordem, e o vetor está ordenado. Não há por que continuar. É essa regra que torna o bubble sort rápido em listas que já chegam ordenadas.
Inversões: a medida da bagunça
Quanto trabalho o bubble sort vai ter? Depende de quão bagunçado está o vetor, e existe um jeito simples de medir isso. Uma inversão é qualquer par de elementos em que o maior aparece antes do menor, sejam eles vizinhos ou não.
Em [5, 2, 4, 1], há 5 pares fora de ordem. O par (2, 4) não conta, porque já está na ordem certa.
Trocas = inversões
Quando o bubble sort troca dois vizinhos fora de ordem, ele conserta exatamente um par invertido, e nenhum outro par muda. Por isso, o número de trocas é sempre igual ao número de inversões. Na figura das passagens, foram 5 trocas, os mesmos 5 arcos. Com isso, dá para prever o trabalho sem executar o algoritmo.
Na prática
Laboratório de ordenação
Informe de 2 a 8 inteiros entre −99 e 99, separados por vírgula. Conte as inversões antes de começar e confira se o total de trocas bate. Experimente também 2, 3, 4, 5, 1 e 5, 1, 2, 3, 4 e veja as “tartarugas” e “lebres” do exercício 4.
Par comparado
Posição final garantida
Implementação em C
Funções: copie para um programa que tenha main
void bubble_sort(int v[], int n) {
for (int fim = n - 1; fim > 0; fim--) { // depois de fim, tudo está pronto
int trocou = 0;
for (int j = 0; j < fim; j++) { // compara v[j] com v[j+1]
if (v[j] > v[j + 1]) { // > estrito: iguais não trocam
int temp = v[j];
v[j] = v[j + 1];
v[j + 1] = temp;
trocou = 1;
}
}
if (!trocou) break; // passagem sem trocas: acabou
}
}
Para usar: declare int v[] = {5, 2, 4, 1}; e chame bubble_sort(v, 4);. O resultado é [1, 2, 4, 5], com as 6 comparações e 5 trocas da figura.
Exemplo real: classificação de um concurso
Um concurso publica a classificação por nota, da maior para a menor. O edital diz que, em caso de empate, fica à frente quem se inscreveu primeiro. O vetor já chega em ordem de inscrição. Basta ordenar por nota com o cuidado certo:
Programa completo: compile e execute
#include <stdio.h>
typedef struct {
char nome[20];
float nota;
} Candidato;
// Maior nota primeiro. Em empate, quem se inscreveu antes continua na frente.
void classificar(Candidato c[], int n) {
for (int fim = n - 1; fim > 0; fim--) {
int trocou = 0;
for (int j = 0; j < fim; j++) {
if (c[j].nota < c[j + 1].nota) { // < estrito: empatados não trocam
Candidato temp = c[j]; // troca o registro inteiro
c[j] = c[j + 1];
c[j + 1] = temp;
trocou = 1;
}
}
if (!trocou) break;
}
}
int main(void) {
Candidato c[] = { // em ordem de inscrição
{"Ana", 8.5f}, {"Bia", 7.0f}, {"Caio", 8.5f}, {"Davi", 9.0f}
};
int n = sizeof c / sizeof c[0];
classificar(c, n);
for (int i = 0; i < n; i++) {
printf("%d. %s (%.1f)\n", i + 1, c[i].nome, c[i].nota);
}
return 0;
}
Ana e Caio empataram com 8.5. Ana se inscreveu antes e ficou à frente, como manda o edital. Isso só acontece por causa de um detalhe: o sinal < estrito.
Estabilidade: por que um sinal muda tudo
Um algoritmo de ordenação é estável quando elementos empatados mantêm entre si a ordem em que chegaram. O bubble sort é estável porque só troca vizinhos quando estão estritamente fora de ordem. Dois empatados nunca trocam, então nunca se ultrapassam. Basta trocar < por <= para perder essa garantia:
As duas versões ordenam por nota corretamente. Só a versão com < respeita o critério de desempate do edital. Em um concurso real, a versão com <= geraria recurso na justiça.
Estabilidade importa sempre que a ordem de chegada carrega informação: inscrição, horário do pedido, posição em uma lista anterior. Também permite ordenar por vários critérios em etapas: ordene primeiro por nome e depois, de forma estável, por nota. O resultado fica por nota e, nos empates, por nome.
Armadilhas frequentes
j <= fim compara v[fim] com v[fim + 1]. Na primeira passagem, isso lê fora do vetor e pode até trazer lixo para dentro dele.
Declarar trocou = 0 fora do laço externo faz a parada antecipada nunca mais funcionar depois da primeira troca.
Usar >= (ou <=) troca elementos iguais: o resultado continua ordenado, mas perde a estabilidade e faz trocas inúteis.
Trocar só o campo da nota, como c[j].nota no exemplo do concurso, separa a nota do nome. Troque a struct inteira.
Quanto custa na prática?
Medimos o bubble sort desta aula com três tipos de entrada:
Medições reais: comparações, trocas e tempo
Elementos
Já ordenado
Aleatório
Ordem decrescente
1 000
999 comparações, 0 trocas, < 0,1 ms
499 mil comparações, 250 mil trocas, 1,1 ms
499 500 comparações e trocas, 0,5 ms
10 000
9 999 comparações, 0 trocas, < 0,1 ms
50 milhões de comparações, 0,11 s
50 milhões, 0,06 s
50 000
49 999 comparações, 0 trocas, < 0,1 ms
1,25 bilhão de comparações, 2,9 s
1,25 bilhão, 1,4 s
Os números da coluna decrescente seguem exatamente a fórmula n(n−1)/2: 1 000 × 999 / 2 = 499 500, e 50 000 × 49 999 / 2 ≈ 1,25 bilhão.
Três lições saem dessa tabela:
Lista já ordenada: uma passagem e pronto. Graças à parada antecipada, 50 mil elementos ordenados custam só 49 999 comparações.
Lista bagunçada: o custo cresce com o quadrado. Com 5 vezes mais elementos (10 mil para 50 mil), o tempo aumentou cerca de 26 vezes, perto dos 25 que o quadrado prevê. Com 1 milhão de elementos, a estimativa é de uns 20 minutos. No pior caso, são n(n−1)/2 comparações: cada elemento é comparado com praticamente todos os outros.
Contar operações não conta tudo. O vetor aleatório fez menos trocas que o decrescente e mesmo assim foi mais lento. O número de operações não conta toda a história do tempo.
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
Na prática, o bubble sort raramente é a melhor escolha: o insertion sort, da próxima aula, faz o mesmo serviço movendo menos dados. O valor do bubble sort está no que ele ensina: invariantes, inversões e estabilidade.
Exercícios progressivos
Use as figuras da aula como modelo: desenhe o vetor e marque o par comparado em cada etapa. A dica não entrega a resposta: ela indica a pergunta certa a fazer.
1. A pilha de provas Básico
Uma professora organiza as provas pelo número de chamada. A pilha está com os números [5, 2, 4], e ela segue o bubble sort à mão. Mostre como a pilha fica depois de cada comparação da primeira passagem. Qual número está garantido no lugar ao final dela?
Ver uma dica
Uma passagem compara os pares (posição 0, posição 1) e depois (posição 1, posição 2). Compare sempre com o vetor atualizado, e não com o original. Qual valor vai sendo “carregado” para a direita?
Conferir a resposta
Compara 5 e 2: 5 > 2, troca. A pilha fica [2, 5, 4].
Compara 5 e 4: 5 > 4, troca. A pilha fica [2, 4, 5].
O 5, o maior, foi carregado até o fim e está garantido no lugar. Neste caso, o vetor já ficou ordenado. A segunda passagem compara 2 e 4, não troca nada e encerra pela parada antecipada.
2. A nota fantasma Básico
Um colega mudou o laço interno para for (int j = 0; j <= fim; j++). Às vezes o programa funciona. Às vezes, o ranking aparece com uma nota que nenhum aluno tirou, e um aluno desaparece. Explique o que está acontecendo.
Ver uma dica
Na primeira passagem, fim vale n − 1. Qual é o último valor de j com <=? Qual posição a comparação acessa nesse momento? Ela existe no vetor?
Conferir a resposta
Com j <= fim, o último j da primeira passagem é n − 1, e a comparação lê v[n]: a posição logo após o fim do vetor. Ali há qualquer coisa que estiver na memória (lixo).
Acessar fora do vetor é comportamento indefinido: a linguagem não garante nada sobre o que acontece. Um cenário comum, que explica o sintoma: se o lixo, comparado com o último aluno, parecer “fora de ordem”, o if manda trocar. O lixo entra no vetor como uma nota fantasma, e o registro verdadeiro vai para fora do vetor e some. A escrita em v[n] também pode corromper outra variável ou derrubar o programa. Por isso o bug é intermitente: depende do que está na memória naquele instante.
Regra: se o corpo do laço acessa v[j + 1], o laço precisa parar em j < fim, para que j + 1 nunca passe de fim.
3. Prever sem simular Intermediário
O juiz de uma maratona de programação quer saber quantas trocas o bubble sort fará em duas listas, mas sem executar o algoritmo: [3, 1, 2] e [4, 3, 2, 1]. Responda contando inversões, como na figura dos arcos.
Ver uma dica
Para cada elemento, conte quantos elementos menores aparecem à direita dele. Some tudo. Por que essa soma é igual ao número de arcos?
Conferir a resposta
[3, 1, 2]: o 3 tem dois menores à direita (1 e 2), e o 1 não tem nenhum. Total: 2 trocas.
[4, 3, 2, 1]: o 4 tem 3 menores à direita, o 3 tem 2, o 2 tem 1. Total: 3 + 2 + 1 = 6 trocas. Todos os pares estão invertidos, o pior caso possível para 4 elementos.
Contar “menores à direita” de cada elemento lista cada arco exatamente uma vez, pelo seu lado esquerdo. Por isso a soma é o número de inversões.
4. Lebres e tartarugas Intermediário
Uma planilha de vendas já estava ordenada, mas um item novo foi colocado no lugar errado. Para cada caso, quantas passagens e quantas comparações o bubble sort (com parada antecipada) faz?
[1, 2, 3, 5, 4]: dois vizinhos trocados no final.
[5, 1, 2, 3, 4]: o maior foi parar no início.
[2, 3, 4, 5, 1]: o menor foi parar no fim.
Os casos 2 e 3 têm o mesmo número de inversões. Por que um custa mais que o outro?
Ver uma dica
Em uma passagem, quantas posições um valor grande consegue andar para a direita? E quantas posições um valor pequeno consegue andar para a esquerda? Lembre que, com a parada antecipada, uma passagem sem trocas encerra o algoritmo. Se todas as n − 1 passagens trocarem algo, não há essa passagem extra.
Conferir a resposta
Caso
Passagens
Comparações
Trocas
[1, 2, 3, 5, 4]
2
4 + 3 = 7
1
[5, 1, 2, 3, 4]
2
4 + 3 = 7
4
[2, 3, 4, 5, 1]
4
4 + 3 + 2 + 1 = 10
4
Um valor grande é carregado pelas trocas sucessivas e pode atravessar o vetor inteiro em uma passagem: é uma lebre. Um valor pequeno anda só uma posição para a esquerda por passagem: é uma tartaruga. No caso 3, o 1 precisa de 4 passagens para chegar ao início.
Por isso, o bubble sort é rápido quando um item fora do lugar é grande e lento quando é pequeno. Uma variante chamada cocktail sort alterna passagens da esquerda para a direita e da direita para a esquerda, justamente para mover as tartarugas mais depressa.
5. Detector de anagramas Avançado
Um jogo de palavras precisa verificar se duas palavras são anagramas: usam exatamente as mesmas letras, em outra ordem. Por exemplo, “amor” e “roma”, ou “pedra” e “padre”. Escreva int anagramas(const char *a, const char *b), que retorna 1 se forem anagramas, usando o bubble sort para ordenar as letras. Considere palavras em minúsculas, sem acentos e com menos de 50 letras.
O que acontece com “amor” e com “roma” se você colocar as letras de cada uma em ordem alfabética? Um char é um número pequeno em C, então o bubble sort funciona com ele sem mudança na lógica. Atenção: os parâmetros são const char *, e você não pode alterar as palavras originais. Onde vai ordenar?
Conferir a resposta
Funções: copie para um programa que tenha main
#include <string.h>
void ordenar_letras(char s[]) {
int n = (int)strlen(s);
for (int fim = n - 1; fim > 0; fim--) {
int trocou = 0;
for (int j = 0; j < fim; j++) {
if (s[j] > s[j + 1]) { // compara os códigos das letras
char temp = s[j];
s[j] = s[j + 1];
s[j + 1] = temp;
trocou = 1;
}
}
if (!trocou) break;
}
}
int anagramas(const char *a, const char *b) {
char x[50], y[50];
if (strlen(a) != strlen(b) || strlen(a) >= sizeof x) return 0;
strcpy(x, a); // copia: as originais não podem ser alteradas
strcpy(y, b);
ordenar_letras(x); // "amor" -> "amor", "roma" -> "amor"
ordenar_letras(y);
return strcmp(x, y) == 0;
}
Ordenar as letras transforma qualquer anagrama na mesma “assinatura”: “amor” e “roma” viram “amor”, e “pedra” e “padre” viram “adepr”. Basta comparar as assinaturas com strcmp.
Por que copiar? Uma chamada como anagramas("amor", "roma") passa textos literais, que ficam em memória somente leitura. Tentar ordená-los diretamente é comportamento indefinido e costuma derrubar o programa. Se você compilar com o MSVC, ele mostra o aviso C4996 em strcpy, recomendando uma versão própria da Microsoft. Aqui o aviso pode ser ignorado com segurança, porque o tamanho é verificado antes da cópia. O teste de tamanho no início descarta palavras de tamanhos diferentes e protege as cópias contra overflow.
./desafio --nivel=hacker
Desafio hacker: o juiz que conta trocas
Tipo: opcional
Dificuldade: ★★★★
Tema: inversões e dividir para conquistar
Limite: n ≤ 200 000, 1 segundo
Um juiz online quer saber quantas trocas o bubble sort faria em um vetor, mas não pode simular o algoritmo. Com n = 200 000, a simulação levaria cerca de 2 × 10¹⁰ operações. Para um vetor aleatório, extrapolando a tabela de medições (16 vezes o tempo de 50 mil), seriam uns 45 segundos. Para o vetor decrescente do exemplo, uns 22. O limite é 1 segundo.
Atenção: este desafio vai além da aula. Ele usa a ideia do merge sort, um algoritmo de ordenação mais rápido que você verá em detalhes mais adiante no curso. As dicas levam até lá passo a passo.
Escreva long long contar_inversoes(int v[], int n), que conte as inversões sem comparar todos os pares. Contrato: a função pode deixar o vetor reordenado, pode usar um vetor auxiliar de n posições e deve retornar −1 se faltar memória.
Entrada[5, 2, 4, 1]
Saída5
Entrada200 000, 199 999, …, 1
Saída19999900000
Dica 1: por que long long?
Com 200 000 elementos em ordem decrescente, todos os pares estão invertidos: são quase 20 bilhões. Um int de 32 bits vai só até cerca de 2,1 bilhões. A resposta precisa de um inteiro de 64 bits.
Dica 2: divida ao meio
Corte o vetor ao meio. Toda inversão é de um destes tipos: os dois elementos estão na metade esquerda, os dois estão na metade direita, ou um está em cada metade. Os dois primeiros tipos são o mesmo problema, menor: use recursão. Falta descobrir como contar o terceiro tipo depressa.
Dica 3: intercalar conta de graça
Se as duas metades estiverem ordenadas, junte-as em uma só lista ordenada, escolhendo sempre o menor dos dois primeiros, como quem junta duas pilhas de provas já ordenadas. Quando o escolhido vier da direita, o que você sabe sobre ele em relação aos elementos que ainda restam na esquerda?
Solução comentada
A figura mostra a ideia com o exemplo [5, 2, 4, 1]:
As inversões dentro de cada metade são contadas pela recursão. As inversões entre as metades são contadas ao intercalar: cada elemento que vem da direita “pula” todos os que restam na esquerda.
Funções: copie para um programa que tenha main
#include <stdlib.h>
// Conta inversões em v[ini..fim) e deixa o trecho ordenado.
// fim é exclusivo: contar(v, tmp, 0, 4) olha v[0], v[1], v[2] e v[3].
long long contar(int v[], int tmp[], int ini, int fim) {
if (fim - ini < 2) return 0; // 0 ou 1 elemento: nenhuma inversão
int meio = ini + (fim - ini) / 2;
long long inv = contar(v, tmp, ini, meio) // inversões só na esquerda
+ contar(v, tmp, meio, fim); // inversões só na direita
int i = ini, j = meio, k = ini; // intercalação
while (i < meio && j < fim) {
if (v[i] <= v[j]) { // <= : iguais NÃO são inversão
tmp[k++] = v[i++];
} else {
inv += meio - i; // v[j] é menor que todos de v[i..meio-1]
tmp[k++] = v[j++];
}
}
while (i < meio) tmp[k++] = v[i++];
while (j < fim) tmp[k++] = v[j++];
for (k = ini; k < fim; k++) v[k] = tmp[k];
return inv;
}
long long contar_inversoes(int v[], int n) {
if (n < 2) return 0;
int *tmp = malloc(n * sizeof *tmp);
if (tmp == NULL) return -1;
long long r = contar(v, tmp, 0, n);
free(tmp);
return r; // atenção: v termina ordenado
}
Por que funciona. Durante a intercalação, as duas metades estão ordenadas. Se v[j], da direita, é menor que v[i], ele também é menor que todos os que vêm depois de v[i] na esquerda, pois esses são ainda maiores. Assim, meio − i inversões são contadas de uma só vez, sem olhar par por par.
Por que é rápido. O vetor é cortado ao meio cerca de 18 vezes até sobrar um elemento (200 000 → 100 000 → … → 1). Em cada um desses níveis, cada elemento é intercalado uma única vez. Isso dá uma ordem de grandeza de 18 × 200 000 ≈ 3,6 milhões de passos de intercalação. Cada passo envolve uma comparação e algumas cópias, contra 20 bilhões de comparações da simulação: milissegundos em vez de dezenas de segundos. A memória extra é o vetor temporário de n posições, mais a pilha da recursão, com cerca de 18 níveis. Quem chama deve verificar o retorno −1, que indica falta de memória. Esse “cortar ao meio e juntar” é o algoritmo merge sort, e o custo dele é chamado de O(n log n).
Detalhes que derrubam soluções: usar < em vez de <= conta elementos iguais como inversões, mas o bubble sort não troca iguais. Usar int no acumulador estoura. E a função altera o vetor: se precisar do original, passe uma cópia.
Como testar: compare com uma força bruta que verifica todos os pares, em milhares de vetores pequenos e aleatórios, com valores repetidos. Nesta página, os dois métodos coincidiram em 3000 vetores, e o vetor decrescente de 200 000 elementos deu exatamente 19 999 900 000.
Revisão rápida
Cada passagem leva o maior valor restante para o fim. Depois de k passagens, as k últimas posições estão prontas.
Uma passagem sem trocas encerra o algoritmo: lista ordenada custa uma passagem só.
Trocas = inversões. Contar os pares fora de ordem prevê o trabalho.
O < estrito na ordenação decrescente, e o > estrito na crescente, tornam a ordenação estável: como iguais nunca são trocados, empatados mantêm a ordem de chegada.
Valores grandes andam rápido (lebres), e valores pequenos andam uma casa por passagem (tartarugas).
Para listas grandes e bagunçadas, o custo explode. Use algoritmos melhores, como o merge sort do desafio.