Módulo 3 · Algoritmos e Estruturas de Dados — Capítulo 01

Complexidade de Algoritmos e Notação Big O

Por que dois algoritmos que dão o mesmo resultado podem ser completamente diferentes na prática — e como medir isso sem precisar rodar o código.

1. "Funciona" não é a mesma coisa que "funciona bem"

Até aqui, nos módulos anteriores, o critério de sucesso de um algoritmo foi simples: ele chega na resposta certa? Esse critério continua valendo, mas a partir de agora entra um segundo critério, igualmente importante: quão rápido ele chega lá quando a quantidade de dados cresce.

Isso parece um detalhe de otimização prematura, mas não é. Um algoritmo que percorre uma lista de 100 itens em uma fração de milissegundo pode, com uma pequena mudança na forma como ele está escrito, levar minutos para processar uma lista de 1 milhão de itens — mesmo fazendo, essencialmente, "a mesma coisa". A diferença não está no que o algoritmo faz, está em como o número de operações cresce conforme a entrada cresce.

É exatamente isso que a complexidade de algoritmos mede, e a notação usada para expressar essa medida chama-se Big O (lê-se "ó grande" ou "big ó").

Nota

Isso não é exclusivo de nenhuma linguagem. Um loop em JavaScript, um loop em Python e um loop em C# têm exatamente a mesma complexidade se fazem a mesma coisa — a notação Big O descreve o algoritmo, não a linguagem em que ele foi escrito.

2. A ideia intuitiva por trás do Big O

Big O responde a uma pergunta específica: se eu dobrar (ou multiplicar por 10, ou por 1000) o tamanho da entrada, o que acontece com o número de operações que o algoritmo executa? Não interessa o tempo exato em segundos — isso depende do processador, da linguagem, de quantos outros programas estão rodando ao mesmo tempo. O que interessa é a taxa de crescimento.

Por convenção, chamamos o tamanho da entrada de n. A pergunta "quantas operações esse algoritmo faz?" vira uma função de n, e o Big O descreve o formato dominante dessa função quando n fica muito grande — ignorando constantes e termos menores, porque eles perdem relevância diante do crescimento.

Pensando em ordens de grandeza

Não é preciso contar operação por operação para usar Big O no dia a dia. A pergunta prática é: "para processar isso, eu preciso olhar cada item uma vez? Duas vezes por item? Comparar cada item com todos os outros?" A resposta a essa pergunta já entrega a complexidade.

3. O(1) — tempo constante

Um algoritmo é O(1) quando o número de operações não depende do tamanho da entrada. Não importa se a entrada tem 10 ou 10 milhões de itens: o algoritmo faz a mesma quantidade de trabalho.

PSEUDOCÓDIGO acesso-direto.txt
FUNÇÃO primeiroItem(lista)
    RETORNE lista[0]
FIM FUNÇÃO

Pegar o primeiro item de um vetor é O(1): é um acesso direto por posição, não importa o tamanho do vetor. Verificar se um número é par ou ímpar também é O(1) — é uma única operação, sempre.

4. O(n) — tempo linear

Um algoritmo é O(n) quando o número de operações cresce na mesma proporção que o tamanho da entrada. Dobrou a entrada, dobrou (aproximadamente) o trabalho. O caso mais comum de O(n) é um único laço que passa por cada item da entrada uma vez.

PSEUDOCÓDIGO soma-vetor.txt
FUNÇÃO soma(vetor)
    total ← 0
    PARA CADA item EM vetor
        total ← total + item
    FIM PARA
    RETORNE total
FIM FUNÇÃO

Se o vetor tem 10 itens, o laço roda 10 vezes. Se tem 1 milhão, roda 1 milhão de vezes. O trabalho cresce linearmente junto com n.

5. O(n²) — tempo quadrático

Um algoritmo é O(n²) quando, para cada item da entrada, ele precisa olhar todos os outros itens de novo. O padrão mais comum é um laço dentro de outro laço, ambos percorrendo (aproximadamente) a mesma entrada.

PSEUDOCÓDIGO pares-duplicados.txt
FUNÇÃO temDuplicado(vetor)
    PARA i DE 0 ATÉ tamanho(vetor) - 1
        PARA j DE 0 ATÉ tamanho(vetor) - 1
            SE i ≠ j E vetor[i] = vetor[j] ENTÃO
                RETORNE VERDADEIRO
            FIM SE
        FIM PARA
    FIM PARA
    RETORNE FALSO
FIM FUNÇÃO

Para cada item (n possibilidades), o algoritmo compara com todos os outros itens (mais n possibilidades). O total de comparações é aproximadamente n × n = n². Com 10 itens, isso são 100 comparações no pior caso. Com 1000 itens, já são 1 milhão.

Atenção

