← Todos os algoritmos

Quick Sort

Ordenação · ver código-fonte no GitHub

Leia em: English · Português · Español

Categoria: Sorting

O problema

O Merge Sort deste repositório garante O(n log n) independentemente da ordem de entrada, mas paga por essa garantia com um buffer auxiliar O(n). O que é necessário para um lote grande e com restrição de memória é O(n log n) em caso médio in place — sem array auxiliar — aceitando um pior caso em troca, desde que esse pior caso possa ser tornado extremamente improvável, em vez de algo que uma entrada real e não confiável possa disparar de propósito ou por acidente.

A solução

Escolha um pivô, particione o intervalo de forma que tudo menor que ele fique à esquerda e tudo maior fique à direita — inteiramente por meio de trocas dentro do array original, sem buffer auxiliar — e então recorra em cada lado. A etapa de particionamento é O(n); a profundidade média de recursão é O(log n), o que resulta em O(n log n) em caso médio. A pegadinha: uma escolha de pivô fixa (sempre o último elemento, por exemplo) atinge seu pior caso O(n²) exatamente com entradas já ordenadas ou ordenadas de forma reversa — precisamente as duas ordenações que os benchmarks deste repositório já testam para todo outro algoritmo de ordenação. Esta implementação escolhe o pivô de forma uniformemente aleatória dentro do intervalo atual antes de cada particionamento. Isso não elimina o pior caso — ele ainda é matematicamente possível — mas vincula o pior caso à semente aleatória em vez da ordem da própria entrada, o que é o que torna o quicksort seguro para rodar sobre entradas que você não controla, em vez de uma mina terrestre esperando pela única forma de entrada que o quebra.

flowchart LR
    subgraph "partition around a pivot (7)"
        direction LR
        A0["5"] --- A1["3"] --- A2["9"] --- A3["1"] --- A4["7*"] --- A5["8"]
    end
    subgraph "after: smaller left, larger right, pivot fixed"
        direction LR
        B0["5"] --- B1["3"] --- B2["1"] --- B3["7*"] --- B4["9"] --- B5["8"]
    end
Caso Custo Por quê
Médio O(n log n) o pivô aleatório divide o intervalo aproximadamente ao meio em média, mesmo formato de recursão do merge sort
Pior (teórico) O(n²) uma sequência de escolhas de pivô azaradas que cada uma separa apenas um elemento — possível para qualquer estratégia de pivô, mas a semente aleatória controla as chances, não a entrada
Espaço O(log n) auxiliar (pilha de recursão) o particionamento acontece in place; sem array de buffer, diferente do Merge Sort

Exemplo clássico

classic/QuickSort é genérico sobre Comparator<? super T> — sem atalho de Arrays.sort/Collections.sort — usando particionamento de Lomuto com um pivô randomizado trocado para a última posição antes de cada chamada de particionamento. O sort(array, comparator) público usa um java.util.Random sem semente; uma sobrecarga sort(array, comparator, random) package-private aceita um Random injetado para que os testes possam ser determinísticos. QuickSortTest cobre um array desordenado, já ordenado, ordenado de forma reversa, duplicatas, um array de elemento único e um vazio, um comparador customizado (descendente), a sobrecarga pública sem semente, e ambas as proteções contra argumento nulo.

Exemplo aplicado: ordenação por percentil de reserva de sinistros de seguros

applied/ClaimAmountSort ordena um grande lote de sinistros de seguro por valor para o cálculo de reserva baseado em percentil (por exemplo, "qual valor de sinistro marca o percentil 95 neste trimestre"). Exportações de sinistros comumente já chegam quase ordenadas — por ID do sinistro, que tende a se correlacionar com a data de abertura, que por sua vez se correlaciona frouxamente com o valor para muitos tipos de sinistro — que é exatamente o tipo de entrada quase ordenada que faria um quicksort não randomizado degradar em direção ao seu pior caso. Randomizar o pivô é o que mantém isso seguro para rodar sobre um lote real, não sinteticamente aleatório. ClaimAmountSortTest cobre sinistros ordenados em ordem crescente de valor, que o array de entrada permanece intocado, e a proteção contra argumento nulo.

Benchmark

./gradlew :sorting:quick-sort:jmh

Execução real nesta máquina (JMH 1.37, JDK 26.0.2, 2 iterações de aquecimento + 3 iterações de medição, 1 fork). Já ordenado e ordenado de forma reversa — as duas ordenações que seriam catastróficas para um quicksort de pivô fixo — comparados a uma entrada totalmente aleatória:

Custo de ordenação size=100 size=1,000 size=10,000
já ordenado 6.127 µs 81.923 µs 1,001.669 µs
ordenado de forma reversa 7.764 µs 82.253 µs 1,216.873 µs
aleatório 8.217 µs 142.259 µs 2,171.216 µs

