Sorting Wars: Benchmarking Selection, Insertion, Gnome e Bubble Sort em 5 Linguagens

posted Originally published at dev.to 6 min read

Algoritmos quadráticos são lentos — isso todo mundo sabe. Mas o quão lentos? E a linguagem escolhida importa mais do que o algoritmo em si? Foi exatamente isso que nossa turma de Algoritmos e Estruturas de Dados I do CEFET-MG decidiu testar empiricamente.

Implementamos Selection Sort, Insertion Sort, Gnome Sort e Bubble Sort em C, C++, Rust, JavaScript e Python, rodamos benchmarks com até 1 milhão de elementos e coletamos os dados. Esse post resume o que encontramos — incluindo alguns resultados que nos surpreenderam bastante.


Por que estudar algoritmos O(n²) em 2025?

Antes de mergulhar nos números, vale responder a pergunta óbvia. Três razões principais:

  1. Didática — a lógica é direta, ideal para aprender análise de complexidade e invariantes de laço.
  2. Entradas pequenas — para N ≤ ~50, o overhead reduzido pode torná-los mais rápidos que algoritmos sofisticados.
  3. Adaptatividade — Insertion Sort e Bubble Sort com parada antecipada chegam a O(n) em entradas quase ordenadas. É por isso que o TimSort (usado em Python e Java) usa Insertion Sort internamente para subconjuntos pequenos.

Os Algoritmos

Selection Sort — O inflexível

Seleciona o mínimo do subarray não ordenado e o move para a posição correta com uma única troca.

PARA i DE 0 ATE N - 2 FACA
  MIN_IDX <- i
  PARA j DE i + 1 ATE N - 1 FACA
    SE VETOR[j] < VETOR[MIN_IDX] ENTAO
      MIN_IDX <- j
  SE MIN_IDX != i ENTAO
    TROCA VETOR[i] COM VETOR[MIN_IDX]

Característica marcante: sempre realiza exatamente n(n-1)/2 comparações, independente da entrada. Não existe melhor caso em comparações — é Θ(n²) em todos os cenários.

Caso Comparações Complexidade
Melhor (ordenado) n(n−1)/2 O(n²)
Médio (aleatório) n(n−1)/2 O(n²)
Pior (invertido) n(n−1)/2 O(n²)

✅ In-place · ❌ Não estável · ❌ Não adaptativo


Insertion Sort — O adaptativo

Mantém um subarray ordenado à esquerda e insere cada novo elemento na posição correta via shifting.

PARA i DE 1 ATE N - 1 FACA
  CHAVE <- VETOR[i]
  j <- i - 1
  ENQUANTO j >= 0 E VETOR[j] > CHAVE FACA
    VETOR[j + 1] <- VETOR[j]
    j <- j - 1
  VETOR[j + 1] <- CHAVE

Em entradas já ordenadas, o laço interno nunca executa: complexidade O(n) no melhor caso. Isso ficou claro nos nossos dados — até 10⁶ elementos, o cenário crescente terminou em microssegundos.

Caso Comparações Complexidade
Melhor (ordenado) n − 1 O(n)
Médio (aleatório) n(n−1)/4 O(n²)
Pior (invertido) n(n−1)/2 O(n²)

✅ In-place · ✅ Estável · ✅ Adaptativo


Gnome Sort — O excêntrico

Opera como um "gnomo organizando vasos": avança comparando adjacentes, e ao encontrar desordem, troca e recua uma posição.

INDICE <- 0
ENQUANTO INDICE < N FACA
  SE INDICE = 0 OU VETOR[INDICE] >= VETOR[INDICE - 1] ENTAO
    INDICE <- INDICE + 1
  SENAO
    TROCA VETOR[INDICE] COM VETOR[INDICE - 1]
    INDICE <- INDICE - 1

Funcionalmente equivalente a um Insertion Sort por trocas — mas com movimento local em vez de shifting. Também adaptativo: O(n) no melhor caso.

