Acelerando a Busca: Por que o O(log N) (Busca Binária) é mais rápido que a Luz? ✨
Vimos nos posts anteriores como migrar de O(N^2) para O(N). Mas e se pudéssemos ser ainda mais rápidos?
A complexidade O(log N) (LogarÃtmica) é o padrão-ouro para qualquer algoritmo de busca. A grande diferença é a taxa de crescimento:
| Tamanho da Entrada (N) | O(N) (Linear) | O(log N) (LogarÃtmico) |
|---|---|---|
| 1.000 | 1.000 passos | 10 passos |
| 1.000.000 | 1.000.000 passos | 20 passos |
| 1.000.000.000 | 1 bilhão de passos | aproximadamente 30 passos |
O tempo de execução é quase indiferente ao tamanho da entrada.
O Segredo: Dividir para Conquistar 🔪
O algoritmo que atinge essa velocidade é a Busca Binária (Binary Search). A lógica é simples: em cada passo, você consegue descartar metade do espaço de busca.
Pré-requisito Obrigatório
Para que a Busca Binária funcione, a estrutura de dados DEVE estar ordenada.
- Você verifica o elemento central (
mid). - Se o valor central for menor que seu alvo, você descarta toda a metade esquerda.
- Se for maior, descarta toda a metade direita.
Isso se repete recursivamente, dividindo o problema ao meio (log2) a cada iteração.
O Desafio O(N) em Elixir 💥
Aqui está a pegadinha para quem programa em Elixir:
Como a Lista Ligada padrão só sabe onde a cabeça está, se você tentar acessar o elemento central (o Ãndice N/2) em uma lista de Elixir, o sistema é forçado a percorrer os N/2 elementos, tornando essa operação O(N).
Se a cada passo da sua busca O(log N) você gasta O(N) para encontrar o meio, a complexidade final se torna O(N log N) no melhor dos casos, o que anula o ganho da Busca Binária!
A Solução em Elixir: Tuplas O(1)
Para realizar a Busca Binária corretamente em O(log N), precisamos de uma estrutura que permita acesso por Ãndice em O(1). No ecossistema Erlang/Elixir, usamos Tuplas para essa finalidade.
Com o acesso O(1) garantido, podemos implementar a lógica recursiva:
defmodule BinarySearch do
@doc "Inicia a Busca Binária O(log N)."
def search(list, target) do
# Conversão para Tupla para simular o acesso O(1) por Ãndice
array = List.to_tuple(list)
# Inicializa a recursão: low=0, high=N-1
binary_search_recursive(array, target, 0, tuple_size(array) - 1)
end
# Condição de Falha: low cruzou high. Target não existe.
defp binary_search_recursive(_array, _target, low, high) when low > high do
:not_found
end
# Condição Recursiva Principal
defp binary_search_recursive(array, target, low, high) do
# 1. Calcular o Ãndice central (mid)
mid = floor((low + high) / 2)
# 2. Obter o valor central (acesso O(1) na tupla)
# Tuplas em Erlang são base 1, por isso o + 1
current_value = :erlang.element(mid + 1, array)
# 3. Condição de Sucesso
if current_value == target do
mid # Retorna o Ãndice
# 4. Alvo é MAIOR: Descartamos a esquerda, movemos low
else if current_value < target do
binary_search_recursive(array, target, mid + 1, high)
# 5. Alvo é MENOR: Descartamos a direita, movemos high
else
binary_search_recursive(array, target, low, mid - 1)
end
end
end
end
A beleza dessa função é que ela demonstra a complexidade O(log N) de forma perfeita, pois as chamadas recursivas sempre reduzem o espaço de busca pela metade.
Próxima Parada: O Padrão Ouro
Se O(log N) é o melhor para a busca, qual é o melhor para a Ordenação?
No próximo post, exploraremos a complexidade O(N log N) (Linearithmic), o padrão-ouro dos algoritmos de ordenação, e desvendaremos o Merge Sort, que combina o poder logarÃtmico de dividir problemas com o trabalho linear de mesclá-los de volta.
Top comments (0)