Entenda a complexidade de algoritmos com a notação Big-O.

Yuri Carvalho
Editorial 20 21
Published in
6 min readJul 27, 2020

Qual imagem vem à sua mente quando falamos da complexidade de um algoritmo? Imensas linhas de códigos? Ou talvez um código que demande várias leituras para entender seu funcionamento? Bom, para de fato entender a complexidade e eficiência de algoritmos, precisamos elucidar alguns conceitos:

A performance de um algoritmo depende do números de passos (ou etapas) que ele leva até cumprir sua tarefa. Estudiosos da área pegaram emprestado o termo Big O notation da matemática para descrever precisamente a eficiência de um algoritmo, possibilitando então analisar diferentes abordagens para um mesmo problema e enfim, decidir qual cumpre o seu papel de forma mais eficiente.

Introdução

Complexidade de tempo

Ao invés de focar em unidades de tempo, a notação Big-O coloca o número de passos em foco, logo, o fator do hardware que está rodando o código não entra em questão. Portanto, não estamos falando de tempo de execução e sim de sua complexidade ao longo do tempo.

A definição da notação Big-O

Pode-se determinar a notação Big-O como: a maneira que um algoritmo responde à diferentes tamanhos de um determinado tipo de dado. Por exemplo, como sua performance se difere quando passamos 10.000 elementos ao invés de apenas 1 elemento.

O grande O significa “Ordem de”, então ao se deparar com a notação O(N), podemos ler “De ordem N” — é uma aproximação da duração do algoritmo dado N elementos de entrada. Isso responde a seguinte questão: Como o número de etapas até completar sua tarefa muda, ao passo que os elementos de entrada crescem?

O(N) descreve quantas etapas um algoritmo necessita com base no número de elementos sobre o qual ele atua.

O melhor x pior cenário

Começando com um simples exemplo: Dado um vetor unidimensional de entrada array[N] , e um valor X , nosso algoritmo irá buscar o valor X percorrendo o vetor do início até o valor ser encontrado.

Dado que esse vetor possui 5 números: [3,1,6,2,7] se estivermos procurando por X=7 , o algoritmo necessitaria de 5 passos para achá-lo, mas se estivermos procurando por X=3 só necessitaria de 1 passo. Portanto o melhor cenário ocorre quando procuramos por um valor que está na primeira célula, e o pior cenário ocorre quando este valor está na última célula ou não existe no vetor.

A notação Big-O possui uma abordagem pessimista em relação à performance e sempre se refere ao pior cenário possível. Isso será de grande relevância quando descrevermos as complexidades abaixo e também quando você for computar a complexidade de seus próprios algoritmos. Sempre pense no pior cenário.

O(1) — Constante

O(1) significa que o algoritmo leva o mesmo número de etapas para ser executado independente da quantidade de dados passados.

Exemplo

📝 Determine se o n-ésimo termo de um vetor é um número ímpar

A resposta exibida no console será: True

Gráfico

Se formos representar o número de passos (eixo y) x o número de elementos de entrada (eixo x), O(1) é uma linha horizontal perfeita, uma vez que o número de etapas no algoritmo permanece o mesmo, independente de quantos dados existam no vetor. É denominado tempo constante.

O(N) — Linear

Um algoritmo que possui a notação O(N) levará tantas etapas quanto o número de elementos de dados sobre o qual ele atua. Então para cada 1 elemento adicionado ao vetor, o algoritmo levará uma etapa a mais para ser concluído.

Exemplo

📝 Percorrer um vetor e exibir cada um de seus elementos

Neste caso, acessamos todos os elementos do vetor um a um, logo o tempo calculado aumenta ao passo que o número de elementos no vetor aumenta.

A resposta exibida será: 0 2 3 4 5

Gráfico

O gráfico se comporta como uma linha diagonal, uma vez que para cada elemento de dado adicionado, o algoritmo realiza um passo a mais. É denominado tempo linear.

Vamos plotar os gráficos de O(1) e O(N) e assumir que o algoritmo O(1) realiza 50 passos:

Podemos observar algumas coisas interessantes;

  • Quando a entrada possui uma quantidade de elementos menor do que 50, o O(N) é mais eficiente.
  • Nos exatos 50 elementos, ambos algoritmos possuem o mesmo número de etapas para sua conclusão.
  • Ao passo em que o número de elementos aumenta, O(N) realiza mais etapas até sua conclusão.

E como a notação Big-O é pessimista e analisa a performance do algoritmo ao passo em que os dados tendem ao infinito, O(N) é considerado menos eficiente do que O(1) .

O(N²)-Quadrático

Representa a complexidade de um algoritmo cuja performance é proporcional ao quadrado do tamanho dos dados na entrada. Se um vetor possui 1 elemento, ele fará 1 operação, se o mesmo possuir 10 elementos, ele fará 100 operações e assim por diante.

Exemplo

📝 Encontrar elementos duplicados em um vetor

A resposta exibida será: True.

Gráfico

O(logN) — Logarítmico

Descreve o comportamento de algoritmos que a cada vez que a entrada de dados dobra, o número de etapas é acrescido de 1.

Exemplo

📝 Algoritmo de busca binária

A resposta exibida será: True.

Nós testamos uma possibilidade por iteração, e nossa gama de possibilidades é dividida por 2 a cada iteração, logo, a complexidade de um algoritmo de busca binária é de O(logN) .

Até agora aprendemos as quatro mais importantes notações, existem algumas outras que cobriremos de forma mais breve:

O(N logN) — Log-Linear

Um algoritmo expresso por esta notação realiza um número de tarefas correspondente a N vezes o log(N)e apesar de sua performance ser ligeiramente pior do que O(N) , muitos algoritmos práticos pertencem a esta categoria, incluindo os de compactação, classificação e pathfinding. Podemos citar como exemplo o conhecido Merge Sort, um algoritmo de dividir para conquistar, ele atua dividindo a entrada em duas partes, chama a si mesma para cada uma e depois une as duas partes classificadas.

O(2ᴺ) — Exponencial

Os algoritmos assim classificados levam o dobro do tempo para rodar para cada elemento adicionado. Temos como exemplo o algoritmo de achar todas as subcategorias de um conjunto de dados devidamente classificado.

O(N!) — Fatorial

Estes algoritmos possuem sua complexidade de tempo pautada no fatorial do número de entradas: n! = n*(n-1)*(n-2)*(n-3)*...*1 . Podemos citar como exemplo o algoritmo que encontra todas as possíveis permutações em um conjunto de dados.

Hierarquia de crescimento

A notação Big-O nos oferece um mecanismo consistente para comparar quaisquer dois algoritmos que cumprem uma mesma tarefa, para enfim determinar qual fará nosso código mais rápido e eficiente. Colocando todos num mesmo gráficos, podemos observar de maneira visual como as diferentes notações se comportam em termos de performance.

Conclusão

Espero que tenham aproveitado a leitura e percebido que a notação Big-O é um conceito bem simples de ser compreendido e aplicado ao seu código para torná-lo cada vez mais eficaz e rápido!

Lembrem-se!

O(1) < O(logN) < O(N) < O(N logN) < O(N²) < O(2ᴺ) < O(N!)

--

--