A Busca Binária (binary search) é um dos algoritmos mais fundamentais e eficientes da computação. Ela encontra a posição de um elemento em um vetor ordenado dividindo repetidamente o espaço de busca pela metade. A cada comparação, metade dos elementos restantes é eliminada, resultando em complexidade O(log n) — drasticamente mais rápido que a busca linear O(n).
Para um vetor de 1 milhão de elementos, a busca linear pode fazer até 1.000.000 de comparações. A busca binária precisa de no máximo 20. Para 1 bilhão de elementos, apenas 30 comparações. Essa eficiência exponencial torna a busca binária indispensável em bancos de dados, sistemas de arquivos, compiladores e praticamente qualquer sistema que trabalhe com dados ordenados.
Como Funciona
A busca binária mantém dois ponteiros (esquerda e direita) que definem o espaço de busca. A cada iteração:
- Calcula o ponto médio.
- Compara o elemento do meio com o alvo.
- Se igual, encontrou. Se menor, busca na metade direita. Se maior, busca na metade esquerda.
Buscar o valor 23 no vetor ordenado:
Índice: [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
Valor: 2 5 8 12 16 23 38 56 72 91
=== Iteração 1 ===
esq=0, dir=9, meio=4
vetor[4] = 16
[ 2 5 8 12 16 23 38 56 72 91]
esq meio dir
16 < 23 → buscar à DIREITA
=== Iteração 2 ===
esq=5, dir=9, meio=7
vetor[7] = 56
[ 23 38 56 72 91]
esq meio dir
56 > 23 → buscar à ESQUERDA
=== Iteração 3 ===
esq=5, dir=6, meio=5
vetor[5] = 23
[ 23 38]
esq dir
meio
23 == 23 → ENCONTRADO no índice 5!
Total: 3 comparações (log2(10) ≈ 3.3)
Visualização da redução do espaço de busca:
Iteração 1: |████████████████████| 10 elementos
Iteração 2: |██████████| 5 elementos
Iteração 3: |████| 2 elementos
Encontrado! |██| 1 elemento
A cada passo, o espaço é dividido pela metade!
Complexidade
| Caso | Tempo | Espaço |
|---|---|---|
| Melhor | O(1) | O(1) iterativo / O(log n) recursivo |
| Médio | O(log n) | O(1) iterativo / O(log n) recursivo |
| Pior | O(log n) | O(1) iterativo / O(log n) recursivo |
- Melhor caso O(1): o elemento buscado está exatamente no meio do vetor na primeira comparação.
- Caso médio e pior O(log n): a cada iteração, o espaço de busca é dividido pela metade. Após k iterações, restam n/2^k elementos. Quando n/2^k = 1, temos k = log2(n).
- Pré-requisito: o vetor DEVE estar ordenado. Se não estiver, a busca binária pode retornar resultados incorretos.
- Espaço: a versão iterativa usa O(1) de espaço extra. A versão recursiva usa O(log n) na pilha de chamadas.
| Tamanho (n) | Comparações (log2 n) |
|---|---|
| 100 | 7 |
| 1.000 | 10 |
| 1.000.000 | 20 |
| 1.000.000.000 | 30 |
Implementação em Rust
Versão Iterativa
/// Busca binária iterativa.
/// Retorna Some(índice) se o elemento for encontrado, None caso contrário.
/// O vetor DEVE estar ordenado em ordem crescente.
fn busca_binaria<T: Ord>(vetor: &[T], alvo: &T) -> Option<usize> {
let mut esquerda = 0;
let mut direita = vetor.len();
while esquerda < direita {
// Evitar overflow: meio = esquerda + (direita - esquerda) / 2
let meio = esquerda + (direita - esquerda) / 2;
match vetor[meio].cmp(alvo) {
std::cmp::Ordering::Equal => return Some(meio),
std::cmp::Ordering::Less => esquerda = meio + 1,
std::cmp::Ordering::Greater => direita = meio,
}
}
None
}
fn main() {
let numeros = vec![2, 5, 8, 12, 16, 23, 38, 56, 72, 91];
// Buscar elementos existentes
println!("Buscar 23: {:?}", busca_binaria(&numeros, &23)); // Some(5)
println!("Buscar 2: {:?}", busca_binaria(&numeros, &2)); // Some(0)
println!("Buscar 91: {:?}", busca_binaria(&numeros, &91)); // Some(9)
// Buscar elemento inexistente
println!("Buscar 50: {:?}", busca_binaria(&numeros, &50)); // None
// Funciona com strings também
let palavras = vec!["abacate", "banana", "caju", "damasco", "figo"];
println!("\nBuscar 'caju': {:?}", busca_binaria(&palavras, &"caju")); // Some(2)
println!("Buscar 'uva': {:?}", busca_binaria(&palavras, &"uva")); // None
}
Versão Recursiva
/// Busca binária recursiva.
/// Retorna Some(índice) se encontrado, None caso contrário.
fn busca_binaria_recursiva<T: Ord>(vetor: &[T], alvo: &T) -> Option<usize> {
busca_recursiva_interna(vetor, alvo, 0, vetor.len())
}
fn busca_recursiva_interna<T: Ord>(
vetor: &[T],
alvo: &T,
esquerda: usize,
direita: usize,
) -> Option<usize> {
if esquerda >= direita {
return None;
}
let meio = esquerda + (direita - esquerda) / 2;
match vetor[meio].cmp(alvo) {
std::cmp::Ordering::Equal => Some(meio),
std::cmp::Ordering::Less => busca_recursiva_interna(vetor, alvo, meio + 1, direita),
std::cmp::Ordering::Greater => busca_recursiva_interna(vetor, alvo, esquerda, meio),
}
}
fn main() {
let v = vec![1, 3, 5, 7, 9, 11, 13, 15];
println!("Buscar 7: {:?}", busca_binaria_recursiva(&v, &7)); // Some(3)
println!("Buscar 6: {:?}", busca_binaria_recursiva(&v, &6)); // None
}
Usando a Biblioteca Padrão
O Rust oferece o método binary_search() em slices:
fn main() {
let v = vec![2, 5, 8, 12, 16, 23, 38, 56, 72, 91];
// binary_search retorna Result<usize, usize>
// Ok(índice) se encontrado
// Err(índice) com a posição onde o elemento deveria ser inserido
match v.binary_search(&23) {
Ok(pos) => println!("Encontrado 23 na posição {}", pos),
Err(pos) => println!("23 não encontrado, inserir na posição {}", pos),
}
match v.binary_search(&50) {
Ok(pos) => println!("Encontrado 50 na posição {}", pos),
Err(pos) => println!("50 não encontrado, inserir na posição {}", pos),
}
// binary_search_by — busca com comparador personalizado
let pares = vec![(1, "um"), (3, "três"), (5, "cinco"), (7, "sete")];
let resultado = pares.binary_search_by(|item| item.0.cmp(&5));
println!("\nBuscar chave 5: {:?}", resultado); // Ok(2)
// binary_search_by_key — busca por campo
let resultado = pares.binary_search_by_key(&3, |item| item.0);
println!("Buscar chave 3: {:?}", resultado); // Ok(1)
// partition_point — encontrar ponto de partição
let ponto = v.partition_point(|&x| x < 30);
println!("\nElementos < 30: {:?}", &v[..ponto]);
println!("Elementos >= 30: {:?}", &v[ponto..]);
}
Exemplo de Execução
Encontrado 23 na posição 5
50 não encontrado, inserir na posição 7
Buscar chave 5: Ok(2)
Buscar chave 3: Ok(1)
Elementos < 30: [2, 5, 8, 12, 16, 23]
Elementos >= 30: [38, 56, 72, 91]
Versão com rastreamento:
fn busca_binaria_rastreada(vetor: &[i32], alvo: i32) -> Option<usize> {
let mut esquerda = 0;
let mut direita = vetor.len();
let mut iteracao = 0;
println!("Buscando {} em {:?}\n", alvo, vetor);
while esquerda < direita {
iteracao += 1;
let meio = esquerda + (direita - esquerda) / 2;
println!(
" Iteração {}: esq={}, dir={}, meio={}, vetor[meio]={}",
iteracao, esquerda, direita, meio, vetor[meio]
);
match vetor[meio].cmp(&alvo) {
std::cmp::Ordering::Equal => {
println!(" → Encontrado na posição {}!", meio);
return Some(meio);
}
std::cmp::Ordering::Less => {
println!(" → {} < {} → buscar à direita", vetor[meio], alvo);
esquerda = meio + 1;
}
std::cmp::Ordering::Greater => {
println!(" → {} > {} → buscar à esquerda", vetor[meio], alvo);
direita = meio;
}
}
}
println!(" → Não encontrado após {} iterações", iteracao);
None
}
fn main() {
let v = vec![2, 5, 8, 12, 16, 23, 38, 56, 72, 91];
busca_binaria_rastreada(&v, 23);
println!();
busca_binaria_rastreada(&v, 50);
}
Variações e Otimizações
1. Lower Bound e Upper Bound
Encontrar a primeira e última posição de um valor em um vetor com duplicatas:
/// Lower bound: encontra o índice do PRIMEIRO elemento >= alvo.
/// Equivalente a std::lower_bound em C++ ou partition_point(|x| x < alvo).
fn lower_bound<T: Ord>(vetor: &[T], alvo: &T) -> usize {
let mut esquerda = 0;
let mut direita = vetor.len();
while esquerda < direita {
let meio = esquerda + (direita - esquerda) / 2;
if vetor[meio] < *alvo {
esquerda = meio + 1;
} else {
direita = meio;
}
}
esquerda
}
/// Upper bound: encontra o índice do PRIMEIRO elemento > alvo.
/// Equivalente a std::upper_bound em C++ ou partition_point(|x| x <= alvo).
fn upper_bound<T: Ord>(vetor: &[T], alvo: &T) -> usize {
let mut esquerda = 0;
let mut direita = vetor.len();
while esquerda < direita {
let meio = esquerda + (direita - esquerda) / 2;
if vetor[meio] <= *alvo {
esquerda = meio + 1;
} else {
direita = meio;
}
}
esquerda
}
/// Contar quantas vezes um valor aparece no vetor ordenado.
fn contar_ocorrencias<T: Ord>(vetor: &[T], alvo: &T) -> usize {
let lb = lower_bound(vetor, alvo);
let ub = upper_bound(vetor, alvo);
ub - lb
}
/// Encontrar o intervalo [primeiro, último] de um valor.
fn buscar_intervalo<T: Ord>(vetor: &[T], alvo: &T) -> Option<(usize, usize)> {
let lb = lower_bound(vetor, alvo);
if lb >= vetor.len() || vetor[lb] != *alvo {
return None;
}
let ub = upper_bound(vetor, alvo);
Some((lb, ub - 1))
}
fn main() {
let v = vec![1, 2, 2, 2, 3, 3, 5, 5, 5, 5, 7, 8];
// 0 1 2 3 4 5 6 7 8 9 10 11
println!("Vetor: {:?}\n", v);
println!("lower_bound(2) = {} (primeira posição >= 2)", lower_bound(&v, &2));
println!("upper_bound(2) = {} (primeira posição > 2)", upper_bound(&v, &2));
println!("Ocorrências de 2: {}", contar_ocorrencias(&v, &2));
println!("Intervalo de 2: {:?}", buscar_intervalo(&v, &2));
println!();
println!("lower_bound(5) = {}", lower_bound(&v, &5));
println!("upper_bound(5) = {}", upper_bound(&v, &5));
println!("Ocorrências de 5: {}", contar_ocorrencias(&v, &5));
println!("Intervalo de 5: {:?}", buscar_intervalo(&v, &5));
println!();
println!("lower_bound(4) = {} (posição de inserção para 4)", lower_bound(&v, &4));
println!("Intervalo de 4: {:?}", buscar_intervalo(&v, &4));
}
2. Posição de Inserção (Search Insert Position)
Encontrar onde um elemento deveria ser inserido para manter o vetor ordenado:
/// Encontra a posição onde `alvo` deveria ser inserido para manter a ordem.
/// Se o alvo já existe, retorna a posição dele.
fn posicao_insercao<T: Ord>(vetor: &[T], alvo: &T) -> usize {
let mut esquerda = 0;
let mut direita = vetor.len();
while esquerda < direita {
let meio = esquerda + (direita - esquerda) / 2;
if vetor[meio] < *alvo {
esquerda = meio + 1;
} else {
direita = meio;
}
}
esquerda
}
/// Vetor ordenado com inserção eficiente usando busca binária.
struct VetorOrdenado<T: Ord> {
dados: Vec<T>,
}
impl<T: Ord> VetorOrdenado<T> {
fn novo() -> Self {
VetorOrdenado { dados: Vec::new() }
}
fn inserir(&mut self, valor: T) {
let pos = posicao_insercao(&self.dados, &valor);
self.dados.insert(pos, valor);
}
fn contem(&self, valor: &T) -> bool {
self.dados.binary_search(valor).is_ok()
}
fn como_slice(&self) -> &[T] {
&self.dados
}
}
fn main() {
let v = vec![1, 3, 5, 7, 9];
println!("Vetor: {:?}\n", v);
for alvo in [0, 4, 6, 10] {
let pos = posicao_insercao(&v, &alvo);
println!("Inserir {} na posição {} → mantém ordenado", alvo, pos);
}
println!("\n--- Vetor Ordenado ---");
let mut vo = VetorOrdenado::novo();
for x in [5, 2, 8, 1, 9, 3] {
vo.inserir(x);
println!("Inserir {} → {:?}", x, vo.como_slice());
}
println!("Contém 5? {}", vo.contem(&5));
println!("Contém 4? {}", vo.contem(&4));
}
3. Busca Binária em Resposta (Binary Search on Answer)
Técnica poderosa para problemas de otimização: buscar sobre o espaço de respostas possíveis.
/// Exemplo: encontrar a raiz quadrada inteira de um número.
/// Maior x tal que x * x <= n.
fn raiz_quadrada_inteira(n: u64) -> u64 {
if n <= 1 {
return n;
}
let mut esquerda: u64 = 1;
let mut direita: u64 = n;
let mut resultado: u64 = 1;
while esquerda <= direita {
let meio = esquerda + (direita - esquerda) / 2;
// Usar meio <= n / meio para evitar overflow (em vez de meio * meio <= n)
if meio <= n / meio {
resultado = meio;
esquerda = meio + 1;
} else {
direita = meio - 1;
}
}
resultado
}
/// Exemplo: verificar se é possível dividir o vetor em k partes
/// onde a soma máxima de cada parte não excede `limite`.
fn pode_dividir(vetor: &[i64], k: usize, limite: i64) -> bool {
let mut partes = 1;
let mut soma_atual = 0;
for &valor in vetor {
if valor > limite {
return false; // Um único elemento já excede o limite
}
soma_atual += valor;
if soma_atual > limite {
partes += 1;
soma_atual = valor;
if partes > k {
return false;
}
}
}
true
}
/// Minimizar a soma máxima ao dividir o vetor em k partes.
fn minimizar_soma_maxima(vetor: &[i64], k: usize) -> i64 {
let mut esquerda: i64 = *vetor.iter().max().unwrap();
let mut direita: i64 = vetor.iter().sum();
while esquerda < direita {
let meio = esquerda + (direita - esquerda) / 2;
if pode_dividir(vetor, k, meio) {
direita = meio;
} else {
esquerda = meio + 1;
}
}
esquerda
}
fn main() {
// Raiz quadrada
for n in [0, 1, 4, 8, 16, 25, 100, 1000000] {
println!("sqrt({}) = {}", n, raiz_quadrada_inteira(n));
}
// Minimizar soma máxima
println!();
let vetor = vec![7, 2, 5, 10, 8];
let k = 2;
let resultado = minimizar_soma_maxima(&vetor, k);
println!(
"Vetor {:?}, dividir em {} partes: soma máxima mínima = {}",
vetor, k, resultado
);
}
Quando Usar
A Busca Binária e adequada nos seguintes cenários:
- Busca em dados ordenados: sempre que o vetor estiver ordenado (ou puder ser ordenado previamente), a busca binária é exponencialmente mais rápida que a busca linear.
- Múltiplas consultas: se você fará muitas buscas no mesmo vetor, vale ordenar uma vez em O(n log n) e fazer cada busca em O(log n), em vez de O(n) por busca linear.
- Binary Search on Answer: para problemas de otimização onde a resposta é monótona (se funciona para x, funciona para x+1), use busca binária no espaço de respostas.
- Lower/Upper bound: encontrar posições de inserção, contar ocorrências, buscar intervalos.
- Verificação de propriedades monótonas: qualquer predicado que mude de falso para verdadeiro (ou vice-versa) ao longo de uma sequência ordenada.
Evite a Busca Binária quando:
- Os dados não estiverem ordenados e haverá apenas uma consulta — busca linear é mais simples.
- O vetor for muito pequeno (n < 10) — busca linear pode ser mais rápida por ter menos overhead.
- Os dados mudarem frequentemente — manter o vetor ordenado pode custar mais que busca linear.
Comparação com Outros Algoritmos
| Característica | Busca Binária | Busca Linear | Busca por Interpolação | Hash Table |
|---|---|---|---|---|
| Complexidade | O(log n) | O(n) | O(log log n)* | O(1)* |
| Pré-requisito | Ordenado | Nenhum | Ord. + uniforme | Hash function |
| Espaço extra | O(1) | O(1) | O(1) | O(n) |
| Dados dinâmicos | Ruim | Bom ✓ | Ruim | Bom ✓ |
| Cache-friendly | Razoável | Sim ✓ | Não | Não |
| Pior caso | O(log n) ✓ | O(n) | O(n) | O(n) |
*A busca por interpolação é O(log log n) para dados uniformemente distribuídos. Hash table é O(1) amortizado, mas O(n) no pior caso.
Exercícios Práticos
Busca em vetor rotacionado: Um vetor ordenado foi rotacionado (ex:
[4, 5, 6, 7, 0, 1, 2]). Implemente uma busca binária modificada que encontre um elemento em O(log n) nesse vetor. Dica: identifique qual metade está ordenada.Pico de montanha: Dado um vetor que primeiro cresce e depois decresce (ex:
[1, 3, 8, 12, 4, 2]), encontre o elemento máximo em O(log n) usando busca binária.Primeira posição de verdadeiro: Dada uma função monótona
f(i)que retornafalsepara i < k etruepara i >= k, encontre k usando busca binária. Implemente e teste comf(i) = vetor[i] >= alvo.Mediana de dois vetores ordenados: Dados dois vetores ordenados de tamanhos m e n, encontre a mediana combinada em O(log(min(m, n))) usando busca binária. Este é um clássico problema de entrevista.
Veja Também
- Busca Linear e Interpolação — alternativas para dados não ordenados ou uniformemente distribuídos
- Merge Sort em Rust — ordenação necessária antes da busca binária
- Quick Sort em Rust — outra forma de ordenar antes de buscar
- Vec — Vetores Dinâmicos — estrutura principal para busca
- Slice — Fatias de Memória — métodos
binary_search(),partition_point() - Result — Tratamento de Erros — tipo de retorno de
binary_search() - Option — Valores Opcionais — representação de busca com/sem resultado