.NET - Apresentando Big O Notation
 Neste artigo vou apresentar o conceito Big O Notation. (O(1),O(n),O(n2).


Se você fez algum curso relacionado a algoritmos, provavelmente já ouviu falar do termo - Big O Notation ou notação Big O. Se nunca ouviu falar hoje você vai aprender mais um conceito importante.

A notação Big O é uma das ferramentas fundamentais usada  para analisar o custo de um algoritmo, e, por isso é bom você conhecer como ela atua e para que serve.

 

A ideia da notação Big-O é descrever o comportamento geral (também chamado de assintótico, pois é o comportamento no limite conforme os dados crescem) do algoritmo em termos do crescimento do número de operações conforme cresce o número de elementos processados (a quantidade de itens é descrita, genericamente, por n).

A ideia é usar a letra O seguida de uma função sobre n que descreva esse crescimento do algoritmo. Quanto mais rapidamente crescer o número de operações para processar os itens, "pior" é o desempenho do algoritmo (pois ele executa mais instruções e, portanto, mais - é "mais complexo").

Em palavras simples, a notação Big O descreve a complexidade do seu código usando termos algébricos e é uma forma de classificar o quanto a sua função ou algoritmo é escalável.

Usando esta notação podemos determinar qual será o comportamento de um algoritmo no pior cenário ou quanto tempo ele gasta para ser concluído com base em seus valores de entrada.

Geralmente estamos preocupados com o tempo que que será gasto para executar o código e isso pode depender de diversos fatores como : velocidade do computador, linguagem de programação usada, compilador que traduziu o programa, algoritmo usado para implementar o código, dentre outros fatores.

Para dimensionar o tempo gasto com base apenas no algoritmo usado, podemos considerar a quantidade dos termos ou o tamanho da entrada dos dados e o quão rápido a função vai crescer com base neste tamanho. (Isso é conhecido como taxa de crescimento.)

Em relação ao tamanho da entrada dos dados e do tempo gasto podemos considerar como ele pode afetar o desempenho do código usando 4 cenários :

  • constante
  • linear
  • exponencial
  • logaritmicos

Vamos analisar cada um destes cenários...

Antes de iniciar temos que entender que todas as notações Big O envolvem o algoritmo O(f(n)), e,  isso significa que o número de operações simples que o computador precisa fazer diminui à medida que o computador avança em n.

1- Cenário Constante

Neste cenário temos que as funções não dependem da quantidade de valores de entrada informados e o tempo gasto para executar o código será o mesmo, ou seja, o tempo de resposta vai se manter o mesmo independente do tamanho da entrada.

Como exemplo para este cenário temos o seguinte código C#:

Neste código independente da quantidade de valores que array receber o método ou função vai sempre imprimir os valores das duas primeiras posições e assim o tempo para executar esta função vai se manter sempre o mesmo.

O tipo de notação dessa função é O(1) e abaixo temos o gráfico mostrando o comportamento em relação ao tempo gasato desta função :

Se um algoritmo tem um O(1), isso significa que, não importa qual seja o valor de n, a quantidade de operações necessárias para resolver o problema permanece constante.

2- Cenário Linear

Neste cenário temos os algoritmos lineares que realizam uma interação com cada valor informado na entrada. Neste caso a quantidade de operações realizadas será correspondente ao número de valores de entrada informados.

Como exemplo temos o seguinte código C#:

Agora temos que o método ou função vai imprimir cada valor do array, e assim, se a entrada de dados aumentar o tempo gasto para executar o código também vai aumentar.

Esse tipo de algoritmo possui uma notação O(n), onde “n” representa o número de valores passados na entrada. Abaixo o gráfico que representa este comportamento:

Desta forma se uma operação tem uma anotação O(n) (pronuncia-se 'O de N'), o que isso significa é que, à medida que 'n' aumenta, também aumenta a quantidade de operações necessárias para concluir o problema.

3- Cenário Exponencial

No cenário exponencial temos que que o número de interações e o tempo de execução vai aumentar de forma exponencial.

Para este cenário vamos considerar o código C# a seguir:

Neste código para cada iteração feita com um elemento do array estamos imprimindo uma combinação possível do elemento com os demais valores. Assim para cada elemento entrado no array, o número de iterações e o tempo de execução vai aumentar de forma exponencial.

Assim esses algoritmos são os mais custosos e os menos eficientes.

Este algoritmo possui uma notação O(n²) representado pelo seguinte gráfico:

Uma operação O(n) dentro de uma operação O(n) é uma operação O(n * n), ou seja,  uma operação O(n ²). Esta é a mais lenta e menos eficiente e, portanto, a menos desejável notação Big O ao considerar a complexidade do tempo.

4- Cenário Logaritmico

No cenário logaritmico temos os algoritmos que são considerados de alto desempenho pois oferecem uma opção de interação muito eficiente para um grande número de entrada de valores.

Como exemplo de código para este cenário temos uma implementação da busca binária usando a abordagem interativa em C#:

Neste código, em cada busca feita o método vai receber dois valores como parâmetros :

  1. Oo primeiro é um array de elementos (que já deve estar ordenado);
  2. O segundo é o valor que queremos localizar no array;

A busca binária possui um tempo de complexidade logarítmico, pois em cada operação eliminamos os valores de entrada que não satisfazem as condições de busca pela metade o que faz com que apenas verifiquemos uma fração de valores dos dados de entrada para encontrar o valor procurado.

O gráfico que representa este cenário é o seguinte:

Um algoritmo com um O(log n) aumenta em complexidade de tempo com n, mas eventualmente se estabiliza. Se o seu código tiver um O(log n), isso é ótimo! O(log n) é a próxima melhor coisa para O(1).


A seguir temos um gráfico comparativo com os 4 cenários abordados:

 


Comparando Arrays com Objects

 

Vamos a seguir comparar as operações com arrays e objetos e definir a notação Big O.

 

Considerando a complexidade de tempo, trabalhar com objetos (chave: pares de valor) quase sempre será mais eficiente do que trabalhar com arrays (listas ordenadas) por não terem uma ordem específica.

 

A seguir temos as anotações Big O para operações com Objects:

Notação Big O para métodos de objeto :

  1. Object.keys — O(n)

  2. Object.values — O(n)

  3. Object.entries — O(n)

  4. hasOwnProperty — O(1)

Os 3 primeiros possuem a notação O(n) pois são usados para criar arrays.
 

Notação Big O de Arrays :

Notação Big O para métodos de Array :

Assim, a complexidade do Big O considerando o tempo nos permite analisar o número de operações necessárias para resolver um problema e comunicar sua eficiência em termos simples, consistentes e compreensíveis para outros desenvolvedores. Ele serve como base de entendimento para trabalhar com algoritmos e estruturas de dados e, portanto, é muito importante conhecê-lo.

 

E estamos conversados...
 

"Que diremos, pois, a estas coisas? Se Deus é por nós, quem será contra nós? "
Romanos 8:31

Porque um menino nos nasceu, um filho se nos deu, e o principado está sobre os seus ombros, e se chamará o seu nome: Maravilhoso, Conselheiro, Deus Forte, Pai da Eternidade, Príncipe da Paz.

Isaías 9:6
Porque um menino nos nasceu, um filho se nos deu, e o principado está sobre os seus ombros, e se chamará o seu nome: Maravilhoso, Conselheiro, Deus Forte, Pai da Eternidade, Príncipe da Paz.

Isaías 9:6

Referências:


José Carlos Macoratti