devly
Voltar para Algoritmos

A Arte da Busca e Ordenação

Domine a eficiência do Binary Search e entenda os trade-offs dos principais algoritmos de ordenação como Merge Sort e Quick Sort.

Atualizado em: 10 de Junho, 2025
Notas do Autor

Eu costumava achar que ordenação era um "problema resolvido". Afinal, toda linguagem tem um .sort(). Pra que saber mais? Até que um entrevistador me perguntou: "Por que, na prática, o Quick Sort costuma ser mais rápido que o Merge Sort, mesmo ambos sendo O(n log n) em média?". E sobre busca: "Seu código funciona, mas você consegue fazer essa busca em O(log n)?".

Foi aí que entendi: eles não querem que você reinvente a roda. Eles querem saber se você entende os mecanismos e os trade-offs por trás da roda. Querem ver se você sabe escolher a melhor ferramenta.

E Binary Search? É mais que um algoritmo, é um padrão mental que aparece em dezenas de problemas disfarçados. Dominar esses conceitos é fundamental.


1. Encontrando uma Agulha no Palheiro: A Busca

Buscar um elemento em uma coleção é uma das tarefas mais comuns. A forma como fazemos isso tem um impacto gigantesco na performance.

Busca Linear: A Força Bruta (O(n))

É a abordagem mais simples: olhe cada item, um por um, até encontrar o que procura. Funciona, mas é lenta para grandes volumes de dados.

Busca Binária (Binary Search): O Pulo do Gato (O(log n))

Esta é uma das técnicas mais importantes do seu arsenal. A ideia é simples e poderosa: a cada passo, você "corta" o problema pela metade.

Analogia: Procurando no Dicionário 📖

Você não procura a palavra "Zebra" começando da letra "A". Você abre o dicionário no meio, vê que está na letra "M", e imediatamente descarta toda a primeira metade. Depois, você pega a segunda metade e repete o processo. Isso é Busca Binária.

Pré-requisito CRÍTICO 🚨

A Busca Binária SÓ funciona se a sua coleção de dados estiver ORDENADA. Se não estiver, os resultados serão incorretos.
javascript
function binarySearch(sortedArray, target) {
  let left = 0;
  let right = sortedArray.length - 1;

  while (left <= right) {
    // Encontra o índice do meio, evitando overflow com (left + right)
    let mid = left + Math.floor((right - left) / 2);

    // Se o alvo está no meio, encontramos!
    if (sortedArray[mid] === target) {
      return mid; // Retorna o índice
    }

    // Se o alvo é maior, ignora a metade da esquerda
    if (sortedArray[mid] < target) {
      left = mid + 1;
    } else {
      // Se o alvo é menor, ignora a metade da direita
      right = mid - 1;
    }
  }

  // Se o loop terminar, o alvo não está no array
  return -1;
}

2. Ordenação: Colocando a Casa em Ordem

Se a busca eficiente depende de dados ordenados, como os ordenamos? Vamos mergulhar nos algoritmos mais famosos, dos mais simples aos mais eficientes, entendendo seus trade-offs.

Os Algoritmos Didáticos (O(n²)) 🐌

Estes algoritmos são ótimos para aprender os fundamentos, mas são muito lentos para grandes conjuntos de dados.

Bubble Sort

Analogia: Pense em bolhas de ar subindo num copo d'água. As bolhas mais leves (menores números) sobem para o topo mais rápido.

Como Funciona: Ele percorre a lista repetidamente, comparando cada par de elementos adjacentes e trocando-os de posição se estiverem na ordem errada. A cada passagem, o maior elemento "borbulha" para o final da lista.

javascript
function bubbleSort(arr) {
  const n = arr.length;
  for (let i = 0; i < n - 1; i++) {
    // O último i elementos já estão no lugar
    for (let j = 0; j < n - i - 1; j++) {
      // Compara elementos adjacentes e troca se estiverem na ordem errada
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // Swap
      }
    }
  }
  return arr;
}
Tempo: O(n²) sempre.
Espaço: O(1) - in-place.
Estável: Sim.

Insertion Sort

Analogia: É como você organiza um baralho de cartas na mão. Você pega uma carta de cada vez e a insere na posição correta dentro das cartas que já estão ordenadas.

Como Funciona: Ele constrói o array ordenado final, um item de cada vez. Ele itera sobre a entrada, e para cada elemento, encontra o local correto na parte já ordenada da lista e o insere lá.

javascript
function insertionSort(arr) {
  const n = arr.length;
  for (let i = 1; i < n; i++) {
    // O elemento atual que vamos inserir na parte ordenada
    let current = arr[i];
    // O último índice da parte já ordenada
    let j = i - 1;

    // Move os elementos da parte ordenada que são maiores que o 'current'
    // para a direita, abrindo espaço para o 'current'
    while (j >= 0 && arr[j] > current) {
      arr[j + 1] = arr[j];
      j--;
    }
    // Insere o 'current' na sua posição correta
    arr[j + 1] = current;
  }
  return arr;
}
Tempo: O(n²) no médio/pior caso, mas O(n) no melhor caso (array quase ordenado).
Espaço: O(1) - in-place.
Estável: Sim.

Os Campeões da Eficiência (O(n log n)) 🚀

Estes são os algoritmos que você encontrará em implementações de bibliotecas padrão e que são esperados em discussões de performance.

Merge Sort

Analogia: Um bibliotecário meticuloso com uma pilha enorme de livros para ordenar. Ele divide a pilha em duas, depois divide de novo, e de novo, até ter várias pilhas de um livro só (que já estão "ordenadas"). Depois, ele começa a juntar (merge) as pilhas de forma ordenada, duas a duas, até ter a pilha original perfeitamente ordenada.

