← Todos os algoritmos

Binary Search

Busca · ver código-fonte no GitHub

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

Categoria: Searching

O problema

O Linear Search deste repositório responde "isso está aqui?" em O(n) porque não pode presumir nada sobre a ordem dos dados. Se os dados já estiverem ordenados — uma suposição comum e barata, uma vez que tenham sido ordenados uma única vez — esse O(n) está deixando uma quantidade enorme de informação sobre a mesa: cada comparação contra uma coleção desordenada quase não diz nada sobre onde mais procurar; uma comparação contra uma coleção ordenada diz qual metade inteira descartar.

A solução

Comparar o alvo com o elemento do meio. Se corresponder, terminou. Se o alvo for menor, toda a metade superior pode ser descartada — todo elemento ali é garantidamente maior. Se for maior, descarta-se a metade inferior. Repete-se no que sobrar. Cada comparação elimina metade dos candidatos restantes, o que é o que torna isso O(log n): o espaço de busca encolhe geometricamente, não linearmente. Implementado de forma iterativa, com uma janela [low, high] que encolhe, em vez de recursiva, de modo que um array muito grande nunca corre o risco de um frame de pilha por bisseção.

Caso Custo Por quê
Melhor caso (correspondência no ponto médio) O(1) a primeiríssima comparação já tem sucesso
Pior caso (sem correspondência, ou correspondência em um extremo) O(log n) o espaço de busca ainda assim é reduzido à metade a cada passo
Médio O(log n) mesmo padrão de redução pela metade, só que com menos passos que o pior caso

Exemplo clássico

classic/BinarySearch é genérico sobre Comparator<? super T> — sem o atalho de Arrays.binarySearch. O laço reduz uma janela [low, high] a cada iteração em vez de usar recursão, o que é o que mantém isso seguro em arrays grandes demais para uma pilha de chamadas recursiva lidar confortavelmente. BinarySearchTest cobre um alvo no meio, no início, no final, um alvo ausente, um array vazio, um array de um único elemento, cada posição de um array de tamanho par (exercitando as duas direções de arredondamento do ponto médio), e ambas as proteções contra argumento nulo.

Exemplo aplicado: consulta de snapshot de chaves PIX do BACEN

applied/RegisteredPixKeyLookup verifica se uma chave PIX está registrada em um snapshot noturno, já ordenado, de todas as chaves registradas — o tipo de exportação em lote que um job de reconciliação extrai uma vez e depois consulta muitas vezes. Diferentemente de uma trie construída incrementalmente à medida que as chaves são registradas em tempo real, isto presume que todo o conjunto de chaves já é conhecido e está fixo para o dia: ordená-lo uma vez, de antemão, e fazer busca binária a cada consulta é mais barato do que manter uma estrutura viva para dados que não mudam até o snapshot do dia seguinte. RegisteredPixKeyLookupTest cobre uma chave registrada, uma chave não registrada, e ambas as proteções contra argumento nulo.

Benchmark

./gradlew :searching:binary-search:jmh

Execução real nesta máquina (JMH 1.37, JDK 26.0.2, 2 iterações de aquecimento + 3 de medição, 1 fork). Mesmo pior caso, mesmos tamanhos, mesma máquina do benchmark de Linear Search deste repositório:

Custo da busca (alvo ausente) size=100 size=10,000 size=1,000,000
Binary Search 9.60 ns 20.62 ns 30.29 ns

Essa é a alegação de O(log n), tornada inconfundível: um aumento de 100x no tamanho (100→10,000) custa apenas ~2.15x mais tempo; um novo aumento de 100x (10,000→1,000,000) custa apenas ~1.47x mais — multiplicadores decrescentes para o mesmo crescimento proporcional nos dados, exatamente a assinatura de uma escala logarítmica (log₂(10,000)/log₂(100) ≈ 2.0, log₂(1,000,000)/log₂(10,000) ≈ 1.5 — a forma prevista, batendo quase exatamente). Contra os 1,676,427.30 ns do Linear Search para a mesma pergunta em size=1,000,000, este módulo responde em 30.29 ns — ~55,353x mais rápido, na mesma máquina, para a mesma pergunta "está aí ou não". A única diferença é um bit de informação que o linear search não tem como usar: os dados estão ordenados.

Quando não usar

Cobertura de testes

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

./gradlew :searching:binary-search:jacocoTestReport

Relatório em searching/binary-search/build/reports/jacoco/test/html/index.html.

Leitura complementar

Testes unitários

src/test/java/com/algorithms/searching/binarysearch/classic/BinarySearchTest.java
package com.algorithms.searching.binarysearch.classic;

import org.junit.jupiter.api.Test;

import java.util.Comparator;

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

class BinarySearchTest {

    @Test
    void findsATargetInTheMiddle() {
        Integer[] array = {1, 3, 5, 7, 9};

        int index = BinarySearch.search(array, 5, Comparator.naturalOrder());

        assertThat(index).isEqualTo(2);
    }

    @Test
    void findsATargetAtTheStart() {
        Integer[] array = {1, 3, 5, 7, 9};

        int index = BinarySearch.search(array, 1, Comparator.naturalOrder());

        assertThat(index).isZero();
    }

    @Test
    void findsATargetAtTheEnd() {
        Integer[] array = {1, 3, 5, 7, 9};

        int index = BinarySearch.search(array, 9, Comparator.naturalOrder());

        assertThat(index).isEqualTo(4);
    }

    @Test
    void returnsMinusOneWhenTheTargetIsMissing() {
        Integer[] array = {1, 3, 5, 7, 9};

        int index = BinarySearch.search(array, 4, Comparator.naturalOrder());

        assertThat(index).isEqualTo(-1);
    }

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

        int index = BinarySearch.search(array, 1, Comparator.naturalOrder());

        assertThat(index).isEqualTo(-1);
    }

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

        int index = BinarySearch.search(array, 42, Comparator.naturalOrder());

        assertThat(index).isZero();
    }

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

        for (int expected = 0; expected < array.length; expected++) {
            assertThat(BinarySearch.search(array, array[expected], Comparator.naturalOrder())).isEqualTo(expected);
        }
    }

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

    @Test
    void rejectsANullComparator() {
        assertThatThrownBy(() -> BinarySearch.search(new Integer[] {1}, 1, null))
                .isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/searching/binarysearch/applied/RegisteredPixKeyLookupTest.java
package com.algorithms.searching.binarysearch.applied;

import org.junit.jupiter.api.Test;

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

class RegisteredPixKeyLookupTest {

    @Test
    void reportsTrueForARegisteredKey() {
        RegisteredPixKeyLookup lookup = new RegisteredPixKeyLookup(
                new String[] {"alice@bank.com", "bob@bank.com", "carol@bank.com"});

        assertThat(lookup.isRegistered("bob@bank.com")).isTrue();
    }

    @Test
    void reportsFalseForAnUnregisteredKey() {
        RegisteredPixKeyLookup lookup = new RegisteredPixKeyLookup(
                new String[] {"alice@bank.com", "bob@bank.com", "carol@bank.com"});

        assertThat(lookup.isRegistered("dave@bank.com")).isFalse();
    }

    @Test
    void rejectsANullSnapshot() {
        assertThatThrownBy(() -> new RegisteredPixKeyLookup(null)).isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANullKey() {
        RegisteredPixKeyLookup lookup = new RegisteredPixKeyLookup(new String[] {"alice@bank.com"});

        assertThatThrownBy(() -> lookup.isRegistered(null)).isInstanceOf(IllegalArgumentException.class);
    }
}

Ver relatório completo de cobertura JaCoCo →