← Todos os algoritmos

Linear Search

Busca · ver código-fonte no GitHub

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

Categoria: Searching

O problema

Encontrar um valor em uma coleção sem nenhuma suposição sobre sua ordem — os dados podem genuinamente estar desordenados, ou ordená-los apenas para executar uma única busca custaria mais do que a própria busca. Seja qual for o motivo, não há atalho disponível: sem uma ordenação para explorar, não há como saber qual metade dos dados descartar.

A solução

Percorrer a coleção a partir do início e testar cada elemento até que um corresponda, ou até que a coleção se esgote. Não há nada de engenhoso aqui de propósito — todo o valor deste módulo está em estabelecer a linha de base honesta que toda busca mais rápida (a começar pelo Binary Search deste próprio repositório) precisa superar, e por que ela não pode ser superada sem uma suposição como "os dados estão ordenados".

Caso Custo Por quê
Melhor caso (correspondência no índice 0) O(1) a primeiríssima comparação já tem sucesso
Pior caso (sem correspondência, ou correspondência no final) O(n) todos os elementos precisam ser examinados
Médio O(n) proporcional a quão adiante na coleção está a correspondência

Exemplo clássico

classic/LinearSearch recebe um Predicate<? super T> em vez de um valor-alvo fixo — uma generalização deliberada em relação a "encontrar este valor exato": é o que uma busca linear real geralmente precisa (encontrar o primeiro elemento que atenda a uma condição, não apenas igual a uma constante), e é isso que permite que o exemplo aplicado abaixo reutilize este mesmo método sem alterações. LinearSearchTest cobre uma correspondência no meio, nenhuma correspondência, uma correspondência na primeiríssima posição, uma correspondência na últimíssima posição, um array vazio, e ambas as proteções contra argumento nulo.

Exemplo aplicado: localizador de chamada excedente em telecom

applied/FirstOverageCallFinder encontra a primeira chamada em um log diário de registros de detalhes de chamadas (CDR) cuja duração excede a franquia do plano de um assinante, para disparar um alerta de excedente em tempo real. O log é ordenado pelo horário de chegada — as chamadas chegam das torres na ordem em que acontecem — e não pela duração, então não existe uma visão ordenada por duração contra a qual fazer busca binária, e reordenar todo o log por duração a cada verificação custaria mais do que a própria varredura linear. Este é o caso honesto para linear search: os dados genuinamente não estão ordenados pelo campo em que a busca é feita. FirstOverageCallFinderTest cobre uma chamada encontrada no meio do log, nenhuma chamada excedendo a franquia, e a proteção contra argumento nulo.

Benchmark

./gradlew :searching:linear-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). Pior caso: o alvo nunca está presente, forçando uma varredura completa. Mesma tarefa, mesmos tamanhos, mesma máquina do benchmark de Binary Search deste repositório, então os dois são diretamente comparáveis:

Custo da busca (alvo ausente) size=100 size=10,000 size=1,000,000
Linear Search 36.41 ns 6,589.62 ns 1,676,427.30 ns

O custo cresce aproximadamente na mesma proporção do tamanho — ~181x para um aumento de 100x no tamanho (100→10,000), ~254x para outro aumento de 100x (10,000→1,000,000) — com ruído em torno do ideal de ~100x, mas inconfundivelmente linear, não sublinear. Em size=1,000,000, a busca equivalente do Binary Search nesta mesma máquina roda em 30.29 ns — a varredura linear deste módulo é ~55,353x mais lenta para exatamente a mesma pergunta "está aí ou não", puramente porque não pode presumir que os dados estão ordenados. Essa diferença é toda a razão pela qual uma busca em dados ordenados merece seu próprio algoritmo.

Quando não usar

Cobertura de testes

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

./gradlew :searching:linear-search:jacocoTestReport

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

Leitura complementar

Testes unitários

src/test/java/com/algorithms/searching/linearsearch/classic/LinearSearchTest.java
package com.algorithms.searching.linearsearch.classic;

import org.junit.jupiter.api.Test;

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

class LinearSearchTest {

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

        int index = LinearSearch.indexOf(array, value -> value == 8);

        assertThat(index).isEqualTo(2);
    }

    @Test
    void returnsMinusOneWhenNoElementMatches() {
        Integer[] array = {5, 3, 8};

        int index = LinearSearch.indexOf(array, value -> value == 100);

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

    @Test
    void matchAtTheFirstPositionReturnsZero() {
        Integer[] array = {7, 3, 8};

        int index = LinearSearch.indexOf(array, value -> value == 7);

        assertThat(index).isZero();
    }

    @Test
    void matchAtTheLastPositionReturnsTheLastIndex() {
        Integer[] array = {7, 3, 8};

        int index = LinearSearch.indexOf(array, value -> value == 8);

        assertThat(index).isEqualTo(2);
    }

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

        int index = LinearSearch.indexOf(array, value -> true);

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

    @Test
    void rejectsANullArray() {
        assertThatThrownBy(() -> LinearSearch.indexOf(null, value -> true))
                .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANullMatcher() {
        assertThatThrownBy(() -> LinearSearch.indexOf(new Integer[] {1}, null))
                .isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/searching/linearsearch/applied/FirstOverageCallFinderTest.java
package com.algorithms.searching.linearsearch.applied;

import org.junit.jupiter.api.Test;

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

class FirstOverageCallFinderTest {

    @Test
    void findsTheFirstCallExceedingTheAllowance() {
        FirstOverageCallFinder finder = new FirstOverageCallFinder();
        CallRecord[] log = {
                new CallRecord("a", 120),
                new CallRecord("b", 600),
                new CallRecord("c", 900),
        };

        int index = finder.findFirstOverage(log, 300);

        assertThat(index).isEqualTo(1);
    }

    @Test
    void returnsMinusOneWhenNoCallExceedsTheAllowance() {
        FirstOverageCallFinder finder = new FirstOverageCallFinder();
        CallRecord[] log = {new CallRecord("a", 60), new CallRecord("b", 120)};

        int index = finder.findFirstOverage(log, 300);

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

    @Test
    void rejectsANullLog() {
        FirstOverageCallFinder finder = new FirstOverageCallFinder();

        assertThatThrownBy(() -> finder.findFirstOverage(null, 300)).isInstanceOf(IllegalArgumentException.class);
    }
}

Ver relatório completo de cobertura JaCoCo →