Algoritmos e como analisar sua complexidade

Fabrício Moraes
Creditas Tech
Published in
8 min readSep 17, 2019

Os algoritmos estão basicamente em todas as tarefas que realizamos no dia a dia. Muitos deles executamos no “automático”, sem sequer perceber “o que” ou “como” estamos fazendo. No desenvolvimento de software não é assim tão diferente. Mas afinal, o que são algoritmos? E por que é importante analisar a complexidade deles?

É o que veremos neste post! :)

O que é um Algoritmo?

Um conjunto de passos necessários para realizar uma tarefa.

Não são só os programas de computador que executam algoritmos, eles também são executados e implementados por nós, humanos.

Por exemplo, você já pensou no algoritmo que executa para alguma das tarefas abaixo?

  • Escovar os dentes
  • Tomar banho
  • Cozinhar
  • Dirigir de casa para o trabalho

No nosso dia a dia, executamos uma série de passos para completar pelo menos uma das tarefas acima. Pegando como exemplo a tarefa Dirigir de casa para o trabalho, os meus passos são basicamente os seguintes:

  1. Pegar meu carro
  2. Dirigir até a casa dos meus amigos para dar carona
  3. Dirigir até o local do escritório onde trabalho
  4. Deixar meu carro no estacionamento

Esse é o conjunto de passos necessários (algoritmo) que eu executo para concluir esta tarefa.

Agora pense no conjunto de passos necessários que você executa para concluir essa mesma tarefa. Muito provavelmente é bem diferente do meu. Os nossos algoritmos podem ser diferentes, mas o nosso objetivo é o mesmo.

Nos programas de computador isso não é diferente: existem diversos algoritmos que realizam uma série de tarefas e muitas das vezes nem nos preocupamos em saber como são, somente os usamos (me incluo nisso).

Por exemplo, como deve ser o algoritmo que o Google Maps ou Waze usam para calcular a melhor rota de um local a outro? Como deve ser o algoritmo que faz o Netflix sugerir filmes e séries com base no que já assistimos?

Eu particularmente não sei como é, mas sei que é eficiente. E é nesse ponto, da eficiência, que a análise da complexidade entra.

O que é a Complexidade de Algoritmos?

Em ciência da computação, a complexidade de algoritmos se refere ao quanto de tempo e memória um algoritmo consome para executar uma tarefa de acordo com o tamanho da sua entrada.

Em geral, são avaliados o consumo de tempo e memória, porém neste post falarei somente da complexidade de tempo.

Analisando a complexidade de tempo, conseguimos determinar como um algoritmo se comportará dado o crescimento dos seus elementos de entrada. Podemos ter uma noção do tempo que levará para processar 10, 100 ou 1000 elementos.

E por que essa análise é importante?

  • Para conseguir determinar qual algoritmo é mais eficiente
  • Para conseguir desenvolver algoritmos mais eficientes
  • Para conseguir analisar se o algoritmo de uma biblioteca, ou até da própria linguagem que está sendo usada, é eficiente

Enfim, o foco é na eficiência!

Complexidade de Tempo

É o tempo que uma atividade leva para ser concluída.

Começarei analisando o tempo usando o método Frequency Count, que basicamente em contar o número de vezes que um passo é executado, onde para cada passo é atribuído uma unidade de tempo, que começa com o valor 1.

O tempo que um algoritmo leva é expressado pela função f(n), tal que n é o tamanho dos dados.

O resultado de uma análise é expressado da seguinte forma:

([função que expressa tempo]) = [Soma das unidades de tempo]

Vamos analisar alguns algoritmos e entender na prática como funciona.

A primeira função efetua a soma de dois números inteiros:

Podemos ver que para tal tarefa, o algoritmo implementado executa apenas um passo: a soma dos dois números. Como para cada passo atribuímos uma unidade de tempo, o resultado é f(n) = 1.

Vamos analisar o próximo exemplo, um algoritmo que divide um número inteiro por outro número inteiro:

Apesar de também ser uma operação matemática como a soma, o algoritmo de divisão contém alguns passos a mais porque é necessário verificar se o divisor é 0, visto que, se for esse o caso, não é possível realizar a divisão dos números. Como temos um total de quatro passos e atribuímos para cada um deles uma unidade de tempo, temos como resultado f(n) = 4.

Já o próximo algoritmo efetua a soma de todos os elementos de uma lista de inteiros:

Neste algoritmo, um dos passos é o for, uma instrução de repetição;, isso quer dizer que o código dentro do for será executado várias vezes. Como esse número de execuções dependerá do tamanho dos dados, neste caso atribuímos o valor n para a unidade de tempo. Como resultado temos f(n) = 2n + 3.

O próximo algoritmo efetua a soma dos elementos de uma lista com os de uma segunda lista.

Como resultado temos f(n) = 2n² + 2n + 3.

Até aqui vimos somente algoritmos simples, certo? Agora imagine analisar algoritmos bem mais complexos e ter que fazer todos estes cálculos? Meio inviável, né? Embora pareça ser o mais apropriado, é uma maneira muito trabalhosa e no fim não vale a pena.

Geralmente, a intenção quando estamos analisando um algoritmo, é saber como ele se comportará nos casos em que temos muitos elementos para processar. São nessas situações que precisamos encontrar o termo predominante da soma.

