Notação Big O

10 min de leituraFácilPython

Big O é o jeito de responder "esse algoritmo aguenta?" sem rodar cronômetro. Em vez de medir segundos, que dependem da sua máquina, ele mede como o número de operações cresce quando a entrada cresce. É a primeira ferramenta do roadmap porque, daqui para frente, todo tópico volta a ela.

O que o Big O mede

Todo algoritmo recebe uma entrada. Chamamos de n o tamanho dela: o número de posições de um array, a quantidade de nós de uma árvore, o número de linhas de uma tabela. A pergunta do Big O é sempre a mesma:

Se n dobrar, o que acontece com o trabalho que o algoritmo faz?

Repare no que ficou de fora. O Big O não diz quantos segundos algo leva, porque isso depende de processador, memória, linguagem e compilador. Ele tira a máquina da equação e classifica o algoritmo em uma família de crescimento. Dois algoritmos O(n) podem ter tempos bem diferentes na prática, mas os dois reagem do mesmo jeito quando a entrada aumenta.

Complexidade de tempo

Quantas operações o algoritmo executa em função de n. "Tempo" aqui é contagem de passos, não relógio de parede.

Complexidade de espaço

Quanta memória extra ele precisa além da entrada: arrays auxiliares, hashes, a pilha da recursão. Também se mede em Big O.

Existem outras notações para a mesma família de perguntas (Ω para o limite inferior, Θ quando os dois limites coincidem), mas Big O virou o padrão porque é o limite superior: a promessa de que, a partir de um certo n, o algoritmo não passa disso.

As três regras

Na prática você quase nunca precisa da definição formal. Três regras resolvem quase todo caso do dia a dia.

1. n é o tamanho da entrada. Quando aparecem duas entradas independentes, elas ganham letras próprias: percorrer dois arrays diferentes é O(n + m), não O(2n).

2. Constantes somem. Um algoritmo que faz 3n operações e outro que faz n estão na mesma família: os dois são O(n). Multiplicar por 3, ou dividir por 2, desloca a curva mas não muda o formato dela. Processar o array pela metade continua sendo O(n).

3. Fica só o termo dominante. Em uma soma, o termo de maior grau engole os outros:

Python
# 4n³ + 3n² + n + 1000  →  O(n³)

Cuidado com o outro lado da regra 3: ela vale para soma, não para multiplicação. Dois laços em sequência somam, e o resultado é O(n). Um laço dentro do outro multiplica, e vira O(n²).

Python
for x in nums:      # n operações
    ...
for x in nums:      # + n operações  →  O(n + n) = O(n)
    ...

for x in nums:          # n vezes...
    for y in nums:      # ...n operações cada  →  O(n × n) = O(n²)
        ...

Termos com variáveis diferentes não se cancelam. n² + k² continua O(n² + k²), porque você não sabe qual dos dois é maior. Só simplifique quando souber que uma entrada domina a outra.

As famílias, do O(1) ao O(n!)

Esta é a tabela que vale colar na parede. Repare menos nos nomes e mais na coluna de n = 1.000: é ali que as famílias deixam de ser parecidas.

Famílias de complexidade e o número de operações no pior caso
FamíliaExemplo típicon = 10n = 100n = 1.000
O(1)
constante
acessar nums[i], ler de um hashescala liso111
O(log n)
logarítmica
busca binária, índice B-treeescala liso4710
O(n)
linear
percorrer o array uma vezescala bem101001.000
O(n log n)
linearítmica
merge sort, quick sort, qualquer Order Byescala bem336649.966
O(n²)
quadrática
dois laços aninhados, comparar todos os parescuidado10010 mil1 milhão
O(n³)
cúbica
três laços aninhados, Floyd-Warshallcuidado1.0001 milhão1 bilhão
O(2ⁿ)
exponencial
fibonacci recursivo sem memo, subconjuntosinviável1.02410³⁰10³⁰¹
O(n!)
fatorial
permutações, caixeiro viajante por força brutainviável3,6 mi10¹⁵⁸10²⁵⁶⁸
Cada linha é uma família, não um algoritmo. Dois algoritmos O(n) podem ter tempos de execução bem diferentes: o que a família garante é o formato da curva quando n cresce.

O gráfico abaixo é a mesma tabela em movimento. Ligue e desligue as famílias, arraste o marcador sobre a curva e, principalmente, aumente a entrada máxima: o que parecia dramático com n = 10 vira uma linha colada no chão quando n chega a 1.000. É esse efeito que faz a constante ser descartada.

Visualizador · como cada família cresce
n = 62

Com n = 62, as famílias marcadas fazem 1 operações em O(1), 6 operações em O(log n), 62 operações em O(n), 369 operações em O(n log n) e 3.844 operações em O(n²). Arraste sobre o gráfico, ou use as setas do teclado, para mover o marcador.

O(1)1
acesso por índice
O(log n)6
busca binária
O(n)62
busca linear
O(n log n)369
merge sort
O(n²)3.844
dois laços aninhados
Entrada máxima: n até 100

Ligue O(2ⁿ) e O(n!) e veja o gráfico inteiro achatar: do lado dessas duas, todas as outras viram uma linha colada no chão. Aí troque para a escala logarítmica e diminua a entrada máxima para 25. Cada família vira uma inclinação diferente, e dá para comparar as oito de uma vez.

Dois detalhes que confundem no começo:

  • O log do Big O é base 2, não base 10. A busca binária corta o espaço de busca ao meio a cada passo, então a conta natural é log₂. Com 1.024 posições são 10 passos; com 1 milhão, 20.
  • O(1) não quer dizer "uma operação". Atualizar três campos de um objeto são três operações, mas continuam sendo três com qualquer entrada. Constante é isso: não reage ao tamanho de n.

