Anatomia da Notação Big‑O: Entenda a Complexidade de Algoritmos
Visão Rápida — A notação Big‑O é uma forma de medir quanto tempo e memória seu código pode consumir à medida que a entrada cresce. Dominar Big‑O é saber identificar padrões de custo para tomar decisões informadas.
1. Por que Big‑O importa?
- Escalabilidade previsível — evita surpresas quando
n
passa de 1 k para 10 M. - Linguagem comum — candidatos e entrevistadores discutem soluções usando Big‑O.
- Comparação objetiva — decide rapidamente qual implementação vale manter e otimizar.
2. Trindade Assintótica
Notação | Significado | Caso analisado |
---|---|---|
Ω (Big Omega) | Limite inferior | Melhor caso |
Θ (Big Theta) | Limite inferior e superior | Caso médio |
O (Big O) | Limite superior | Pior caso |
Regra de ouro: use Big‑O para garantir que seu algoritmo nunca excede determinado custo.
3. Casos de Complexidade
- Pior caso (Big O) — entrada que provoca máximo de trabalho.
- Caso médio (Θ) — desempenho típico para entradas aleatórias.
- Melhor caso (Ω) — raramente decisivo, mas serve de garantia mínima.
4. Como calcular Big‑O passo a passo
Mapeie as variáveis de entrada
Identifique tudo o que pode crescer:n
(tamanho do vetor),m
(número de arestas),k
(profundidade da árvore) – elas vão aparecer nas contas.Marque as operações‑chave
Conte comparações, atribuções, leituras de memória e chamadas de função que realmente impactam desempenho. Comentários e variáveis auxiliares sem custo assintótico podem ser ignorados.Avalie blocos lineares
Se dois trechos rodam um após o outro, soma‑se o custo:O(n) // loop A O(n^2) // loop B
Resultado → O(n²) porque o termo mais alto domina a soma.
Inclua condicionais no pior caso
Paraif / else
, some apenas o caminho mais caro:if found: # O(1) return else: # O(n) linear_search()
Big‑O = O(n).
Multiplique loops aninhados
Cada loop interno se multiplica pelo externo:for (i = 0; i < n; i++) // n for (j = 0; j < m; j++) // m op(); // 1 // → O(n · m)
Loops dependentes (‑até
j < i
) geram progressão aritmética →O(n²/2) = O(n²)
.Trate recursão como recorrência
Escreva a equação e resolva via:- Árvore de recursão – visualize níveis e conte nós.
- Teorema Mestre – para
T(n) = a T(n/b) + f(n)
. - Substituição – chute a solução e prove por indução.
Normalização & descarte de detalhes
Remova constantes e termos menores:3n + 7 → O(n) n/2 → O(n) log₂n → O(log n) // base muda por constante
Documente complexidade de espaço
Analise variáveis alocadas, pilha de recursão e buffers.
Ex.: Merge Sort usaO(n)
extra; Quick Sort in‑place usaO(log n)
de pilha.Cheque gargalos de I/O
Operações de disco/rede podem dominar o tempo de CPU – use Big‑O para CPU e para I/O quando necessário.Valide com experimento rápido
Usetime
,perf
oucProfile
para confirmar que o comportamento real segue a análise assintótica.
Mini‑exemplo completo
def foo(arr):
n = len(arr) # O(1)
total = 0 # O(1)
# Loop externo => n
for i in range(n):
total += arr[i] # O(1)
# Loop interno dependente => i
for j in range(i):
total += arr[j] # O(1)
return total
Receita:
- Loops aninhados → soma de série 1 + 2 + … + (n‑1) =
n(n‑1)/2
→O(n²)
. - Constantes ignoradas → O(n²) tempo, O(1) espaço.
Dica prática: ao revisar código, escreva o Big‑O no comentário de cada bloco. Isso treina o olhar e facilita code‑review de colegas iniciantes.
5. Tabela de Complexidade de Estruturas
Estrutura | Inserção | Remoção/Update | Busca/Consulta | Observações |
---|---|---|---|---|
Array estático | O(1) | O(n) | O(1) | Remoção desloca elementos |
Array dinâmico (vector, ArrayList) | Amort. O(1) | O(n) | O(1) | Realoca ao dobrar a capacidade |
Lista ligada | O(1) | O(1) | O(n) | Sem acesso aleatório |
Pilha (Stack) | O(1) push | O(1) pop | O(n) | Acesso ao topo é O(1) |
Fila (Queue) | O(1) enqueue | O(1) dequeue | O(n) | FIFO |
Deque | O(1) | O(1) | O(n) | Inserção/rem. em ambas extremidades |
Hash table / Hash map | O(1)¹ | O(1)¹ | O(1)¹ | ¹Média; colisões → O(n) |
Árvore balanceada (AVL, RB‑Tree) | O(log n) | O(log n) | O(log n) | Altura ≈ log₂ n |
B‑Tree (ordem m) | O(log n) | O(log n) | O(log n) | Otimizada para disco/SSD |
Heap (min/max) | O(log n) | O(log n) | O(1) topo | Implementa fila de prioridade |
Skip List | O(log n) | O(log n) | O(log n) | Probabilística; implementação simples |
Trie (Árvore de prefixos) | O(L) | O(L) | O(L) | L = tamanho da chave; busca por prefixo eficiente |
Segment Tree | O(log n) update | O(log n) update | O(log n) intervalo | Consulta/atualização de intervalos |
Fenwick Tree (BIT) | O(log n) update | O(log n) update | O(log n) prefix sum | Menos memória que Segment Tree |
Bloom Filter | O(k) | — | O(k) teste | Probabilístico: falsos‑positivos, sem remoção nativa |
Notas
- Amortizado: tempo médio por operação após muitas operações (caso do array dinâmico).
- k em Bloom Filter indica o nº de funções hash usadas.
- Para tries,
O(L)
é proporcional ao comprimento da palavra, não ao nº de chaves.
6. Complexidade por Gráfico
Observe como O(n!) explode comparado a O(n log n) — fundamental para escolhas arquiteturais.
7. Exemplos Comentados
Esta seção reúne trechos clássicos de código para ilustrar como cada classe de Big‑O se comporta na prática, tomando como base o artigo “Common Big‑O Notations” da GeeksforGeeks.
Complexidade | Classe | Exemplo resumido | Ideia‑chave |
---|---|---|---|
O(1) | Constante | Acesso a arr[i] | Tempo indep. de n |
O(n) | Linear | Procurar valor em array | Percorre todos os elementos |
O(log n) | Logarítmica | Binary Search | Divide a entrada pela metade |
O(n log n) | Quasi‑linear | Merge Sort | Divide & conquista + merge |
O(n²) | Quadrática | Bubble Sort, loops duplos | Comparações par a par |
O(n³) | Cúbica | Multiplicação de matrizes (ingênua) | Três loops aninhados |
O(2ⁿ) | Exponencial | Geração de subconjuntos | Cresce dobrando a cada elemento |
O(n!) | Fatorial | Todas as permutações | Número explode além do exponencial |
7.1 Linear Time — O(n)
bool findElement(int arr[], int n, int key) {
for (int i = 0; i < n; ++i)
if (arr[i] == key) return true;
return false;
}
Executa uma comparação por elemento → cresce linearmente.
7.2 Logarithmic Time — O(log n)
def binary_search(arr, x):
l, r = 0, len(arr) - 1
while l <= r:
mid = (l + r) // 2
if arr[mid] == x:
return mid
l, r = (mid + 1, r) if arr[mid] < x else (l, mid - 1)
return -1
Cada passo descarta metade da entrada → curva logarítmica.
7.3 Quadratic Time — O(n²) (Nested Loops)
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
process(i, j); // n × n chamadas
Segundo loop roda n
vezes para cada volta do primeiro → n²
.
7.4 Cúbica — O(n³)
void multiply(int A[][N], int B[][N], int C[][N]) {
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j) {
C[i][j] = 0;
for (int k = 0; k < N; ++k)
C[i][j] += A[i][k] * B[k][j];
}
}
Três loops aninhados sobre N
→ N³
operações.
7.5 Exponencial — O(2ⁿ)
void generateSubsets(int arr[], int n) {
for (int mask = 0; mask < (1 << n); ++mask) {
for (int j = 0; j < n; ++j)
if (mask & (1 << j)) cout << arr[j] << ' ';
cout << '\n';
}
}
Há 2ⁿ
máscaras possíveis, logo tempo dobra a cada incremento de n
.
7.6 Fatorial — O(n!)
void permute(int a[], int l, int r) {
if (l == r) { print(a, r); return; }
for (int i = l; i <= r; ++i) {
swap(a[l], a[i]);
permute(a, l + 1, r); // n! permutações
swap(a[l], a[i]); // backtrack
}
}
Número de permutações de n
itens é n!
, logo tempo explode rapidamente.
7.7 Master Theorem Express
Para recursões da forma T(n) = a T(n/b) + f(n)
:
Caso | Relação f(n) vs. n^{log_b a} | Complexidade |
---|---|---|
1 | f(n) assintoticamente menor | Θ(n^{log_b a}) |
2 | f(n) igual | Θ(n^{log_b a} · log n) |
3 | f(n) maior | Θ(f(n)) |
Takeaway: Algoritmos acima de
O(n log n)
já podem se tornar gargalos em escala. Use esta lista como checklist mental na hora de propor soluções ou revisar PRs.
8. Espaço versus Tempo
Big‑O também mede memória.
Merge Sort: O(n log n)
tempo, O(n)
espaço.
Heap Sort: O(n log n)
tempo, O(1) espaço — troca mais comparação por menos RAM.
9. Armadilhas Frequentes
Cenário | Erro comum | Solução |
---|---|---|
Fibonacci recursivo | O(2ⁿ) | Memorizar → O(n) |
std::vector::insert | Achar que é O(1) | Desloca elementos → O(n) |
Hash sem tratamento | Supor O(1) sempre | Use chaining ou open addressing |
10. Guia de Primeiros Passos (Roadmap Para Iniciantes)
- Domine uma linguagem (Python/Repl.it é ótimo para feedback rápido).
- Assista 2 vídeos curtos sobre notação Big‑O (GfG & Computerphile).
- Analise 5 trechos de código do seu dia‐a‐dia — escreva a complexidade em um caderno.
- Implemente buscas & sorts do zero (Linear, Binária, Bubble, Merge).
- Use plataformas gamificadas (HackerRank “Time Complexity” warm‑up).
- Participe de um contest curto (Codeforces Div. 4) — força você a otimizar rápido.
11. Exercício Guiado
Objetivo: converter um algoritmo ingênuo
O(n²)
emO(n)
.
Passo 1 — Código inicial
def count_pairs(arr):
count = 0
for i in range(len(arr)):
for j in range(i+1, len(arr)):
if arr[i] == arr[j]:
count += 1
return count
Passo 2 — Diagnóstico
Dois loops aninhados → quadrático.
Passo 3 — Otimização com Hash
from collections import Counter
def count_pairs(arr):
freq = Counter(arr) # O(n)
return sum(c * (c-1) // 2 for c in freq.values()) # O(k)
Agora o algoritmo roda em O(n) tempo e O(k) espaço.
12. Mini‑FAQ
Q: Por que ignoramos constantes?
A: Porque em escala grande, fatores proporcionais são irrelevantes frente ao crescimento assintótico.
Q: Big‑O mede tempo de qual operação?
A: Qualquer métrica de custo: CPU, memória, I/O. Especifique qual está analisando.
Q: Preciso decorar todas as classes?
A: Não. Entenda o estilo de curva: constante, log, linear, quadrática, exponencial.
13. Glossário Rápido
- Entrada (n) — quantidade de dados processados.
- Dominante — parte do código que mais contribui para o custo.
- Assintótico — tendência quando
n → ∞
. - Overhead — custo extra além da lógica principal (ex.: alocação).
14. Checklist de Entrevista
- Qual o pior caso?
- Há estimativa realista do caso médio?
- Complexidade de espaço?
- Estrutura de dados alternativa?
- Pode paralelizar?
- Trade‑off tempo × memória aceitável?
15. Conclusão
A notação Big‑O é a régua que ajuda a identificar gargalos antes que eles explodam em produção. Com um olhar crítico e prática constante, você passará a reconhecer padrões complexos à primeira vista — e otimizar à segunda.
Desafio Final: Pegue um script seu de ontem, estime Big‑O, depois meça com
time
. Quanto as estimativas batem com a realidade?