Como Funciona: É um algoritmo clássico de "dividir para conquistar". Ele divide o array pela metade recursivamente até que as sub-listas tenham um elemento. Então, ele mescla essas sub-listas de volta, garantindo que a lista resultante esteja ordenada.

javascript
function mergeSort(arr) {
  if (arr.length <= 1) {
    return arr; // Caso base: array de 0 ou 1 elemento já está ordenado
  }

  // 1. Dividir
  const middle = Math.floor(arr.length / 2);
  const leftHalf = arr.slice(0, middle);
  const rightHalf = arr.slice(middle);

  // Chamada recursiva para cada metade
  const sortedLeft = mergeSort(leftHalf);
  const sortedRight = mergeSort(rightHalf);

  // 2. Conquistar (Mesclar)
  return merge(sortedLeft, sortedRight);
}

function merge(left, right) {
  const result = [];
  let leftIndex = 0;
  let rightIndex = 0;

  // Compara os elementos das duas metades e adiciona o menor ao resultado
  while (leftIndex < left.length && rightIndex < right.length) {
    if (left[leftIndex] < right[rightIndex]) {
      result.push(left[leftIndex]);
      leftIndex++;
    } else {
      result.push(right[rightIndex]);
      rightIndex++;
    }
  }

  // Adiciona os elementos restantes (se houver) de uma das metades
  return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}
Tempo: O(n log n) em todos os casos (melhor, médio e pior). Sua performance é garantida.
Espaço: O(n) - precisa de um array auxiliar para fazer o merge.
Estável: Sim.

Quick Sort

Analogia: Um bibliotecário com mais pressa. Ele pega um livro aleatório da pilha e o usa como "marcador" (pivô). Ele joga todos os livros que vêm antes (alfabeticamente) para a esquerda e todos que vêm depois para a direita. Agora ele tem duas pilhas menores e repete o processo caótico em cada uma até que tudo esteja no lugar.

Como Funciona: Também é "dividir para conquistar". Ele seleciona um elemento como pivô e particiona os outros elementos em dois sub-arrays, de acordo com se são menores ou maiores que o pivô. Depois, ordena os sub-arrays recursivamente.

javascript
function quickSort(arr, left = 0, right = arr.length - 1) {
  if (left >= right) {
    return; // Caso base: o subarray tem 0 ou 1 elemento
  }

  // Encontra a posição do pivô após o particionamento
  const pivotIndex = partition(arr, left, right);

  // Ordena recursivamente a sub-lista da esquerda
  quickSort(arr, left, pivotIndex - 1);
  // Ordena recursivamente a sub-lista da direita
  quickSort(arr, pivotIndex + 1, right);

  return arr;
}

function partition(arr, left, right) {
  const pivotValue = arr[right]; // Usando o último elemento como pivô
  let partitionIndex = left;

  for (let i = left; i < right; i++) {
    if (arr[i] < pivotValue) {
      // Encontrou um elemento menor que o pivô, troca com o partitionIndex
      [arr[i], arr[partitionIndex]] = [arr[partitionIndex], arr[i]]; // Swap
      partitionIndex++;
    }
  }

  // Coloca o pivô na sua posição final correta
  [arr[partitionIndex], arr[right]] = [arr[right], arr[partitionIndex]]; // Swap
  return partitionIndex;
}
Tempo: O(n log n) no caso médio (o mais comum), mas O(n²) no pior caso (se os pivôs forem mal escolhidos, ex: em um array já ordenado).
Espaço: O(log n) - in-place (usa a pilha de recursão).
Estável: Não.

3. Tabela Resumo e Menções Honrosas

Aqui está um resumo para consulta rápida dos principais algoritmos e seus trade-offs:

AlgoritmoTempo (Médio)Tempo (Pior)EspaçoEstável?
Insertion SortO(n²)O(n²)O(1)Sim
Merge SortO(n log n)O(n log n)O(n)Sim
Quick SortO(n log n)O(n²)O(log n)Não

Outros Notáveis:

Heap Sort: Outro algoritmo O(n log n) que é in-place, uma boa alternativa ao Quick Sort para garantir performance sem o risco do pior caso O(n²).
Radix Sort / Counting Sort: Algoritmos que não se baseiam em comparações. Para tipos de dados específicos (como inteiros dentro de um certo range), eles podem ser mais rápidos que O(n log n), alcançando performance linear O(n+k).

4. Na Prática: .sort() e a Vida Real

No seu dia a dia, você provavelmente usará a função de ordenação nativa da sua linguagem (como Array.prototype.sort() em JavaScript). Essas funções são altamente otimizadas e geralmente implementam uma variação híbrida de Quick Sort, Merge Sort ou outro algoritmo eficiente (como o Timsort no Python e em muitas implementações de JS).

Então, por que aprender a teoria?

Para as entrevistas! Saber que o .sort() da sua linguagem é, em média, O(n log n) é o primeiro passo. Saber explicar por quê, e quais os trade-offs das abordagens clássicas (memória do Merge Sort vs. pior caso do Quick Sort), é o que demonstra sua profundidade como engenheiro(a).

💊 Pílula Devly

Binary Search é sua arma secreta para encontrar qualquer coisa em dados ordenados. Merge Sort e Quick Sort são os padrões-ouro da ordenação. Entender seus trade-offs (velocidade vs. memória, estabilidade vs. pior caso) é o que separa um dev júnior de um sênior na hora de discutir performance.

Anterior

Estruturas Lineares: Linked Lists, Pilhas e Filas

Voltar

Próximo

Pensando em Recursão (e o Labirinto do Backtracking)

Continuar