Por exemplo, qual é o termo predominante da soma para o terceiro algoritmo?

f(n) = 2n + 3

Neste caso, o termo predominante é 2*n, explicarei o porquê!

Em qualquer situação onde n for maior que 1 (n>1), o produto (resultado da multiplicação) já valerá mais que o segundo termo da soma.

Faça um teste, substitua n por qualquer valor maior que 1 e resolva a multiplicação.

E o termo predominante da soma para o último algoritmo?

f(n) = 2n² + 2n + 3

Neste caso, o termo predominante é 2*n², porque quando n > 1, o produto já valerá mais do que o segundo e terceiro termo da soma. Faça o teste também!

Por isso, existe uma forma mais comum, por assim dizer, para analisar a complexidade de algoritmos, em que os termos constantes e menos significativos são desconsiderados. Essa forma é a Complexidade Assintótica.

É aqui que entra a Ordem de complexidade, ou Notação-O ou Big-O.

Notação Big-O

Usada para classificar um algoritmo de acordo com a taxa de crescimento das operações à medida que cresce o número de elementos processados.

A notação Big-O também define uma função que expressa a complexidade de tempo de um algoritmo. Para isso é usada a letra O seguida de uma função sobre n.

As classes mais comuns são:

  • O(1) — Constante, número de operações não cresce à medida que cresce o número de elementos
  • O(log n) — Logarítmico, número de operações é menor que o número de elementos
  • O(n) — Linear, o número de operações cresce proporcionalmente ao número de elementos
  • O(n²) — Quadrático, o número de operações será maior do que o número de elementos
  • O(2^n) — Exponencial, o número de operações cresce muito rápido comparado ao número de elementos
  • O(n!) — Fatorial, o número de operações cresce muito, muito rápido, mesmo para um pequeno número de elementos

Abaixo, temos um gráfico que ilustra a Taxa de Crescimento das Operações x Quantidade de Elementos:

Podemos ver no gráfico a seguinte classificação:

  • Zona vermelha é horrível, evite!
  • Zona laranja é ruim, evite também!
  • Zona amarela é justo, razoável!
  • Zona verde claro é bom, legal!
  • Zona verde escuro é excelente, parabéns!

Com isso, vamos agora usar a notação Big-O para classificar os algoritmos que vimos anteriormente, considerando sempre o termo predominante da soma.

O primeiro algoritmo teve como resultado f(n) = 1, ou seja independente do crescimento dos elementos, o algoritmo executará apenas uma operação. Podemos então definir esse algoritmo como sendo Constante — O(1).

O segundo algoritmo teve como resultado f(n) = 4, ou seja independente do crescimento dos elementos, o algoritmo executará quatro operações. Podemos então definir que esse algoritmo também é Constante — O(1).

O terceiro algoritmo teve como resultado f(n) = 2n + 3, o que significa que o número de operações irá crescer proporcionalmente ao número de elementos, visto que temos o n como variante nesta função. Podemos então definir que esse algoritmo é Linear — O(n).

O último algoritmo teve como resultado f(n) = 2n² + 2n + 3, ou seja, o crescimento das operações será maior que o número de elementos. Também temos o n como variante nesta função, porém nesse caso há um termo elevado à segunda potência (quadrado). Podemos então definir que esse algoritmo é Quadrático — O(n²).

A tabela abaixo uma tabela ilustra o crescimento do número de operações de acordo com o crescimento do número de elementos.

Perceba que para 16 elementos, em um algoritmo exponencial, serão executadas no mínimo 65.536 operações. Bem preocupante, certo?

Em geral, o processo de análise é o mesmo que fizemos anteriormente, contando o número de passos e identificando o termo predominante.

Algumas tendências que podemos observar:

  • Algoritmos sem laço de repetição tendem a ser Constantes — O(1)
  • Algoritmos que tenham laços de repetição, mas não aninhados, tendem a ser Lineares — O(n)
  • Algoritmos que tenham dois laços de repetição aninhados, tendem a ser Quadráticos — O(n²)
  • O acesso direto pelo índice de um array, geralmente é O(1).
  • Métodos de extensão das listas, em média são O(n). Por exemplo: any, sum e filter.
  • Algoritmos de ordenação como Bubble Sort, Insertion Sort e Selection Sort em média são O(n²).

Conhecer essas classes e saber como classificar os algoritmos nos dá a capacidade de determinar o quanto um algoritmo é bom ou ruim, eficiente ou ineficiente. Claro que não conseguimos medir o tempo exato em segundos, minutos ou horas. Até porque para isso precisaríamos executar, medir e alterar. Imagine fazer isso para algoritmos que levam horas ou até dias? Sem chances!

Então, espero que eu tenha cumprido o objetivo deste post, que foi de dar uma visão geral do que são os algoritmos e como analisamos sua complexidade, usando o método Frequency Count e a notação Big-O.

Abaixo deixo algumas referências para leitura e estudo!

Referências:

Vídeos 1 à 1.7

Tem interesse em trabalhar conosco? Nós estamos sempre procurando por pessoas apaixonadas por tecnologia para fazer parte da nossa tripulação! Você pode conferir nossas vagas aqui.

--

--