Laços aninhados nem sempre significam O(n²) — só significam isso quando os dois laços percorrem uma entrada de tamanho proporcional a n. Um laço que roda n vezes dentro de outro que roda sempre 5 vezes, fixo, é O(n), não O(n²) — o 5 é uma constante e desaparece na notação Big O.

6. O(log n) — tempo logarítmico

Um algoritmo é O(log n) quando, a cada passo, ele consegue descartar metade dos dados restantes em vez de olhar item por item. É uma das complexidades mais eficientes que existem para problemas em que "olhar tudo" pareceria inevitável.

A intuição: pense em procurar uma palavra num dicionário físico. Você não folheia página por página a partir da primeira. Você abre no meio, vê se a palavra vem antes ou depois, descarta a metade errada, e repete o processo na metade que sobrou. A cada passo o problema fica pela metade do tamanho — por isso o crescimento é logarítmico, não linear. O capítulo 3 detalha esse algoritmo (busca binária) com pseudocódigo completo.

O que importa reter agora é a escala: dobrar o tamanho da entrada em um algoritmo O(log n) adiciona só mais um passo ao processo, não o dobro de passos.

7. Comparando o crescimento na prática

A tabela abaixo mostra, para diferentes tamanhos de entrada, aproximadamente quantas operações cada classe de complexidade exige. Os números tornam concreto por que essa diferença importa tanto quando os dados crescem.

Complexidaden = 10n = 1.000n = 1.000.000
O(1) — constante111
O(log n) — logarítmica≈ 3≈ 10≈ 20
O(n) — linear101.0001.000.000
O(n log n) — linearítmica≈ 33≈ 10.000≈ 20.000.000
O(n²) — quadrática1001.000.0001.000.000.000.000

Repare no salto: com 1 milhão de itens, um algoritmo O(n) faz 1 milhão de operações — factível em uma fração de segundo. Um algoritmo O(n²) faria 1 trilhão de operações no mesmo cenário, o que pode significar horas de processamento. A mesma tarefa, resolvida de duas formas diferentes, pode ser a diferença entre "instantâneo" e "inviável".

Nunca ignore isso ao trabalhar com dados grandes

Código que roda rápido em teste com 20 registros e roda em produção com 2 milhões de registros é uma das causas mais comuns de sistemas que "funcionavam bem" e começam a travar depois de meses em produção. Antes de escrever um laço aninhado sobre uma coleção que pode crescer, pergunte: existe uma forma de fazer isso com uma passada só?

8. Melhor caso, pior caso e caso médio

Um mesmo algoritmo pode ter comportamentos diferentes dependendo dos dados de entrada. Buscar um item numa lista pode encontrar o valor na primeira posição (melhor caso, O(1) nessa execução específica) ou só na última posição, ou nunca (pior caso, O(n)). Quando falamos da complexidade de um algoritmo sem qualificar, normalmente estamos nos referindo ao pior caso — é a garantia mais segura de se basear, porque é o teto do que pode acontecer.

📌 Resumo do capítulo

  • Complexidade mede como o número de operações de um algoritmo cresce conforme a entrada (n) cresce — não o tempo exato em segundos.
  • Notação Big O descreve essa taxa de crescimento, ignorando constantes e termos menores.
  • O(1) — constante: mesmo número de operações independente do tamanho da entrada (ex: acessar por índice).
  • O(n) — linear: um laço simples que passa por cada item uma vez.
  • O(n²) — quadrática: laços aninhados percorrendo a mesma entrada (comparar cada item com todos os outros).
  • O(log n) — logarítmica: descarta metade dos dados a cada passo (ex: busca binária).
  • Com entradas grandes, a diferença entre essas classes deixa de ser sutil e passa a ser a diferença entre "instantâneo" e "inviável".
  • Complexidade "pior caso" é a referência padrão por ser a garantia mais segura.

✏️ Praticando

  1. Classifique a complexidade (O(1), O(n) ou O(n²)) de: (a) verificar se o primeiro elemento de um vetor é zero; (b) somar todos os elementos de um vetor; (c) verificar se um vetor tem algum par de elementos iguais comparando todos com todos.
  2. Escreva em pseudocódigo uma função que recebe um vetor e retorna o maior valor. Qual é a complexidade dela? Justifique contando quantas vezes o laço executa.
  3. Um colega escreveu um algoritmo com três laços aninhados, todos percorrendo o mesmo vetor de tamanho n. Qual complexidade isso gera? (Dica: siga o mesmo raciocínio usado para chegar em O(n²).)
  4. Usando a tabela da seção 7 como referência, explique com suas palavras por que um algoritmo O(n log n) é considerado "quase tão bom quanto" O(n) para a maioria dos propósitos práticos, mesmo n log n sendo maior que n.
  5. Pense em uma tarefa do seu dia a dia como programador JS/Node (por exemplo, filtrar uma lista de usuários) e identifique se a forma como você normalmente resolve isso é O(n) ou O(n²).