Esse é todo o ponto, tornado mensurável: em size=10,000, o aleatório é apenas ~2.2x mais caro que o já ordenado — não o 100x ou mais que o pior caso O(n²) de um quicksort não randomizado mostraria exatamente com essa entrada. O pivô randomizado está fazendo seu trabalho. O crescimento entre os tamanhos também confirma O(n log n), não O(n²): o aleatório vai de size=1,000 para size=10,000 (10x os dados) a ~15.3x o custo — próximo do ~13.3x que um formato O(n log n) prevê, longe do ~100x que uma ordenação quadrática mostraria para o mesmo salto. Vale a pena comparar com o benchmark do Merge Sort deste repositório na mesma máquina: ambos ficam em uma faixa semelhante em size=10,000 (quicksort ~1,000–2,200 µs aqui vs. merge sort ~950–1,700 µs) — desempenho de caso médio genuinamente comparável, com o quicksort não pagando alocação de buffer auxiliar e o merge sort não correndo risco de pior caso.

Quando não usar

Cobertura de testes

100% de cobertura de instruções, 100% de cobertura de branches (JaCoCo). Reproduza você mesmo:

./gradlew :sorting:quick-sort:jacocoTestReport

Relatório em sorting/quick-sort/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

src/test/java/com/algorithms/sorting/quicksort/classic/QuickSortTest.java
package com.algorithms.sorting.quicksort.classic;

import org.junit.jupiter.api.Test;

import java.util.Comparator;
import java.util.Random;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class QuickSortTest {

    private static final Random SEEDED = new Random(42);

    @Test
    void sortsAnUnorderedArrayIntoAscendingOrder() {
        Integer[] array = {5, 3, 8, 1, 9, 2};

        QuickSort.sort(array, Comparator.naturalOrder(), SEEDED);

        assertThat(array).containsExactly(1, 2, 3, 5, 8, 9);
    }

    @Test
    void alreadySortedArrayStaysSorted() {
        Integer[] array = {1, 2, 3, 4, 5};

        QuickSort.sort(array, Comparator.naturalOrder(), SEEDED);

        assertThat(array).containsExactly(1, 2, 3, 4, 5);
    }

    @Test
    void reverseSortedArrayEndsUpAscending() {
        Integer[] array = {5, 4, 3, 2, 1};

        QuickSort.sort(array, Comparator.naturalOrder(), SEEDED);

        assertThat(array).containsExactly(1, 2, 3, 4, 5);
    }

    @Test
    void duplicatesAreHandledCorrectly() {
        Integer[] array = {3, 1, 3, 2, 1};

        QuickSort.sort(array, Comparator.naturalOrder(), SEEDED);

        assertThat(array).containsExactly(1, 1, 2, 3, 3);
    }

    @Test
    void singleElementArrayStaysUnchanged() {
        Integer[] array = {42};

        QuickSort.sort(array, Comparator.naturalOrder(), SEEDED);

        assertThat(array).containsExactly(42);
    }

    @Test
    void emptyArrayStaysEmpty() {
        Integer[] array = {};

        QuickSort.sort(array, Comparator.naturalOrder(), SEEDED);

        assertThat(array).isEmpty();
    }

    @Test
    void sortsUsingACustomComparatorForDescendingOrder() {
        Integer[] array = {1, 5, 3, 2, 4};

        QuickSort.sort(array, Comparator.reverseOrder(), SEEDED);

        assertThat(array).containsExactly(5, 4, 3, 2, 1);
    }

    @Test
    void publicSortMethodWorksWithoutAnExplicitRandom() {
        Integer[] array = {4, 2, 7, 1};

        QuickSort.sort(array, Comparator.naturalOrder());

        assertThat(array).containsExactly(1, 2, 4, 7);
    }

    @Test
    void rejectsANullArray() {
        assertThatThrownBy(() -> QuickSort.sort(null, Comparator.naturalOrder()))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANullComparator() {
        assertThatThrownBy(() -> QuickSort.sort(new Integer[] {1, 2}, null))
                .isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/sorting/quicksort/applied/ClaimAmountSortTest.java
package com.algorithms.sorting.quicksort.applied;

import org.junit.jupiter.api.Test;

import java.math.BigDecimal;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class ClaimAmountSortTest {

    @Test
    void sortByAmountOrdersClaimsAscending() {
        ClaimAmountSort sorter = new ClaimAmountSort();
        InsuranceClaim[] claims = {
                claim("mid", "500.00"),
                claim("high", "9000.00"),
                claim("low", "12.50"),
        };

        InsuranceClaim[] sorted = sorter.sortByAmount(claims);

        assertThat(sorted).extracting(InsuranceClaim::claimId).containsExactly("low", "mid", "high");
    }

    @Test
    void doesNotMutateTheInputArray() {
        ClaimAmountSort sorter = new ClaimAmountSort();
        InsuranceClaim[] claims = {claim("b", "200.00"), claim("a", "10.00")};

        sorter.sortByAmount(claims);

        assertThat(claims).extracting(InsuranceClaim::claimId).containsExactly("b", "a");
    }

    @Test
    void rejectsNullClaims() {
        ClaimAmountSort sorter = new ClaimAmountSort();

        assertThatThrownBy(() -> sorter.sortByAmount(null)).isInstanceOf(IllegalArgumentException.class);
    }

    private static InsuranceClaim claim(String id, String amount) {
        return new InsuranceClaim(id, new BigDecimal(amount));
    }
}

Ver relatório completo de cobertura JaCoCo →