Ordenação e estabilidade

Traçar ordenações, provar a intercalação e distinguir garantias de pior caso e custos esperados.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

Ordenar [6,2,5,1] produz [1,2,5,6]. A saída respeita a comparação crescente e conserva os mesmos elementos, incluindo repetidos.

Se só procuras o máximo, um percurso chega. Se vais fazer muitas pesquisas binárias, pagar a ordenação pode compensar. A decisão depende do uso posterior, não apenas do tamanho.

A regra que decide qual de dois elementos vem primeiro chama-se comparador. Para registos com a mesma chave, importa também saber se a ordenação conserva a sua ordem inicial.

Ordenações elementares

Uma ordenação é estável quando conserva a ordem relativa de elementos com chaves equivalentes. A seleção pode trocar elementos distantes e inverter empates. A inserção e a bubble conservam-nos quando deslocam ou trocam apenas elementos estritamente fora de ordem.

Na seleção, antes de tratares a posição i, o prefixo [0,i) já contém os i menores elementos nas posições finais. Procura o mínimo no sufixo e troca-o com v[i].

Faz n(n−1)/2n(n-1)/2 comparações em qualquer entrada, ou seja, Θ(n2)\Theta(n^2). Usa apenas O(n)O(n) trocas. Não é estável: em [2A,2B,1], a primeira troca produz [1,2B,2A].

Na inserção, o prefixo [0,i) está ordenado e contém os elementos originais desse prefixo. Guarda v[i], desloca para a direita os elementos estritamente maiores e insere o valor no espaço aberto.

[4,2,3] passa por [2,4,3] e [2,3,4]. Deslocar apenas os estritamente maiores preserva a ordem dos empates. O número de deslocações é o número de inversões, ou seja, pares fora de ordem. O custo é Θ(n+I)\Theta(n+I), com I inversões. Uma entrada ordenada dá linear; uma inversa dá quadrático.

Na bubble sort, compara vizinhos e troca quando estão invertidos. Uma passagem leva o máximo do intervalo para o fim. Sem uma condição de paragem, continua quadrática mesmo se a entrada já estiver ordenada. Com um indicador de ausência de trocas, o melhor caso é linear. Trocar só elementos estritamente fora de ordem torna-a estável.

Mergesort e prova do merge

Divide [l,r) em [l,m) e [m,r), ordena cada metade e intercala-as. Durante a intercalação, o prefixo auxiliar já escrito contém os menores elementos da entrada da junção, por ordem; i e j apontam para o primeiro elemento não consumido de cada metade. O menor dos dois candidatos é o menor de todos os elementos restantes, porque cada metade está ordenada. Escrevê-lo preserva o invariante. Quando uma metade acaba, copia a outra.

Traça [6,2,5,1]: as metades ordenam para [2,6] e [1,5]. O merge escolhe 1, depois 2, depois 5 e copia 6. Faz três comparações nesta junção; as duas junções inferiores fizeram uma cada, dando cinco no total.

  1. Metades ordenadas
    2, 61, 5
    1 é menor que 2, por isso sai da metade direita.
  2. Depois de escrever 1
    2, 65saída: 1
    2 é menor que 5, depois 5 é menor que 6.
  3. Junção concluída
    1256
    Três comparações nesta junção; cinco no mergesort completo de quatro valores.
Na junção só comparamos as frentes ainda disponíveis. Copiar o último 6 não exige outra comparação de chaves.
Mergesort com valores repetidos
#include <algorithm>
#include <iostream>
#include <vector>
void mergeSort(std::vector<int>& a, std::vector<int>& t,
               std::size_t l, std::size_t r) {
    if (r - l < 2) return;
    const auto m = l + (r - l) / 2;
    mergeSort(a, t, l, m);
    mergeSort(a, t, m, r);
    auto i = l, j = m, k = l;
    while (i < m && j < r)
        t[k++] = a[j] < a[i] ? a[j++] : a[i++];
    while (i < m) t[k++] = a[i++];
    while (j < r) t[k++] = a[j++];
    for (k = l; k < r; ++k) a[k] = t[k];
}
int main() {
    std::vector<int> a{5, 2, 7, 1, 6, 3, 4, 2}, t(a.size());
    auto esperado = a;
    std::sort(esperado.begin(), esperado.end());
    mergeSort(a, t, 0, a.size());
    for (std::size_t i = 0; i < a.size(); ++i)
        std::cout << (i ? " " : "") << a[i];
    std::cout << '\n';
    std::cout << "igual a sort: " << (a == esperado) << '\n';
}
Resultado