Caso Comparações Complexidade
Melhor (ordenado) n − 1 O(n)
Médio (aleatório) (A DEPENDER)O(n²)
Pior (invertido) n(n−1)/2 O(n²)

✅ In-place · ✅ Estável · ✅ Adaptativo


Bubble Sort — O clássico (com parada antecipada)

Realiza passagens comparando pares adjacentes. A variável swapped permite encerramento antecipado quando o vetor já está ordenado.

PARA i DE 0 ATE N - 1 FACA
  SWAPPED <- FALSO
  PARA j DE 0 ATE N - i - 2 FACA
    SE VETOR[j] > VETOR[j + 1] ENTAO
      TROCA VETOR[j] COM VETOR[j+1]
      SWAPPED <- VERDADEIRO
  SE SWAPPED = FALSO ENTAO
    RETORNE  // Encerramento antecipado
Caso Comparações Complexidade
Melhor (ordenado) n − 1 O(n)
Médio (aleatório) n(n−1)/4 O(n²)
Pior (invertido) n(n−1)/2 O(n²)

✅ In-place · ✅ Estável · ✅ Adaptativo (com parada antecipada)


Metodologia

  • Tamanhos de entrada: N ∈ { 10², 10³, 10⁴, 10⁵, 10⁶ }
  • Cenários: crescente (melhor caso), aleatório (caso médio), decrescente (pior caso)
  • Repetições: 5 execuções; média das 3 centrais (descartando menor e maior)
  • Otimizações testadas: -O0 e -O3 para C/C++/Rust
  • Medição: apenas tempo de ordenação (I/O descartado)

Cada linguagem usou sua estrutura de dados nativa:

Linguagem Estrutura Tipo
C double[] com mallocdouble 64-bit
C++ std::vector<double> double 64-bit
Rust Vec<f64> f64 64-bit
JavaScript Array (V8 float64) Number
Python list (refs a PyObject)float

Resultados — Selection Sort

O Selection Sort foi o mais previsível: tempos quase idênticos nos três cenários, confirmando a não-adaptatividade.

N = 10⁵ — tempo em segundos (-O3 / otimização máxima)

Linguagem Crescente (s) Aleatório (s) Decrescente (s)
C (-O3) 2.0500 3.5188 1.9618
C++ (-O3) 2.1247 3.5188 1.9089
Rust (-O3) 2.3265 4.2560 2.1320
JavaScript (V8) 3.5835 6.6564 3.5851
Python 93.164 164.890 98.378

Python para N = 10⁶ foi descartado — tempo estimado superior a 10 horas.

Hierarquia: C -O3 ≈ C++ -O3 < Rust < JavaScript ≪ Python


Resultados — Insertion Sort

Aqui a adaptatividade brilhou. No cenário crescente com N = 10⁶, todas as linguagens terminaram em menos de 12 ms. Já no cenário decrescente, o pior caso quadrático foi brutal:

N = 10⁵ — tempo em segundos (-O3 / otimização máxima)

Linguagem Crescente (s) Aleatório (s) Decrescente (s)
C (-O3) 0.000055 0.615683 1.188966
C++ (-O3) 0.0000634 0.587483 1.183000
Rust (-O3) 0.000079 0.897895 1.774292
JavaScript (V8) 0.000809 1.525250 3.033301
Python 0.005321 136.295 226.560

Para N = 10⁶ no pior caso sem otimização, C++ chegou a 3670 s (~1 hora). Com -O3, caiu para 119 s — speedup de ~30×.


Resultados — Gnome Sort

O Gnome Sort trouxe a maior surpresa da série: Rust superou C e C++ no caso médio.

N = 10⁵ — tempo em segundos (-O3 / otimização máxima)

