Quicksort: Particionamento, Pivôs e Dois Esquemas Clássicos
Objetivo
Entenda o quicksort: uma ordenação in-place, dividir-para-conquistar, que escolhe um pivô, particiona o array em torno dele, e ordena recursivamente as duas partes resultantes — caso médio O(n log n) com constantes pequenas, ao custo de um pior caso O(n²) se o pivô for mal escolhido.
Casos de Uso
- A ordenação genérica padrão quando velocidade no caso médio e baixo overhead de memória (in-place, sem array auxiliar) importam mais do que uma garantia de pior caso.
- Um exemplo canônico de dividir-para-conquistar: entender a recursão do quicksort torna mergesort, busca binária e a maioria dos algoritmos de árvore mais fáceis de raciocinar.
- Um exercício padrão de quadro-branco/entrevista — espere ser cobrado a implementar o particionamento, explicar o pior caso, ou traçá-lo manualmente.
Aprofundamento
O algoritmo: particiona, depois recursa
Os dois livros expressam os mesmos três passos. Escolha um pivô do subarray, particione de forma que tudo ≤ pivô fique à sua esquerda e tudo ≥ pivô fique à sua direita, depois ordene recursivamente cada lado:
javavoid quicksort(int[] a, int lo, int hi) {
if (hi <= lo) return;
int pivotIndex = partition(a, lo, hi);
quicksort(a, lo, pivotIndex - 1);
quicksort(a, pivotIndex + 1, hi);
}Os dois livros divergem completamente em como o partition funciona — e essa diferença vale a pena conhecer, já que é uma fonte comum de bugs de off-by-one ao implementar quicksort de memória.
O particionamento Lomuto de Cormen: uma varredura para frente, pivô no final
O CLRS escolhe o último elemento como pivô e percorre o subarray uma vez com dois índices, i rastreando a fronteira da região "≤ pivô":
javaint partition(int[] a, int lo, int hi) {
int pivot = a[hi]; // CLRS escolhe o último elemento
int i = lo - 1; // fronteira da região "≤ pivô"
for (int j = lo; j < hi; j++) {
if (a[j] <= pivot) {
i++;
swap(a, i, j);
}
}
swap(a, i + 1, hi); // pivô cai logo à direita da região "≤"
return i + 1;
}O particionamento estilo Hoare de Sedgewick: dois ponteiros varrendo para dentro
O Algorithms, 4ª Ed., em vez disso escolhe o primeiro elemento como pivô e varre de ambas as extremidades em direção ao meio, trocando pares fora de posição conforme avança:
javaint partition(int[] a, int lo, int hi) {
int pivot = a[lo]; // Sedgewick escolhe o primeiro elemento
int i = lo, j = hi + 1;
while (true) {
while (a[++i] < pivot) if (i == hi) break;
while (a[--j] > pivot) if (j == lo) break;
if (i >= j) break;
swap(a, i, j);
}
swap(a, lo, j); // pivô cai no ponto de encontro
return j;
}Ambos são corretos, in-place, e O(n) por chamada de particionamento — a diferença é a direção da varredura e qual extremidade guarda o pivô, não o comportamento assintótico.
Veja acontecendo: o particionamento e as trocas de verdade
O próprio exemplo de Sedgewick embaralha as letras de "QUICKSORT" e as ordena. Isto executa o particionamento real estilo Hoare mostrado acima, passo a passo — cada escolha de pivô e cada troca, não só onde cada letra termina:
Caso médio vs. pior caso
Um pivô que divide o subarray aproximadamente ao meio a cada vez dá a mesma recorrência do mergesort, T(n) = 2T(n/2) + O(n), que resolve para O(n log n). Um pivô que é sempre o menor ou o maior elemento (entrada já ordenada com uma escolha ingênua de pivô no primeiro/último elemento) degrada cada particionamento para uma divisão de tamanho 1/tamanho (n−1), dando T(n) = T(n-1) + O(n), que resolve para O(n²) — o pior caso que ambos os livros derivam em detalhe.
Trade-offs
Pior caso O(n²) em entrada adversarial ou já ordenada, ao contrário do O(n log n) garantido do mergesort — a mitigação padrão que ambos os livros cobrem é aleatorizar a escolha do pivô (embaralhar o array primeiro, ou escolher um elemento aleatório como pivô) para que o pior caso se torne extremamente improvável em vez de disparado por entradas comuns como dados ordenados ou ordenados ao contrário.
Não é estável — elementos iguais podem ser reordenados entre si durante o particionamento, diferente de mergesort ou insertion sort. Se preservar a ordem relativa original de chaves iguais importa (por exemplo, ordenar transações já ordenadas por data pelo valor), uma ordenação estável é o padrão mais seguro.
Em Java em produção, você não está chamando nenhuma das implementações do livro-texto —
Arrays.sort()num array primitivo (int[],long[], etc.) usa um quicksort de pivô duplo (dual-pivot), não o esquema de pivô único que ambos os livros ensinam, eArrays.sort()num arrayObject[]/genérico usa TimSort (um híbrido estável de merge/insertion sort), não quicksort de forma alguma — porque estabilidade importa para objetos com lógica customizada decompareTo()/Comparator, mas não para primitivos crus:javaint[] nums = {5, 3, 1, 4, 2}; Arrays.sort(nums); // quicksort de pivô duplo, instável, tudo bem para primitivos Integer[] boxed = {5, 3, 1, 4, 2}; Arrays.sort(boxed); // TimSort, estável, usado porque a ordem de chaves iguais pode importar para objetos