Executa o código para ver a saída e os gráficos.

Na igualdade, o programa escolhe o elemento da esquerda e preserva a estabilidade. A divisão termina quando o intervalo tem menos de dois elementos, incluindo o vetor vazio. O vetor auxiliar é criado uma vez e reutilizado. A referência com std::sort parte de uma cópia da entrada original e verifica ordem e preservação dos valores.

A recorrência é T(n)=2T(n/2)+Θ(n)T(n)=2T(n/2)+\Theta(n), logo Θ(nlog⁡n)\Theta(n\log n) em melhor, médio e pior caso nesta implementação. O auxiliar ocupa Θ(n)\Theta(n) e a pilha O(log⁡n)O(\log n). Criar um novo vetor por chamada aumenta alocações, mas não obriga a manter o trabalho de todos os níveis em memória ao mesmo tempo.

Mergesort em listas ligadas

Numa lista simplesmente ligada não tens acesso direto ao meio, mas podes encontrá-lo por um percurso. Separar e intercalar por ligações permite ordenar sem copiar valores nem criar nós auxiliares:

  1. Com zero ou um nó, termina.
  2. Separa os primeiros n/2 nós dos restantes numa lista inicialmente vazia.
  3. Ordena recursivamente as duas listas.
  4. Intercala-as, escolhendo a esquerda no empate e esvaziando a lista temporária.

Em [4A,1B,4C,2D,4E], a primeira separação cria [4A,1B] e [4C,2D,4E]. As chamadas devolvem [1B,4A] e [2D,4C,4E]; a junção dá [1B,2D,4A,4C,4E]. Todos os nós da metade esquerda precediam os da direita na entrada. Por isso escolher a esquerda numa igualdade, juntamente com a estabilidade de cada chamada, conserva 4A,4C,4E nessa ordem.

Separar custa Θ(n)\Theta(n) neste nível e intercalar custa O(n)O(n); a recorrência é T(n)=T(⌊n/2⌋)+T(⌈n/2⌉)+Θ(n)T(n)=T(\lfloor n/2\rfloor)+T(\lceil n/2\rceil)+\Theta(n), dando Θ(nlog⁡n)\Theta(n\log n). Há O(log⁡n)O(\log n) chamadas suspensas, com um número constante de apontadores por chamada. O auxiliar é, portanto, O(log⁡n)O(\log n), sem o vetor de O(n)O(n) usado na versão anterior. A lista temporária é um objeto com cabeça e tamanho, não uma cópia de metade dos nós.

std::sort exige iteradores de acesso direto e não serve para std::list. O método lista.sort() permite ordenar uma std::list com estabilidade e O(nlog⁡n)O(n\log n) comparações. Ordenar uma lista com seleção por percursos repetidos, movendo o máximo do prefixo para o fim, também é possível, mas soma n+(n−1)+⋯+2n+(n-1)+\cdots+2 visitas e custa Θ(n2)\Theta(n^2).

Quicksort e partição

No esquema de Lomuto, para um intervalo não vazio [l,r), escolhe p=v[r-1]. Percorre j=l..r-2 e mantém [l,i) com valores menores que p e [i,j) com valores maiores ou iguais. Se v[j]<p, troca com v[i] e avança i. No fim, troca o pivô com v[i]. O pivô fica na posição final; resolve [l,i) e [i+1,r).

Em [5,2,7,1,4], o pivô é 4. Depois de processares 5, 2, 7 e 1, o vetor fica [2,1,7,5,4]; a troca final produz [2,1,4,5,7]. Foram quatro comparações com o pivô. A partição não ordena cada lado.

