O Quick Sort (ordenação rápida) é um dos algoritmos de ordenação mais utilizados na prática. Inventado por Tony Hoare em 1959, ele utiliza a estratégia de divisão e conquista: escolhe um elemento como pivô, particiona o vetor de modo que todos os menores fiquem à esquerda e os maiores à direita, e então ordena recursivamente cada partição.
O Quick Sort é o algoritmo padrão em muitas implementações de bibliotecas de ordenação — incluindo a função sort_unstable() do Rust — por ser extremamente eficiente na prática: seu desempenho médio é O(n log n) com constantes muito pequenas, e por ser in-place, usa apenas O(log n) de memória extra para a pilha de recursão. Sua principal fraqueza é o pior caso O(n²), que pode ser evitado com boas estratégias de escolha do pivô.
Como Funciona
O algoritmo segue três passos:
- Escolher pivô: selecionar um elemento do vetor como referência.
- Particionar: reorganizar o vetor para que elementos menores que o pivô fiquem à esquerda e maiores à direita.
- Recursar: aplicar Quick Sort nas duas partições.
Vetor inicial: [8, 3, 7, 1, 5, 2, 9, 4, 6]
^
pivô = 6
=== PARTICIONAMENTO ===
Objetivo: colocar todos os < 6 à esquerda e todos os > 6 à direita
[8, 3, 7, 1, 5, 2, 9, 4, 6]
i→ pivô
Passo 1: 8 > 6 → não move i
Passo 2: 3 < 6 → troca com posição i → [3, 8, 7, 1, 5, 2, 9, 4, 6], i avança
Passo 3: 7 > 6 → não move i
Passo 4: 1 < 6 → troca → [3, 1, 7, 8, 5, 2, 9, 4, 6], i avança
Passo 5: 5 < 6 → troca → [3, 1, 5, 8, 7, 2, 9, 4, 6], i avança
Passo 6: 2 < 6 → troca → [3, 1, 5, 2, 7, 8, 9, 4, 6], i avança
Passo 7: 9 > 6 → não move i
Passo 8: 4 < 6 → troca → [3, 1, 5, 2, 4, 8, 9, 7, 6], i avança
Colocar pivô na posição i: [3, 1, 5, 2, 4, 6, 9, 7, 8]
|--- < 6 ---| ^ |-- > 6 -|
pivô
=== RECURSÃO ===
Quick Sort([3, 1, 5, 2, 4]) Quick Sort([9, 7, 8])
↓ ↓
[1, 2, 3, 4, 5] [7, 8, 9]
Resultado: [1, 2, 3, 4, 5, 6, 7, 8, 9]
Complexidade
| Caso | Tempo | Espaço |
|---|---|---|
| Melhor | O(n log n) | O(log n) |
| Médio | O(n log n) | O(log n) |
| Pior | O(n²) | O(n) |
- Melhor caso O(n log n): quando o pivô sempre divide o vetor em duas metades iguais. Gera log n níveis de recursão com O(n) trabalho cada.
- Caso médio O(n log n): mesmo com divisões desbalanceadas, desde que não sejam extremas, a complexidade permanece O(n log n). Análise probabilística mostra que pivôs aleatórios geram partições razoáveis na maioria das vezes.
- Pior caso O(n²): ocorre quando o pivô é sempre o menor ou maior elemento (ex: vetor já ordenado com pivô no final). Cada partição remove apenas 1 elemento, gerando n níveis de recursão.
- Espaço O(log n): a pilha de recursão tem profundidade log n no caso médio. No pior caso, pode chegar a O(n).
Implementação em Rust
Particionamento de Lomuto
/// Quick Sort com particionamento de Lomuto.
/// O pivô é o último elemento.
fn quick_sort_lomuto<T: PartialOrd>(vetor: &mut [T]) {
let n = vetor.len();
if n <= 1 {
return;
}
let indice_pivo = particionar_lomuto(vetor);
// Ordenar a parte esquerda (antes do pivô)
quick_sort_lomuto(&mut vetor[..indice_pivo]);
// Ordenar a parte direita (depois do pivô)
if indice_pivo + 1 < n {
quick_sort_lomuto(&mut vetor[indice_pivo + 1..]);
}
}
/// Particionamento de Lomuto.
/// Escolhe o último elemento como pivô.
/// Retorna a posição final do pivô.
fn particionar_lomuto<T: PartialOrd>(vetor: &mut [T]) -> usize {
let n = vetor.len();
let pivo_idx = n - 1;
let mut i = 0; // posição onde o próximo elemento menor será colocado
for j in 0..pivo_idx {
if vetor[j] <= vetor[pivo_idx] {
vetor.swap(i, j);
i += 1;
}
}
// Colocar o pivô na posição correta
vetor.swap(i, pivo_idx);
i
}
fn main() {
let mut v = vec![8, 3, 7, 1, 5, 2, 9, 4, 6];
println!("Antes: {:?}", v);
quick_sort_lomuto(&mut v);
println!("Depois: {:?}", v);
}
Particionamento de Hoare
O esquema de Hoare é mais eficiente que o de Lomuto, pois faz em média 3x menos trocas:
/// Quick Sort com particionamento de Hoare.
/// Geralmente mais eficiente que Lomuto.
fn quick_sort_hoare<T: PartialOrd + Clone>(vetor: &mut [T]) {
let n = vetor.len();
if n <= 1 {
return;
}
quick_sort_hoare_interno(vetor, 0, n - 1);
}
fn quick_sort_hoare_interno<T: PartialOrd + Clone>(
vetor: &mut [T],
inicio: usize,
fim: usize,
) {
if inicio >= fim {
return;
}
let p = particionar_hoare(vetor, inicio, fim);
if p > 0 {
quick_sort_hoare_interno(vetor, inicio, p);
}
quick_sort_hoare_interno(vetor, p + 1, fim);
}
/// Particionamento de Hoare.
/// Usa dois ponteiros que se movem de fora para dentro.
fn particionar_hoare<T: PartialOrd + Clone>(
vetor: &mut [T],
inicio: usize,
fim: usize,
) -> usize {
let pivo = vetor[inicio].clone();
let mut i = inicio;
let mut j = fim;
loop {
// Avançar i enquanto vetor[i] < pivô
while vetor[i] < pivo {
i += 1;
}
// Retroceder j enquanto vetor[j] > pivô
while vetor[j] > pivo {
j -= 1;
}
if i >= j {
return j;
}
vetor.swap(i, j);
i += 1;
j -= 1;
}
}
fn main() {
let mut v = vec![8, 3, 7, 1, 5, 2, 9, 4, 6];
println!("Antes: {:?}", v);
quick_sort_hoare(&mut v);
println!("Depois: {:?}", v);
}
Exemplo de Execução
Antes: [8, 3, 7, 1, 5, 2, 9, 4, 6]
Depois: [1, 2, 3, 4, 5, 6, 7, 8, 9]
Rastreamento do particionamento de Lomuto com o vetor [8, 3, 7, 1, 5]:
Pivô = 5 (último elemento)
i = 0
j=0: vetor[0]=8 > 5 → nada [8, 3, 7, 1, 5] i=0
j=1: vetor[1]=3 ≤ 5 → swap(0,1) [3, 8, 7, 1, 5] i=1
j=2: vetor[2]=7 > 5 → nada [3, 8, 7, 1, 5] i=1
j=3: vetor[3]=1 ≤ 5 → swap(1,3) [3, 1, 7, 8, 5] i=2
Colocar pivô: swap(2, 4) [3, 1, 5, 8, 7]
←<5→ ^ ←>5→
pivô na posição 2
Variações e Otimizações
1. Mediana de Três
Escolher o pivô como a mediana de três elementos (primeiro, meio e último) evita o pior caso em vetores já ordenados ou quase ordenados:
/// Quick Sort com mediana de três para escolha do pivô.
fn quick_sort_mediana<T: PartialOrd>(vetor: &mut [T]) {
let n = vetor.len();
if n <= 1 {
return;
}
// Otimização: Insertion Sort para partições pequenas
if n <= 16 {
for i in 1..n {
let mut j = i;
while j > 0 && vetor[j - 1] > vetor[j] {
vetor.swap(j - 1, j);
j -= 1;
}
}
return;
}
// Mediana de três: organizar primeiro, meio e último
let meio = n / 2;
let ultimo = n - 1;
// Colocar a mediana na posição do meio
if vetor[0] > vetor[meio] {
vetor.swap(0, meio);
}
if vetor[0] > vetor[ultimo] {
vetor.swap(0, ultimo);
}
if vetor[meio] > vetor[ultimo] {
vetor.swap(meio, ultimo);
}
// Mover o pivô (mediana) para a penúltima posição
vetor.swap(meio, ultimo - 1);
// Particionar usando o pivô na posição ultimo-1
let mut i = 0;
let mut j = ultimo - 1;
let pivo_pos = ultimo - 1;
loop {
i += 1;
while vetor[i] < vetor[pivo_pos] {
i += 1;
}
j -= 1;
while vetor[j] > vetor[pivo_pos] {
if j == 0 {
break;
}
j -= 1;
}
if i >= j {
break;
}
vetor.swap(i, j);
}
// Restaurar o pivô
vetor.swap(i, pivo_pos);
// Recursão nas duas partições
quick_sort_mediana(&mut vetor[..i]);
if i + 1 < n {
quick_sort_mediana(&mut vetor[i + 1..]);
}
}
fn main() {
let mut v = vec![8, 3, 7, 1, 5, 2, 9, 4, 6];
println!("Antes: {:?}", v);
quick_sort_mediana(&mut v);
println!("Depois: {:?}", v);
// Caso que seria pior caso sem mediana de três
let mut ordenado = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
quick_sort_mediana(&mut ordenado);
println!("Já ordenado: {:?}", ordenado);
}
2. Particionamento de Três Vias (Dutch National Flag)
Quando há muitos elementos duplicados, o particionamento de três vias (menor, igual, maior) é significativamente mais eficiente:
/// Quick Sort com particionamento de três vias.
/// Eficiente quando há muitos elementos duplicados.
fn quick_sort_tres_vias<T: PartialOrd + Clone>(vetor: &mut [T]) {
let n = vetor.len();
if n <= 1 {
return;
}
quick_sort_tres_vias_interno(vetor, 0, n - 1);
}
fn quick_sort_tres_vias_interno<T: PartialOrd + Clone>(
vetor: &mut [T],
inicio: usize,
fim: usize,
) {
if inicio >= fim {
return;
}
let pivo = vetor[inicio].clone();
let mut menor = inicio; // vetor[inicio..menor] < pivô
let mut atual = inicio; // vetor[menor..atual] == pivô
let mut maior = fim; // vetor[maior+1..fim] > pivô
while atual <= maior {
if vetor[atual] < pivo {
vetor.swap(menor, atual);
menor += 1;
atual += 1;
} else if vetor[atual] > pivo {
vetor.swap(atual, maior);
if maior == 0 {
break;
}
maior -= 1;
// Não incrementa atual — o elemento trocado precisa ser verificado
} else {
atual += 1;
}
}
// Recursão: pular todos os iguais ao pivô
if menor > 0 {
quick_sort_tres_vias_interno(vetor, inicio, menor - 1);
}
if maior < fim {
quick_sort_tres_vias_interno(vetor, maior + 1, fim);
}
}
fn main() {
// Vetor com muitos duplicados
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 1, 3];
println!("Antes: {:?}", v);
quick_sort_tres_vias(&mut v);
println!("Depois: {:?}", v);
}
Visualização do particionamento de três vias:
Pivô = 5 [5, 3, 8, 5, 2, 5, 9, 1]
[5, 3, 8, 5, 2, 5, 9, 1]
m a M
atual=5: == pivô → avança
[5, 3, 8, 5, 2, 5, 9, 1] m=0, a=1, M=7
atual=3: < pivô → swap(menor, atual), avança ambos
[3, 5, 8, 5, 2, 5, 9, 1] m=1, a=2, M=7
atual=8: > pivô → swap(atual, maior)
[3, 5, 1, 5, 2, 5, 9, 8] m=1, a=2, M=6
... continua até atual > maior ...
Resultado: [< 5 ... | 5, 5, 5 | ... > 5]
← menor ← iguais → maior →
3. Tail-Call Optimization
Para evitar estouro de pilha, podemos aplicar recursão apenas na partição menor e iterar na maior:
/// Quick Sort com otimização de tail-call.
/// Garante profundidade de recursão O(log n).
fn quick_sort_tail<T: PartialOrd>(vetor: &mut [T]) {
let mut inicio = 0;
let mut fim = vetor.len();
while fim - inicio > 1 {
// Particionar
let pivo_idx = {
let n = fim - inicio;
let pivo_pos = fim - 1;
let mut i = inicio;
for j in inicio..pivo_pos {
if vetor[j] <= vetor[pivo_pos] {
vetor.swap(i, j);
i += 1;
}
}
vetor.swap(i, pivo_pos);
i
};
// Recursão na partição menor, iteração na maior
let tamanho_esquerda = pivo_idx - inicio;
let tamanho_direita = fim - pivo_idx - 1;
if tamanho_esquerda < tamanho_direita {
quick_sort_tail(&mut vetor[inicio..pivo_idx]);
inicio = pivo_idx + 1; // iterar na parte direita
} else {
if pivo_idx + 1 < fim {
quick_sort_tail(&mut vetor[pivo_idx + 1..fim]);
}
fim = pivo_idx; // iterar na parte esquerda
}
}
}
fn main() {
let mut v = vec![8, 3, 7, 1, 5, 2, 9, 4, 6];
println!("Antes: {:?}", v);
quick_sort_tail(&mut v);
println!("Depois: {:?}", v);
}
Quando Usar
O Quick Sort é a escolha adequada nos seguintes cenários:
- Uso geral: é o algoritmo de ordenação mais rápido na prática para a maioria dos conjuntos de dados em memória, graças ao excelente padrão de acesso ao cache.
- Memória limitada: por ser in-place (O(log n) de espaço extra), é preferível ao Merge Sort quando memória é restrita.
- Estabilidade não necessária: o Quick Sort padrão é instável. Se estabilidade for necessária, use Merge Sort.
- Muitos duplicados: com particionamento de três vias, o Quick Sort lida eficientemente com chaves repetidas.
- Dados em memória (não em disco): o Quick Sort se beneficia do acesso sequencial à memória.
Evite o Quick Sort quando:
- Garantia de O(n log n) for obrigatória no pior caso — use Merge Sort ou Heap Sort.
- Os dados estiverem em disco — o Merge Sort é melhor para ordenação externa.
- Estabilidade for necessária — use Merge Sort ou o
sort()estável do Rust.
Comparação com Outros Algoritmos
| Característica | Quick Sort | Merge Sort | Heap Sort | Insertion Sort |
|---|---|---|---|---|
| Tempo (médio) | O(n log n) | O(n log n) | O(n log n) | O(n²) |
| Tempo (pior) | O(n²) | O(n log n) ✓ | O(n log n) ✓ | O(n²) |
| Espaço | O(log n) | O(n) | O(1) ✓ | O(1) ✓ |
| Estável | Não | Sim ✓ | Não | Sim ✓ |
| Cache-friendly | Sim ✓ | Não | Não | Sim ✓ |
| In-place | Sim ✓ | Não | Sim ✓ | Sim ✓ |
| Velocidade real | Mais rápido ✓ | Bom | Mais lento | Bom (n pequeno) |
O Quick Sort é geralmente 2-3x mais rápido que o Merge Sort e Heap Sort na prática, mesmo compartilhando a mesma complexidade assintótica. Isso se deve ao melhor padrão de acesso ao cache e menor overhead por operação. A função sort_unstable() do Rust usa uma variante do Quick Sort (pattern-defeating quicksort) que combina o melhor de vários algoritmos.
Exercícios Práticos
Quickselect: Implemente o algoritmo Quickselect para encontrar o k-ésimo menor elemento em tempo esperado O(n). Use o particionamento do Quick Sort mas recurse apenas no lado que contém a posição k.
Quick Sort com pivô aleatório: Use uma seed fixa e uma função de hash simples (sem crates externas) para selecionar pivôs pseudo-aleatórios. Compare o desempenho com pivô fixo em vetores já ordenados.
Análise de profundidade: Modifique o Quick Sort para rastrear a profundidade máxima de recursão. Teste com vetores aleatórios, ordenados e com muitos duplicados. Quais cenários geram a maior profundidade?
Introsort simplificado: Implemente um Quick Sort que monitora a profundidade de recursão e, se ultrapassar 2 * log2(n), muda para Heap Sort. Esta é a ideia básica do Introsort, usado em C++.
Veja Também
- Merge Sort em Rust — alternativa estável com O(n log n) garantido
- Heap Sort em Rust — in-place com O(n log n) garantido
- Insertion Sort em Rust — caso base para partições pequenas
- Vec — Vetores Dinâmicos — métodos
sort()esort_unstable() - Slice — Fatias de Memória — particionamento e slicing
- Option — Valores Opcionais — tratamento de valores opcionais em particionamento
- Otimização de Performance em Rust — cache-friendliness e branch prediction