Uma introdução visual à notação Big O
(samwho.dev)- Notação Big O expressa o desempenho de uma função pelo padrão de crescimento conforme a variação do tamanho da entrada
- O texto explica, com exemplos, os principais tipos de Big O: constante, logarítmico, linear e quadrático
- Dependendo da estrutura de dados e do algoritmo, a complexidade de tempo muda, mostrando diferenças em tarefas como ordenação e busca em arrays
- Para melhorar o desempenho do código na prática, os pontos centrais são escolher a estrutura de dados adequada e eliminar operações desnecessárias dentro de loops
- Big O sempre representa, da forma mais simplificada possível, a relação entre entrada e tempo de execução, e ao otimizar desempenho é importante medir o código diretamente
Visão geral da notação Big O
- Notação Big O é uma forma de descrever o padrão de crescimento do tempo de execução conforme o tamanho da entrada (n), em vez de medir diretamente o tempo
- Ela classifica o tempo de execução de uma função de acordo com a entrada, e normalmente analisa formas como constante (O(1)), logarítmica (O(log n)), linear (O(n)) e quadrática (O(n²))
- Este texto explica cada categoria por meio de conceitos, exemplos visuais e exemplos de código reais, de forma acessível até para iniciantes
Iteração (Iterating) e algoritmos lineares
- A função
sum(n)é um exemplo de estrutura iterativa que soma de 1 até n, e quanto maior o valor de entrada n, maior o tempo de execução em proporção direta - Na prática,
sum(1e9)leva cerca de 1 segundo, esum(2e9)cerca de 2 segundos, então o tempo de relógio (wall-clock time) cresce em um padrão O(n) - Complexidade de tempo é a relação entre a entrada da função e o tempo de execução, e isso é expresso com notação Big O (O(n) — proporcional a n)
- Em vez de iterar, usar a fórmula matemática
sum(n) = (n*(n+1))/2faz com que o tempo de execução seja constante, independentemente do valor de entrada n - Esse tipo de função tem complexidade de tempo constante O(1), cuja característica é não apresentar crescimento no tempo de execução conforme a entrada muda
Sintaxe da notação Big O
- O O de Big O vem de “Order” (ordem de crescimento) e indica apenas a forma de crescimento em si
- Ela não representa o valor absoluto do tempo de execução, mas sim apenas o 'padrão' de crescimento em relação à entrada, de forma concisa
- Por exemplo, mesmo para uma função O(n), não se escreve de forma detalhada como 'O(2n)' ou 'O(n+1)'; escolhe-se apenas o termo mais simples e dominante
Redução de tempo usando a composição da entrada
- Como no exemplo da fórmula de
sum(n), é possível transformar a complexidade de tempo de O(n) em O(1) ao melhorar o algoritmo - Ainda assim, ter complexidade constante não significa automaticamente ser mais rápido; o tempo total de execução pode variar conforme o tipo de operação
- Um algoritmo O(n) pode ser mais rápido do que um O(1) para certas entradas específicas, mas à medida que a entrada cresce, o método O(1) sempre tende a levar vantagem
Ordenação (Sorting) e algoritmos quadráticos: exemplo com Bubble Sort
- Bubble Sort é um exemplo básico de ordenação que organiza um array repetindo trocas entre elementos adjacentes
- Quando o array já está ordenado, basta 1 passagem (O(n)); em ordem inversa, é preciso percorrer repetidamente n vezes → no pior caso, o total de operações é n²
- Um algoritmo O(n²) tem tempo de execução que cresce fortemente de forma quadrática à medida que a entrada aumenta
- Na prática, Big O normalmente considera o pior caso (worst-case), embora em alguns contextos também se indiquem média e melhor caso
- Dependendo do estado inicial do array, o número de repetições pode diminuir, mas considerando o pior caso, ele continua sendo classificado como complexidade quadrática
Busca (Searching) e algoritmos logarítmicos: exemplo com busca binária
- Busca binária (Binary Search) estima o valor central de um intervalo ordenado e, em cada etapa, elimina metade da área candidata
- Por exemplo, para adivinhar um número específico entre 1 e 100, são necessárias no máximo 7 tentativas; entre 1 e 1 bilhão, menos de 31 tentativas
- Como a lista de candidatos diminui pela metade a cada etapa, o tempo de execução é O(log n) (complexidade logarítmica)
- Algoritmos logarítmicos têm crescimento extremamente lento quando n aumenta, sendo muito mais eficientes do que os lineares ou quadráticos
- Ao comparar gráficos, a diferença de crescimento entre log n, n e n² aparece de forma muito clara
Aplicação prática: dicas para melhorar a complexidade de tempo
Encontrar um item em uma lista
- Em geral, uma função que procura um valor em um array é O(n)
- Se a busca for frequente, usar uma estrutura de dados como Set pode melhorar para O(1)
- Porém, o próprio processo de conversão com
new Set(array)é O(n), então isso só vale a pena para consultas frequentes (considerando o custo da conversão) - Exemplo:
items.has("banana")oferece complexidade de tempo constante
Escrever loops aproveitando o índice
-
Código que usa
.indexOfdentro do loop, como abaixo, costuma ser uma causa comum de problema de desempenhofunction buildList(items) { const output = []; for (const item of items) { const index = items.indexOf(item); output.push(`Item ${index + 1}: ${item}`); } return output.join("\n"); } -
Como
.indexOfé uma operação O(n) dentro do loop, o padrão total se torna O(n^2) -
Ao usar iteração baseada em índice ou
forEach((item, index) => ...), é possível melhorar para O(n)function buildList(items) { const output = []; for (let i = 0; i < items.length; i++) { output.push(`Item ${i + 1}: ${items[i]}`); } return output.join("\n"); }
Uso de memoization
-
Em estruturas com cálculos repetidos em chamadas sucessivas, como no fatorial, é possível melhorar o desempenho aplicando cache de resultados (usando
Map) -
A consulta em
Mapé O(1), o que minimiza recálculos desnecessários -
Ainda assim, o cache contribui para melhorar o tempo médio e, mesmo que a complexidade no pior caso não mude, pode trazer ganho real de eficiência
const cache = new Map(); function factorial(n) { if (cache.has(n)) { return cache.get(n); } if (n === 0) { return 1; } const result = n * factorial(n - 1); cache.set(n, result); return result; }
Avaliação de desempenho e conclusão
- Ao melhorar o desempenho de um código, além da complexidade de tempo teórica, é preciso confirmar a melhoria real com testes de execução diretos
- Big O expressa, da forma mais essencial e simplificada possível, a relação e o padrão de crescimento entre entrada e tempo de execução
- Escolher bons algoritmos e otimizar a estrutura de dados permite maximizar a eficiência do código
Resumo
- Notação Big O expressa a relação entre o valor de entrada de uma função e o tempo de execução
- Principais categorias de desempenho: O(1) (constante), O(log n) (logarítmica), O(n) (linear), O(n^2) (quadrática)
- Para escrever código eficiente, é importante usar o algoritmo adequado e otimizar os loops
- O desempenho real deve ser medido diretamente para validar se houve melhoria
- Usar gráficos comparativos de crescimento ajuda a entender rapidamente as características da complexidade de tempo
1 comentários
Comentários no Hacker News
Este artigo, junto com os comentários no HN, continua a tradição de explicar Big O Notation e discutir sua aplicação prática e detalhes técnicos. Como exemplos úteis, há este texto explicativo e este texto sobre a postura de especialistas
O(1) na prática usa uma função de hash, o que não é simples, mas ainda assim envolve um custo constante de operação. Se os dados forem muito pequenos, até um algoritmo péssimo como O(n^2) pode ser mais rápido em tempo real
modTenho a impressão de que a importância moderna de Big-O não é a mesma de antes. O hardware atual tem multithreading, pipeline, NUMA, caches complexos e afins, então há operações que terminam em menos de um ciclo e outras que podem levar centenas ou milhares de ciclos. Se você tentar explicar algoritmos só pelo número de iterações no innermost loop, acaba distorcendo a realidade. E, ao falar de Big-O, também é obrigatório mencionar outras notações, como Big-Omega. (Aliás, também gostei da animação sobre Big-O)
O realmente interessante é que, em computação quântica, certas operações crescem como O(n^7) em relação ao número de átomos, mas os cientistas não têm medo de executar esse cálculo na prática. Isso porque N é pequeno o bastante, computadores e memória continuam ficando mais rápidos, e o valor do resultado é enorme. (Não sou especialista em ciência da computação, então perdoem se usei a notação O() de forma errada)
Gostei muito da visualização. Mesmo tendo estudado algoritmos no passado, continuar vendo isso de forma visual ainda ajuda bastante
Talvez por eu ter cursado engenharia elétrica, sempre senti que Big O Notation era tratado como um conceito meio passado por cima. Sempre era algo apresentado como se fosse óbvio, então acho que nunca vi uma explicação realmente gentil e detalhada. Fico curioso sobre em que nível de matemática ou computação esse conceito costuma ser introduzido pela primeira vez
A visualização dinâmica ajudou demais a entender. Espero que façam mais lições e materiais assim
Toda vez que aparece uma thread sobre Big-O Notation, eu sempre espero que alguém explique como esse conceito se conecta ao anime The Big O. Até hoje eu não entendo muito bem sobre o que aquele anime é
Pessoalmente, acho que a forma mais eficaz de entender Big O Notation é conectá-la com analogias do cotidiano
Acho que é um material bonito. Enviei um sinal e espero que tenha chegado bem; fiquei com aquela sensação de ter ganhado uma colherada de dopamina à toa