Linguagem Crescente (s) Aleatório (s) Decrescente (s)
C (-O3) 0.000179 15.19068 28.90668
C++ (-O3) 0.000159 13.96807 28.25787
Rust (-O3) 0.000049 3.94810 8.27646
JavaScript (V8) 0.001584 13.90908 27.45163
Python 0.019030 21.28324 257.00624

Por quê? O padrão de acesso do Gnome Sort — muitos acessos locais e trocas adjacentes — se beneficia especialmente das otimizações do compilador LLVM (backend do Rust), que gera código de swap em registrador sem acesso intermediário à memória.


Resultados — Bubble Sort

O Bubble Sort entregou o resultado mais inesperado de todo o projeto:

N = 10⁵ — tempo em segundos (-O3 / otimização máxima)

Linguagem Crescente (s) Aleatório (s) Decrescente (s)
C (-O3) 0.000042 20.489569 23.568626
C++ (-O3) 0.000045 21.165759 23.919268
Rust (-O3) 0.000051 13.513529 9.231813
JavaScript (V8) 0.001753 15.012168 6.905683
Python 0.003504 312.509064 403.412438

JavaScript bateu C e C++ no pior caso. O motor V8 identificou o padrão repetitivo de trocas adjacentes e gerou código de máquina altamente especializado via JIT. É um caso raro onde a compilação dinâmica supera a compilação estática.


Principais Aprendizados

1. Otimização de compilador importa muito mais do que parece

O impacto de -O3 vs -O0 para Rust no Bubble Sort chegou a 23× para N = 10⁴. Em modo debug, o Rust mantém verificações de bounds checking em todo acesso ao array — correto para desenvolvimento, devastador para performance.

2. Adaptatividade é real e mensurável

Insertion Sort com N = 10⁶ no cenário crescente: < 1 ms. No cenário decrescente: até 3670 s (sem otimização). Isso é uma diferença de mais de 10 ordens de magnitude no número de operações.

3. Python tem overhead estrutural, não só de interpretador

A list do Python armazena referências a objetos PyObject — cada acesso e comparação envolve uma indireção de ponteiro. Isso multiplica o custo por operação mesmo para tipos simples como int e float.

4. JIT pode superar código estático em padrões repetitivos

O V8 observa o comportamento em runtime e especializa o código gerado. Para algoritmos com padrões de acesso altamente previsíveis (como Bubble Sort), isso pode ser mais eficiente que otimizações estáticas conservadoras de um compilador AOT.

5. A hierarquia não é fixa

A ordem geral foi C ≈ C++ < Rust < JS < Python, mas exceções importantes existem — Rust venceu todos no Gnome Sort, JS venceu todos no Bubble Sort em pior caso. O melhor resultado depende do algoritmo, tamanho de entrada e padrão de acesso.


Conclusão

Algoritmos simples ensinam mais do que complexidade assintótica. Ensinam sobre a relação entre padrões de acesso à memória e otimizações de compilador, sobre como compilação JIT funciona na prática, e sobre por que constantes importam tanto quanto expoentes para tamanhos reais de entrada.

Se você quiser explorar o código completo, estrutura de Makefiles, geradores de dataset e scripts de benchmark, o repositório está público no GitHub.

{% embed https://github.com/Bernardo-Lebron/simple_sort_algorithms %}


Trabalho prático da disciplina de Algoritmos e Estruturas de Dados I — CEFET-MG Campus V, Divinópolis. Orientação: Prof. Michel Pires.

More Posts

5 Web Dev Pitfalls That Are Silently Killing Your Projects (With Real Fixes)

Dharanidharan - Mar 3

How Insertion Sort Works: Simplified Explanation

Saptarshi Sarkar - Nov 29, 2025

The Making of a 10x Engineer in AI Era: Why Big-O Thinking Changes Everything

Tito - May 11

**Cuckoo Hashing**, a powerful hashing technique that takes its inspiration straight from nature.

m13ha - Aug 27, 2025

Google Drive Sync

Pocket Portfolioverified - Jan 5
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

2 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!