Contando operações no mesmo array

Teoria bonita, mas o que convence mesmo é ver o contador. Abaixo, quatro algoritmos rodando sobre o mesmo array de 8 posições. Troque entre eles e compare o número final de operações, e também o campo "pior caso com n = 16".

Visualizador · contando operações no mesmo array
passo 1 de 7
0
2
E
1
6
·
2
10
·
3
15
·
4
20
·
5
43
·
6
60
·
7
70
D
operações até aqui0
n (tamanho da entrada)8
pior caso com n = 84
pior caso com n = 165

O array precisa estar ordenado. Procurando 20 entre as posições 0 e 7.

busca_binaria.py · log₂(n) operações
1def busca_binaria(nums, alvo):
2 esq, dir = 0, len(nums) - 1
3 while esq <= dir:
4 meio = (esq + dir) // 2
5 if nums[meio] == alvo:
6 return meio
7 if nums[meio] < alvo:
8 esq = meio + 1
9 else:
10 dir = meio - 1
11 return -1
Variáveis
esq0
dir7
operações0

passo · espaço roda

O que você deveria ver ao rodar os quatro:

AlgoritmoPior caso com n = 8Pior caso com n = 16Família
Acesso por índice11O(1)
Busca binária45O(log n)
Busca linear816O(n)
Todos os pares28120O(n²)

Dobrar a entrada não fez nada com o primeiro, somou 1 no segundo, dobrou o terceiro e praticamente quadruplicou o quarto. Essa é a diferença entre famílias, e é por isso que a discussão sobre performance começa aqui, não no micro-otimizador de linha de código.

Repare também na busca binária procurando o 20: ela acerta em 3 passos, contra os 5 da busca linear. Com 1 milhão de posições seriam 20 passos contra 1 milhão. É o mesmo motivo pelo qual um índice de banco de dados transforma um full scan em algo instantâneo.

Melhor caso, caso médio e pior caso

O mesmo algoritmo pode ter três respostas dependendo da entrada que chega.

Melhor caso

A busca linear acha o alvo na primeira posição: O(1). Um Bubble Sort otimizado recebe o array já ordenado e faz uma passada só: O(n). Bonito, e raro.

Pior caso

O alvo não existe e a busca linear percorre tudo: O(n). É o que se assume por padrão, inclusive em entrevista, quando ninguém especifica qual caso.

O caso médio é o que costuma governar a decisão de arquitetura, mas é o mais difícil de calcular, porque depende da distribuição real dos dados. Por isso o pior caso ganha tanto espaço: ele é o contrato, a garantia de que pior do que aquilo não fica.

Algumas estruturas existem justamente para eliminar essa distância. Uma árvore AVL se rebalanceia a cada inserção para que nenhum ramo fique muito mais alto que o outro, e com isso busca, inserção e remoção ficam em O(log n) no pior caso, não só na média. Uma BST comum, sem rebalanceamento, degenera para uma lista e cai para O(n) quando os dados chegam já ordenados.

As armadilhas que pegam todo mundo

Eficiente não é a mesma coisa que rápido. Levar 500 TB de dados num caminhão de São Paulo ao Rio é O(1): o tempo não muda se você colocar mais um disco. Mandar pela internet é O(n): dobrou o volume, dobrou o tempo. Para 1 GB a internet ganha fácil; para 500 TB, o caminhão. Complexidade fala de escala, o custo absoluto de cada operação continua importando.

Ordenar para depois buscar quase sempre é pior. A tentação é clássica: "busca binária é O(log n), muito melhor que O(n), então vou ordenar e buscar". Só que ordenar custa O(n log n), e n log n + log n continua sendo O(n log n), que é pior que a busca linear O(n). A conta só vira quando você ordena uma vez e busca muitas: aí o custo inicial se dilui, que é exatamente o raciocínio por trás de um índice de banco de dados.

Com entrada pequena, a família não decide nada. Ordenar as 26 letras do alfabeto vai ser instantâneo com qualquer algoritmo. Se o problema garante que n é pequeno e limitado, escolha o código mais simples de ler e manter. O Big O só cobra a conta quando a escala aparece.

Complexidade não é legibilidade. Um algoritmo O(n²) pode ser trivial de entender e um O(n) pode ser um quebra-cabeça. São eixos diferentes, e "complexo" no sentido do Big O não quer dizer "complicado de ler".

Quando o problema é naturalmente O(2ⁿ) ou O(n!), como o caixeiro viajante ou montar todas as permutações, não existe truque de implementação que salve. O caminho passa a ser outro: heurísticas, aproximações, programação dinâmica ou podar o espaço de busca com backtracking. Uma boa solução rápida costuma valer mais que a ótima que nunca termina.

O próximo passo é aplicar isso em algo concreto: veja como Two Pointers transforma um O(n²) em O(n), e como a Sliding Window evita recalcular o que já foi calculado.

Vídeo da aula

Direto do canal da comunidade Craft & Code Club · 1:38:08.

Problemas para praticar

Na ordem em que recomendamos resolver. Marque os que você já fez, fica salvo aqui.

FácilBinary SearchLeetCode 704
FácilContains DuplicateLeetCode 217
FácilTwo SumLeetCode 1
FácilFibonacci NumberLeetCode 509

Referências

Artigos e materiais externos para se aprofundar.

Travou em algum passo? Traga sua questão para o Discord da comunidade ou para os encontros semanais.

Entrar
Concluiu este tópico?
Marque para acompanhar seu progresso.

Este tópico também tem página própria, fora deste roadmap: Notação Big O.