Busca binária iterativa e recursiva
Suponha um catálogo de filmes ordenado por ano de lançamento. Neste cenário, como poderíamos localizar rapidamente uma obra específica sem percorrer item a item o conjunto de dados? Esse problema é bastante recorrente em bancos de dados, sistemas de arquivos, compiladores e estruturas de indexação e pode ser resolvido de forma elegante pela busca binária, um dos algoritmos mais estudados em Ciência da Computação por combinar simplicidade conceitual e eficiência assintótica notável.
Neste artigo vamos apresentar a busca binária em duas formulações — iterativa e recursiva —, discutir suas implementações em C++ sobre vetores estáticos ordenados, analisar formalmente sua complexidade e apontar armadilhas comuns de implementação. Manteremos, como fio condutor dos exemplos, um pequeno sistema de consulta a um acervo de filmes ordenado por ano de lançamento — cenário simples, mas suficiente para evidenciar cada aspecto do algoritmo.
O que é busca binária?
A busca binária é um algoritmo de pesquisa utilizado para localizar um valor em uma sequência previamente ordenada. Sua ideia central é comparar o valor procurado com o elemento central do vetor e, a partir dessa comparação, descartar metade dos elementos restantes, continuando a busca apenas na metade onde o valor ainda pode estar. Observe que essa lógica é contrária à busca sequencial, pois o algoritmo não vai verificar os elementos um por um em todo o conjunto de dados.
Podemos dizer também que o algoritmo é uma técnica de busca baseada na estratégia de divisão sucessiva do espaço de pesquisa. Dado um vetor ordenado, o algoritmo mantém dois limites: o início e o fim da região onde o valor pode estar. A cada passo, o algoritmo calcula a posição central dessa região e compara o elemento central com o valor procurado.
Se o elemento central for igual ao valor procurado, a busca termina. Se o valor procurado for menor que o elemento central, o algoritmo descarta a metade direita. Se for maior, descarta a metade esquerda. Segundo Martins (2009), esse processo continua até que o valor seja encontrado ou até que a região de busca se torne vazia, ou seja, até que o limite final torne-se menor que o limite inicial. Portanto, o encerramento da busca depende de duas condições: encontrar o valor ou esgotar o intervalo possível de pesquisa.
Pré-condição para a busca binária
A busca binária exige uma pré-condição indispensável para sua aplicação: o vetor precisa estar ordenado. Sem ordenação, não há garantia de que o valor menor esteja à esquerda nem de que o valor maior esteja à direita. Nesse caso, descartar metade do vetor pode eliminar exatamente a região onde o valor procurado se encontra.
Por exemplo, considere o vetor ordenado:
int codigos[] = {101, 203, 305, 410, 512, 608, 777};
Se procuramos o código 410, podemos comparar primeiro com o elemento central. Como o vetor está ordenado, cada decisão elimina uma parte do problema com segurança.
Agora considere um vetor desordenado:
int codigos[] = {410, 101, 777, 203, 512, 305, 608};
Nesse caso, a busca binária não é adequada, pois a posição relativa dos elementos não preserva uma ordem lógica. Antes de aplicar a busca binária, seria necessário ordenar o vetor ou escolher outro método de busca.
Intuição do algoritmo
Imagine uma lista telefônica impressa. Para encontrar um nome, dificilmente uma pessoa começa na primeira página e lê todos os nomes até chegar ao resultado. O procedimento natural é abrir a lista em uma região intermediária, verificar se o nome procurado viria antes ou depois daquela página e repetir o processo em uma parte menor da lista. A busca binária funciona de forma semelhante.
Basicamente, podemos resumir o princípio da busca binária em três passos, aplicados repetidamente sobre um intervalo [esquerda, direita] que representa a região do vetor ainda não descartada:
- Calcula-se o índice do elemento central do intervalo.
- Compara-se o valor buscado com o elemento central:
- Se forem iguais, a busca termina com sucesso.
- Se o valor buscado for menor, a busca prossegue na metade esquerda.
- Se for maior, a busca prossegue na metade direita.
- O processo se repete até que o elemento seja encontrado ou o intervalo se torne vazio.
A execução desses três passos transforma a pesquisa em um processo de redução progressiva do intervalo de análise. Se o vetor possui 1.000 elementos, a primeira comparação pode reduzir o problema para aproximadamente 500 elementos. A segunda, para 250. A terceira, para 125. Esse comportamento explica sua eficiência.
Ziviani (2010) situa a busca binária como vantajosa à medida que o volume de dados cresce, justamente por evitar a varredura completa da estrutura. Em contrapartida, o autor alerta que a manutenção da ordenação tem custo — cada inserção em uma posição intermediária exige deslocar os registros subsequentes —, de modo que a busca binária não deve ser empregada em aplicações muito dinâmicas, isto é, com muitas inserções e remoções intercaladas com buscas (Ziviani, 2010).
Exemplo conceitual passo a passo
Considere o vetor:
Índice: 0 1 2 3 4 5 6
Valor: 10 20 30 40 50 60 70
Nesse conjunto, deseja-se procurar o valor 60. Na primeira iteração, o início é 0, o fim é 6 e o meio é 3. O valor na posição 3 é 40. Como 60 é maior que 40, a busca continua na metade direita.
Agora o início passa a ser 4, o fim continua 6 e o meio passa a ser 5. O valor na posição 5 é 60. Como o valor foi encontrado, o algoritmo retorna a posição 5.
Observe que o algoritmo encontrou o valor com apenas duas comparações. Uma busca sequencial, nesse mesmo caso, poderia exigir seis comparações se começasse no índice 0.
Com base no exemplo conceitual, podemos descrever a função de busca binária em forma de pseudocódigo assim:
FUNÇÃO buscaBinaria(vetor, tamanho, valorProcurado)
inicio ← 0
fim ← tamanho - 1
ENQUANTO inicio <= fim FAÇA
meio ← inicio + (fim - inicio) / 2
SE vetor[meio] = valorProcurado ENTÃO
RETORNE meio
FIM SE
SE valorProcurado < vetor[meio] ENTÃO
fim ← meio - 1
SENÃO
inicio ← meio + 1
FIM SE
FIM ENQUANTO
RETORNE -1
FIM FUNÇÃO
A lógica do pseudocódigo é começar considerando todo o vetor como área possível de busca. Em seguida, calcular a posição central. Se o valor central for o elemento procurado, retorna sua posição. Se o valor procurado for menor, continua apenas na metade esquerda. Se for maior, continua apenas na metade direita. Quando a variável inicio ultrapassar a variável fim, significa que não existe mais região válida para pesquisar e, por isso, a função retornará o valor -1.
Busca binária iterativa
Agora, vamos transformar a lógica da busca binária num código em linguagem C++. A primeira versão que demonstraremos é a iterativa, usando uma estrutura de repetição (geralmente while) para atualizar os limites da busca. Ela tende a ser a forma mais direta para iniciantes, pois deixa explícitas as mudanças nas variáveis inicio, fim e meio.
Leia e observe o algoritmo a seguir:
#include <iostream>
using namespace std;
int buscaBinariaIterativa(int dados[], int tamanho, int valorProcurado) {
int inicio = 0;
int fim = tamanho - 1;
while (inicio <= fim) {
int meio = inicio + (fim - inicio) / 2;
if (dados[meio] == valorProcurado) {
return meio;
}
if (valorProcurado < dados[meio]) {
fim = meio - 1;
} else {
inicio = meio + 1;
}
}
return -1;
}
int main() {
int codigos[] = {101, 203, 305, 410, 512, 608, 777};
int tamanho = 7;
int valorProcurado = 410;
int posicao = buscaBinariaIterativa(codigos, tamanho, valorProcurado);
if (posicao != -1) {
cout << "Codigo encontrado na posicao " << posicao << endl;
} else {
cout << "Codigo nao encontrado." << endl;
}
return 0;
}No código iterativo, a função retorna a posição onde o valor foi encontrado ou o valor -1 caso o valor não exista no vetor, conforme estabelecido no pseudocódigo. O retorno de -1 é uma convenção comum para indicar ausência de resultado válido, apesar de não ser uma regra rígida e imutável.
A expressão usada para calcular o meio do conjunto foi:
int meio = inicio + (fim - inicio) / 2;
Esse método de cálculo é preferível à versão abaixo:
int meio = (inicio + fim) / 2;
porque em vetores muito grandes, calcular (inicio + fim) pode ultrapassar o limite de armazenamento de um inteiro. Como a forma inicio + (fim - inicio) / 2 evita esse risco, optamos por ela. Contudo, é válido ressaltar que em vetores pequenos, as duas formas costumam produzir o mesmo resultado.
Busca binária recursiva
Evoluindo nosso algoritmo, agora vamos aparesentar a versão recursiva. Ela expressa a busca binária como um problema que se chama novamente sobre uma parte menor do vetor. A cada chamada, o intervalo [inicio, fim] diminui. O caso-base ocorre quando o valor é encontrado ou quando inicio > fim.
O algoritmo de busca binária em C++ ficaria assim:
#include <iostream>
using namespace std;
int buscaBinariaRecursiva(int dados[], int inicio, int fim, int valorProcurado) {
if (inicio > fim) {
return -1;
}
int meio = inicio + (fim - inicio) / 2;
if (dados[meio] == valorProcurado) {
return meio;
}
if (valorProcurado < dados[meio]) {
return buscaBinariaRecursiva(dados, inicio, meio - 1, valorProcurado);
}
return buscaBinariaRecursiva(dados, meio + 1, fim, valorProcurado);
}
int main() {
int codigos[] = {101, 203, 305, 410, 512, 608, 777};
int tamanho = 7;
int valorProcurado = 608;
int posicao = buscaBinariaRecursiva(codigos, 0, tamanho - 1, valorProcurado);
if (posicao != -1) {
cout << "Codigo encontrado na posicao " << posicao << endl;
} else {
cout << "Codigo nao encontrado." << endl;
}
return 0;
}A versão recursiva é útil para mostrar uma característica da busca binária chamada “dividir para conquistar”, ou seja, a cada comparação, o algoritmo divide o intervalo de busca para aproximadamente metade do tamanho original.
Comparação entre versão iterativa e recursiva
A versão iterativa e a versão recursiva resolvem o mesmo problema e possuem a mesma complexidade assintótica de tempo: O(log n). A diferença está principalmente na forma de controle.
A versão iterativa controla o processo por meio de um laço. Em geral, é mais econômica em memória, pois não cria novas chamadas de função a cada passo. Cormen et al. (2012) observam, ao comparar versões recursivas e iterativas em buscas sobre árvores, que uma versão iterativa pode ser mais eficiente em muitos computadores por eliminar a sobrecarga da recursão. Embora o exemplo discutido pelos autores esteja relacionado a árvores de busca binária, o princípio também ajuda a compreender a diferença prática entre a busca binária iterativa e a recursiva em vetores.
A versão recursiva, por outro lado, é conceitualmente elegante. Ela evidencia que o problema original é reduzido a um subproblema da mesma natureza: buscar o mesmo valor em uma metade menor do vetor.
Para fins didáticos, podemos elencar os prós e contras de ambas as implementações na tabela a seguir:
| Critério | Iterativa | Recursiva |
|---|---|---|
| Legibilidade | Boa, especialmente para quem já conhece o algoritmo | Excelente, pois espelha diretamente a definição por indução do problema |
| Espaço extra | Θ(1) | Θ(log n) (pilha de chamadas) |
| Risco de stack overflow | Inexistente | Presente em cenários extremos |
| Facilidade de depuração | Mais direta (estado visível em variáveis de laço) | Requer acompanhar múltiplos quadros de pilha |
| Uso didático | Reforça o conceito de invariante de laço | Reforça o conceito de indução e casos base/recursivo |
Do ponto de vista de engenharia de software, a versão iterativa é geralmente preferida em código de produção por sua eficiência de espaço e ausência de risco de estouro de pilha. A versão recursiva, por sua vez, tem grande valor pedagógico por evidenciar a estrutura de divisão e conquista de forma mais explícita, justificando sua presença constante em disciplinas introdutórias de algoritmos.
Análise de complexidade
A busca binária tem complexidade de tempo Θ(log n). Isso ocorre porque, a cada comparação, o tamanho do problema é dividido aproximadamente por dois.
Se o vetor possui n elementos, depois da primeira comparação restam aproximadamente (n / 2) elementos. Depois da segunda, (n / 4). Depois da terceira, (n / 8). O processo continua até que reste apenas um elemento ou nenhum elemento.
Formalmente, considere T(n) o tempo de execução da busca binária sobre um vetor de n elementos. A cada chamada, o algoritmo realiza uma quantidade constante de trabalho (uma comparação, o cálculo do centro) e, no pior caso, invoca-se recursivamente sobre um subvetor de aproximadamente (n / 2) elementos. Essa relação é expressa pela recorrência:
Cormen et al. (2012) demonstram que recorrências desse formato — em que o problema é dividido por uma constante a cada chamada, com trabalho adicional constante — resultam em complexidade Θ(log n). Intuitivamente, a pergunta é: quantas vezes é possível dividir n por 2 até restar apenas 1 elemento? A resposta é log₂n, o que fundamenta o resultado.
Complexidade de tempo
| Caso | Complexidade | Justificativa |
|---|---|---|
| Melhor caso | Θ(1) | O elemento buscado está exatamente na posição central na primeira comparação. |
| Caso médio | Θ(log n) | Em média, o algoritmo converge após um número logarítmico de divisões. |
| Pior caso | Θ(log n) | O elemento está em uma extremidade ou não existe; o intervalo é reduzido até restar um único elemento. |
Só para que você tenha uma ideia mais clara do que isso significa e possa dimensionar esse ganho, pense o seguinte: suponha um vetor com 1 milhão de elementos. A busca sequencial pode exigir, no pior caso, 1.000.000 de comparações, enquanto a busca binária necessita de, no máximo, cerca de 20 comparações — log₂(1.000.000) ≈ 20 (Ziviani, 2010). Esse é o ganho da busca binária em relação à sequencial para vetores grandes.
Complexidade de espaço
Aqui reside uma diferença prática relevante entre as duas versões implementadas:
- Versão iterativa: complexidade de espaço
Θ(1), pois utiliza apenas variáveis auxiliares (esquerda,direita,centro), independentemente do tamanho do vetor. - Versão recursiva: complexidade de espaço
Θ(log n), pois cada chamada recursiva consome um novo quadro (stack frame) na pilha de execução, e a profundidade máxima da recursão é proporcional alog n.
Essa diferença é especialmente relevante em ambientes com pilha de execução limitada, ou ao lidar com vetores extremamente grandes, situação em que a recursão profunda pode levar a estouro de pilha (stack overflow), que é um risco inexistente na versão iterativa.
Cuidado com valores repetidos
Em muitos materiais introdutórios, a busca binária é apresentada sobre vetores ordenados e sem repetição. Martins (2009), por exemplo, descreve a busca binária nesse contexto didático, considerando vetor ordenado e sem dados repetidos.
Entretanto, em sistemas reais, pode haver valores repetidos. Nesse caso, a busca binária tradicional retorna uma das ocorrências, mas não necessariamente a primeira ou a última. Se for necessário encontrar a primeira ocorrência de um valor repetido, o algoritmo precisa ser adaptado.
Observe a versão a seguir.
#include <iostream>
using namespace std;
int buscarPrimeiraOcorrencia(int dados[], int tamanho, int valorProcurado) {
int inicio = 0;
int fim = tamanho - 1;
int resultado = -1;
while (inicio <= fim) {
int meio = inicio + (fim - inicio) / 2;
if (dados[meio] == valorProcurado) {
resultado = meio;
fim = meio - 1;
} else if (valorProcurado < dados[meio]) {
fim = meio - 1;
} else {
inicio = meio + 1;
}
}
return resultado;
}
int main() {
int notas[] = {50, 60, 60, 60, 70, 80, 90};
int tamanho = 7;
int valorProcurado = 60;
int posicao = buscarPrimeiraOcorrencia(notas, tamanho, valorProcurado);
if (posicao != -1) {
cout << "Primeira ocorrencia encontrada na posicao " << posicao << endl;
} else {
cout << "Valor nao encontrado." << endl;
}
return 0;
}Repare que mesmo encontrando uma ocorrência do elemento procurado, a função não retorna posição obtida. Ela atualiza a variável fim, reduzindo o espaço de busca para verificar se há alguma ocorrência anterior à encontrada.
Busca binária em um cenário simples de trabalho
Considere um pequeno sistema acadêmico que armazena códigos de matrícula já ordenados. O objetivo é verificar rapidamente se uma matrícula está registrada.
#include <iostream>
using namespace std;
int buscarMatricula(int matriculas[], int tamanho, int matriculaProcurada) {
int inicio = 0;
int fim = tamanho - 1;
while (inicio <= fim) {
int meio = inicio + (fim - inicio) / 2;
if (matriculas[meio] == matriculaProcurada) {
return meio;
}
if (matriculaProcurada < matriculas[meio]) {
fim = meio - 1;
} else {
inicio = meio + 1;
}
}
return -1;
}
int main() {
int matriculas[] = {1001, 1005, 1010, 1020, 1033, 1048, 1050, 1062};
int tamanho = 8;
int matriculaProcurada;
cout << "Digite a matricula que deseja localizar: ";
cin >> matriculaProcurada;
int posicao = buscarMatricula(matriculas, tamanho, matriculaProcurada);
if (posicao != -1) {
cout << "Matricula localizada no indice " << posicao << "." << endl;
} else {
cout << "Matricula nao localizada." << endl;
}
return 0;
}Esse exemplo é simples, mas representa um padrão recorrente em sistemas reais. A necessidade de consultar rapidamente se um identificador existe em uma coleção ordenada é mais comum do que parece. Em estruturas mais avançadas, a mesma ideia aparece em índices, árvores de busca, tabelas ordenadas e bancos de dados.
Quando usar e quando não usar busca binária
A busca binária deve ser usada quando os dados estão ordenados e quando há muitas operações de busca sobre a mesma coleção. Se o vetor já está ordenado, a busca binária é muito eficiente.
Outros exemplos onde pode-se aplicar a estratégia são:
- Indexação de bancos de dados e sistemas de arquivos, onde estruturas ordenadas permitem localizar registros rapidamente.
- Controle de versão (o comando
git bisect, por exemplo, aplica o mesmo princípio de divisão para localizar o commit que introduziu um defeito). - Bibliotecas padrão de linguagens de programação, como as funções de busca em coleções ordenadas disponíveis em diversas linguagens.
- Problemas de otimização por “busca binária na resposta”, técnica avançada em que o algoritmo não busca um valor em um vetor, mas sim a menor ou maior resposta que satisfaz uma condição monótona — extensão conceitual frequente em competições de programação.
Agora, se o vetor não está ordenado, talvez seja necessário avaliar o custo de ordená-lo antes de aplicar a busca binária. Se a aplicação fará apenas uma única busca em um vetor pequeno e desordenado, uma busca sequencial pode ser suficiente. Portanto, é preciso avaliar o contexto com calma e ponderar as possibilidades.
Conclusão
A busca binária é uma técnica essencial para estudantes de algoritmos e estruturas de dados porque demonstra, de forma clara, como uma boa estratégia algorítmica pode reduzir o custo de execução de uma tarefa. Enquanto a busca sequencial pode exigir até n comparações, a busca binária exige aproximadamente log2(n) comparações no pior caso, desde que o vetor esteja ordenado.
Dominar a busca binária ajuda o programador a compreender conceitos fundamentais como pré-condição, divisão do problema, atualização de limites, análise de complexidade, recursividade e eficiência computacional. Na prática profissional, essa compreensão prepara o estudante para estruturas mais sofisticadas, como árvores de busca binária, índices ordenados e algoritmos de consulta em grandes volumes de dados. Portanto, a busca binária é pequena em implementação, mas grande em importância conceitual.
Obrigado pela leitura e bons estudos!
Referências
CORMEN, Thomas H.; LEISERSON, Charles E.; RIVEST, Ronald L.; STEIN, Clifford. Algoritmos: teoria e prática. 3. ed. Rio de Janeiro: Elsevier, 2012.
MARTINS, Paulo Roberto. Algoritmos e estrutura de dados: análise e desenvolvimento de sistemas. São Paulo: Pearson Prentice Hall, 2009.
ZIVIANI, Nivio. Projeto de algoritmos: com implementações em Java e C++. 3 ed. São Paulo: Cengage Learning, 2010.