Se as divisões forem equilibradas, o custo é Θ(nlog⁡n)\Theta(n\log n). Se cada pivô isolar um único elemento, T(n)=T(n−1)+Θ(n)=Θ(n2)T(n)=T(n-1)+\Theta(n)=\Theta(n^2). O último elemento como pivô provoca esse caso numa sequência crescente; com a comparação estrita, todos os valores iguais também o provocam.

Com chaves distintas, escolher o pivô uniformemente ao acaso dá custo esperado O(nlog⁡n)O(n\log n), mas não elimina o pior caso. Com Lomuto e todas as chaves iguais, a aleatoriedade não muda as partições: o custo continua quadrático. Uma partição em três regiões, menores, iguais e maiores, evita repetir trabalho nos empates. A pilha do quicksort recursivo ingénuo pode ocupar Θ(n)\Theta(n) no pior caso. Recursar só sobre a região menor e iterar sobre a maior limita a pilha a O(log⁡n)O(\log n), mesmo quando o tempo é quadrático.

O esquema de Hoare aproxima dois índices e troca pares fora da região pretendida. A fronteira devolvida não tem de ser a posição final do pivô. Não combines os intervalos recursivos de Lomuto com a fronteira de Hoare.

Estabilidade e comparação válida

Ao ordenares registos primeiro pelo nome e depois pela nota com uma ordenação estável, os empates de nota continuam por nome. Podes obter a mesma ordem final com um comparador que inclua ambos os critérios.

O comparador de C++ deve definir uma ordem fraca estrita: comp(x,x) é falso; a relação é transitiva; a equivalência definida por nem comp(a,b) nem comp(b,a) também é transitiva. <= não serve. Subtrair duas chaves para decidir a ordem pode causar overflow, por isso compara-as diretamente. Não uses uma tolerância de ponto flutuante que torne a equivalência não transitiva.

std::sort garante O(nlog⁡n)O(n\log n) comparações no pior caso em C++17 e não garante estabilidade. O padrão não exige uma implementação concreta. std::stable_sort preserva os empates; com memória auxiliar suficiente faz O(nlog⁡n)O(n\log n) comparações, e sem essa memória pode usar O(nlog⁡2n)O(n\log^2 n). O custo de copiar ou comparar os registos pode acrescentar outro fator.

Limite inferior e ordenação sem comparações

Porque comparar tem um limite inferior

Para n chaves distintas, uma ordenação por comparações tem de distinguir n! permutações. A árvore de decisões binária precisa de pelo menos n! folhas, logo altura pelo menos ⌈log⁡2(n!)⌉=Ω(nlog⁡n)\lceil\log_2(n!)\rceil=\Omega(n\log n). É um limite de pior caso no modelo de comparação, não um limite para qualquer algoritmo com informação sobre as chaves.

Counting sort usa chaves inteiras em [0,k). Conta frequências, calcula prefixos das contagens e coloca cada elemento na sua posição. Para preservares empates, percorre a entrada da direita para a esquerda e reduz a posição final disponível da respetiva chave. Para [(2,A),(1,B),(2,C),(0,D)], a saída é [(0,D),(1,B),(2,A),(2,C)]. Com k=3, as frequências são [1,1,2]. As somas acumuladas [1,2,4] indicam as fronteiras depois de cada grupo de chaves. Subtrair um à fronteira dá a última posição livre dessa chave. Depois de escrever um registo, reduz a fronteira. Percorre da direita para a esquerda: (0,D) ocupa 0; (2,C) ocupa 3; (1,B) ocupa 1; (2,A) ocupa 2. Escrever da direita preserva A antes de C. Reconstruir só frequências ordena números, mas perde os outros campos dos registos.

O tempo é Θ(n+k)\Theta(n+k) e o espaço auxiliar da versão estável é Θ(n+k)\Theta(n+k). Um intervalo enorme de chaves pode tornar esta escolha pior que comparar. Radix sort por dígitos do menos significativo para o mais significativo exige uma ordenação estável em cada passagem; isso conserva a ordem dos dígitos já processados. Com [170,45,75,90,2] e base 10, cada passagem lê a sequência produzida pela anterior:

DígitoEntradaDígitos lidosSaída estável
Unidades[170,45,75,90,2][0,5,5,0,2][170,90,2,45,75]
Dezenas[170,90,2,45,75][7,9,0,4,7][2,45,170,75,90]
Centenas[2,45,170,75,90][0,0,1,0,0][2,45,75,90,170]

Nas dezenas, 170 e 75 têm o mesmo dígito 7. A estabilidade conserva 170 antes de 75, mantendo a ordem das unidades, 0 antes de 5. Nas centenas, os valores com dígito 0 conservam a ordem dos dois dígitos inferiores.

Depois de d passagens, a ordem dos d dígitos processados está correta; o custo é Θ(d(n+b))\Theta(d(n+b)) com base b. Chaves negativas e comprimentos diferentes exigem uma representação e uma política explícitas.

AlgoritmoMelhorPiorAuxiliar habitualEstável nesta versão?
SeleçãoΘ(n2)\Theta(n^2)Θ(n2)\Theta(n^2)O(1)O(1)não
InserçãoΘ(n)\Theta(n)Θ(n2)\Theta(n^2)O(1)O(1)sim
Bubble com paragemΘ(n)\Theta(n)Θ(n2)\Theta(n^2)O(1)O(1)sim
Mergesort em vetorΘ(nlog⁡n)\Theta(n\log n)Θ(nlog⁡n)\Theta(n\log n)O(n)O(n)sim
Quicksort LomutoΘ(nlog⁡n)\Theta(n\log n)Θ(n2)\Theta(n^2)O(n)O(n) na recursão ingénuanão

Para uma visão geral de merge, quicksort e bubble sort, podes consultar Rodrigo Dias:

Abrir no YouTube

Exercícios

As justificações e os programas em C++ são de autoavaliação: compara-os com os critérios da solução. Nas questões numéricas, a verificação automática confirma apenas o valor.

Intercalar com contagem exata

Intercala [1,6,8] e [2,3,9]. Quantas comparações entre os candidatos fazes antes de uma metade acabar?

Pista

Compara apenas as frentes ainda não consumidas.

Mais uma pista

Os candidatos escolhidos são 1, 2, 3, 6 e 8; depois sobra 9.

Solução

Faz cinco comparações: 1/2, 6/2, 6/3, 6/9, 8/9. Copiar o 9 restante não compara candidatos. A saída é [1,2,3,6,8,9], porque o prefixo escrito está ordenado e contém os menores elementos da entrada desta junção.

Erros frequentes

Não contes a cópia final como comparação de chaves; não confundas esta junção com todas as chamadas do mergesort.

Voltar à explicação
Separar média e pior caso

Lomuto usa o último pivô e o teste estrito <. Analisa o quicksort de n chaves todas iguais: tamanho das partições, número total de comparações e espaço recursivo ingénuo.

Pista

Nenhum elemento passa no teste estrito.

Mais uma pista

Cada chamada deixa um lado vazio e outro com n-1 elementos.

Solução

O pivô vai para a primeira posição. As comparações somam (n-1)+(n-2)+…+1=n(n-1)/2, portanto Θ(n²). A profundidade recursiva é Θ(n). Um pivô aleatório não corrige este caso se a partição continuar a separar só um igual, porque uma partição em três regiões resolve os iguais num único nível.

Confere a tua resposta:

  • Obtive partições de tamanhos 0 e n-1 em cada chamada.
  • Somei n(n-1)/2 comparações e distingui tempo quadrático de pilha linear.
  • Expliquei por que um pivô aleatório não separa valores iguais.

Erros frequentes

Não atribuas O(log n) à pilha sem provar equilíbrio ou recursar só sobre o lado menor.

Voltar à explicação
Ordenar por dois critérios sem perder empates

Tens os registos Rui-15, Ana-12, Rui-12 e Ana-15. Ordenas primeiro pelo nome e depois pela nota crescente. Que garantia precisas para os empates de nota ficarem por nome? Dá a ordem final e um comparador que obtenha essa ordem numa única ordenação.

Pista

A segunda ordenação não deve trocar registos de nota equivalente.

Mais uma pista

Num comparador único, só comparas nomes quando as notas coincidem.

Solução

A segunda ordenação tem de ser estável, porque os empates de nota devem continuar por nome. A ordem final é Ana-12, Rui-12, Ana-15, Rui-15. A primeira ordenação só precisa de colocar os nomes por ordem; a segunda preserva essa ordem dentro de cada nota. Um comparador direto usa a.nota != b.nota ? a.nota < b.nota : a.nome < b.nome. std::sort pode usar este comparador sem estabilidade porque inclui os dois critérios.

Confere a tua resposta:

  • Obtive Ana-12, Rui-12, Ana-15, Rui-15.
  • Exigi estabilidade na segunda ordenação, por nota.
  • Dei um comparador direto por nota e depois nome, sem usar <=.

Erros frequentes

Ordenar estavelmente por nome no fim faria o nome ser o critério principal.

Voltar à explicação
Traçar frequências e prefixos

Traça o counting sort em [(2,A),(1,B),(2,C),(0,D)] com k=3. Porque percorrer da direita conserva a estabilidade?

Pista
Conta primeiro, depois coloca.
Mais uma pista

A última posição livre de cada chave desce a cada escrita.

Solução

Frequências [1,1,2]; prefixos acumulados [1,2,4]. Percorrendo da direita: D ocupa 1-1=0; C ocupa 4-1=3; B ocupa 2-1=1; A ocupa 3-1=2, porque escrever C já reduziu a fronteira da chave 2. A saída é [(0,D),(1,B),(2,A),(2,C)]. A fica antes de C porque o último registo de cada chave recebe primeiro a última posição livre. Usar estas mesmas fronteiras finais num percurso da esquerda para a direita inverteria os empates; outra versão com posições iniciais pode percorrer da esquerda e ser estável.

Confere a tua resposta:

  • Calculei frequências [1,1,2] para o exemplo.
  • Mostrei a escrita da direita para a esquerda.
  • Preservei A antes de C nos empates.

Erros frequentes

Reconstruir só frequências perde os campos dos registos.
Voltar à explicação
Ordenar uma lista com divisões de tamanhos diferentes

Uma lista contém [5A,1B,5C,3D,2E,5F,4G]. Tens separar(resto,k), que conserva os primeiros k nós, e intercalar(outra), que junta listas ordenadas e escolhe a esquerda no empate. Escreve os passos de um mergesort, dá as metades da primeira divisão e as metades já ordenadas, depois a saída final. Justifica estabilidade, tempo e espaço auxiliar. As operações alteram ligações, sem copiar os valores ou criar nós.

Solução

O caso base é n<=1. Cria uma lista vazia, separa após floor(n/2) nós, ordena recursivamente a origem e o resto, e intercala o resto na origem. A primeira divisão cria [5A,1B,5C] e [3D,2E,5F,4G]; depois de ordenar, são [1B,5A,5C] e [2E,3D,4G,5F]. A saída é [1B,2E,3D,4G,5A,5C,5F]. Escolher a esquerda no empate conserva A e C antes de F; as chamadas também conservam os empates interiores. A divisão linear e a junção linear dão T(n)=T(floor(n/2))+T(ceil(n/2))+Θ(n)=Θ(n log n). A profundidade é logarítmica e cada chamada guarda apenas um número constante de apontadores e tamanhos, por isso o auxiliar é O(log n). A lista temporária fica vazia após a junção e não liberta os nós transferidos.

Confere a tua resposta:

  • Dividi sete nós em três e quatro, sem perder o nó extra.
  • Obtive 1B,2E,3D,4G,5A,5C,5F e conservei a ordem dos três 5.
  • Compus separar, duas chamadas e intercalar, com base n<=1.
  • Justifiquei Θ(n log n) e pilha O(log n), sem vetor auxiliar.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.