Você está na página 1de 10

COMPLEXIDADE DE ALGORITMOS

Escrito por:
Wanderson de Oliveira Paes
Orientado por:
Vinícius Menezes de Oliveira

Rio Grande/RS
15/07/2019
INTRODUÇÃO

Nesse material será apresentado Complexidade de Algoritmos de forma


objetiva. Busque conversar com veteranos e buscar na internet por textos e vídeos
para compreender melhor.

ANÁLISE DE ALGORITMOS

Por que análise de desempenho? ​Há muitas coisas importantes que


devem ser tomadas, como facilidade de uso, modularidade, segurança, facilidade de
manutenção etc. ​Por que se preocupar com o desempenho? A resposta é
simples, se temos desempenho, temos todos os pontos levantados acima.
Temos problemas que determinados algoritmos são melhores para resolver.
Hoje em dia o quão rápido um código resolve um problema é de extrema
importância para o usuário.
Dado dois algoritmos diferentes para resolver uma tarefa, como
encontramos o melhor entre eles?
Uma maneira ingênua de fazer isso é medir o tempo de execução dos dois
códigos no seu computador para diferentes entradas e ver qual deles tem o menor
tempo. Porém existem muitos problemas com essa abordagem para análise de
algoritmos. Como:
➔ Pode ser possível que para algumas entradas, o primeiro algoritmo
seja melhor que o segundo. E para outras entradas o segundo seja
melhor que o primeiro.
➔ Pode além disso ser possível que para algumas entradas, o primeiro
algoritmo seja melhor em um computador e o segundo trabalhe melhor
em outro computador para algumas outras entradas.

ANÁLISE ASSINTÓTICA

É uma boa ideia para lidar com a questão acima em análise de algoritmos.
Em Análise Assintótica, nós avaliamos a performance do algoritmo em termos do
tamanho da entrada, noś não medimos o tempo de execução. Calculamos como o
tempo (ou espaço) usado por um algoritmo aumenta com o tamanho da entrada.
Por exemplo, considerando um problema de busca de um item em um vetor
ordenado. Uma maneira de procurar é usar Busca Linear (ordem de crescimento é
linear) e outro jeito é por Busca Binária (ordem de crescimento é logarítmica).
Considerando a execução do código de Busca Linear em um computador rápido e a
Busca Binária em um computador lento. Para valores de tamanho de vetor pequeno,
o computador rápido pode levar menos tempo. Porém depois de determinado
tamanho do vetor de entrada, a Busca Binária começa definitivamente a levar
menos tempo em comparação a Busca Linear, mesmo que a Busca Binária esteja
rodando em uma máquina lenta. Isso acontece por conta da ordem de crescimento.
Análise Assintótica sempre funciona?
Análise Assintótica não é perfeita, mas é o melhor caminho disponível para
análise de algoritmos. Você pode se deparar com algoritmo assintoticamente lento,
mas para sua aplicação com apenas entradas pequenas, ele é mais rápido que um
algoritmo assintoticamente mais rápido.

CASOS

Temos três casos de análise de algoritmos: Pior caso, Caso médio e Melhor
caso. Temos um exemplo de Busca Linear a seguir para analisar assintoticamente.
int​ ​buscaLinear​(​int​ vetor[], ​int​ tamanho, ​int​ x){
​int​ i;
​for​ (i=0; i<tamanho; i++){
​if​ (vetor[i] == x) ​return​ i;
}
​return​ -1;
}

Analisando o Pior Caso


No pior caso, calculamos o limite superior no tempo de execução de um
algoritmo. Devemos conhecer o caso que faz com que o número máximo de
operações seja executado. Para a Busca Linear, o pior caso ocorre quando o
elemento a ser pesquisado (x no código acima) não está presente o vetor. Desta
forma comparamos todos os elementos do vetor, um por um. Portanto, o pior caso

teria complexidade de θ (n) .

Analisando o Caso Médio


No caso médio, tomamos todas as entradas possíveis e calculamos o tempo
de computação para todas as entradas. Soma todos os valores calculados e divide
a soma pelo número total de entradas. Nós devemos saber (ou prever) a
distribuição de casos. Para o problema de pesquisa linear, vamos supor que todos
os casos sejam distribuídos uniformemente (incluindo o caso de x não estar
presente no vetor). Então, somamos todos os casos e dividimos a soma por (n + 1).
A seguir, o valor da complexidade do caso médio.
n+1
∑ θ(i)
i=1 θ((n+1)*(n+2)/2)
(n+1) = (n+1) = θ(n)
Analisando o Melhor Caso
No melhor caso, calculamos o limite inferior no tempo de execução de um
algoritmo. Devemos conhecer o caso que faz com que o número mínimo de
operações seja executado. No problema de Busca Linear, o melhor caso ocorre
quando o elemento a ser procurado está na primeira posição do vetor. Assim o
número de operações é constante (não depende de n). Portanto a complexidade no

melhor caso seria θ (1) .

Na maioria das vezes, procuramos analisar o pior caso de um algoritmo.


Garantindo que o número de operações não passe do limite superior do tempo de
execução que o pior caso fornece.
A análise de casos médios, não é fácil na maioria dos casos práticos e
raramente é feita. Na análise de casos médios, devemos conhecer (ou prever) a
distribuição matemática de todas as entradas possíveis.
No melhor caso, podemos dizer que não temos informações muito úteis.
Pois dificilmente teremos problemas que sempre se encaixe no melhor caso e se
somente sabermos o melhor caso, no pior caso do algoritmo pode ser que leve
muito tempo para ser executado.
Para alguns algoritmos, todos os casos são assintoticamente os mesmos,
por exemplo o Merge Sort, que em todos os casos tem θ (n * log(n)) operações. A

maioria dos casos terão melhor e pior caso, como por exemplo o Insertion Sort, que

no pior caso quando os elementos estão inversamentes ordenados, tem θ (n2 )

operações. E no melhor caso quando os elementos já estão ordenados, tem θ (n)


operações.

NOTAÇÃO ASSINTÓTICA

Notação assintóticas são ferramentas matemáticas para representar a


complexidade temporal dos algoritmos para análise assintótica. A seguir veremos
três notações usadas para representar a complexidade do tempo dos algoritmos.
Notação θ : A notação teta limita as funções de cima e de baixo, de modo
que define o comportamento assintótico exato. Uma maneira simples de obter a
notação teta de uma expressão é eliminar os termos de baixa ordem e ignorar as
constantes principais. Por exemplo, considerando a expressão a seguir: ​3n³ + 6n² +
6000 = θ (n³)

Notação Big O: A notação Big O define um limite superior de um algoritmo,


ele limita uma função apenas de cima. Por exemplo, considerando o Insertion Sort
que tem como melhor caso um tempo linear e pior caso um tempo quadrático.
Podemos dizer que a complexidade do Insertion Sort é O(n²). Note que O(n²)
também cobre o tempo linear.

Se usarmos notação para representar a complexidade do tempo do


Insertion Sort, temos que usar duas instruções para os melhores e piores casos:
2
O pior caso do Insertion Sort é θ(n ) .
O melhor caso do Insertion Sort é θ(n) .
O Big O é muito usado quando só temos limite superior na complexidade de
um algoritmo. Muitas vezes encontramos facilmente um limite superior
simplesmente olhando para o algoritmo.
Notação Ω : A notação sigma fornece um limite inferior assintótico. Como já
discutido, o melhor caso não é muito útil. Considerando o exemplo do Insertion Sort,
podemos escrever a complexidade como Ω(n) .
ANÁLISE DE LOOPS

Analisando algoritmos iterativos com exemplos simples.


O(1)​: A complexidade de uma função é considerada O(1) se não possui
nenhum loop, recursão e chamada para qualquer função não constante.
Quando iteramos apenas uma vez um loop, ele é O(1). Veja o exemplo:
for​(i=1; i<=1; i++){
//Alguma expressão O(1)
}
O(n)​: A complexidade de um loop é considerada O(n) se o loop tem uma
variável que é incrementada ou decrementada por uma constante. Veja os
exemplos a seguir:
//incrementada
for​(i=1; i<=n; i++){
//Alguma expressão O(1)
}
//decrementada
for​(i=n; i>0; i--){
//Alguma expressão O(1)
}
O(n²)​: A complexidade de loop é considerada O(n²) quando temos dois
loops aninhados iterados por uma constante. Assim a complexidade é igual ao
número de vezes que a instrução mais interna é executada. Veja o exemplo:
​for​(i=1; i<=n; i++){
for​(j=1; j<=n; j++){
//Alguma expressão O(1)
}
}
Para termos a complexidade de loop igual a O(n³), é só acrescentar mais
um loop aninhado no exemplo anterior.
O(log(n)): Temos a complexidade logarítmica de um loop quando a variável
iterativa é dividida ou multiplicada por uma constante. Essa constante que multiplica
ou divide será a base do logarítmo. Segue o exemplo:
for​(i=1; i<=n; i=i*2){
//Alguma expressão O(1)
}
for​(i=n; i>0; i=i/2){
//Alguma expressão O(1)
}

Como combinar as complexidades de loops consecutivos?


Quando temos loops consecutivos, calculamos a complexidade através da
soma das complexidades individuais dos loops. Veja o exemplo:
for​(i=1; i<=n; i++){
//Alguma expressão O(1)
}
for​(i=1; i<=m; i++){
//Alguma expressão O(1)
}
A complexidade do trecho de código acima é O(n) + O(m), logo O(n+m).
Se m == n, O a complexidade torna-se O(2n) que podemos considerar como
O(n).

Como calcular a complexidade quando temos muitos if, else dentro de


loops?
Como já discutimos o pior caso é o mais usado e vantajoso na análise.
Portanto, precisamos considerar o pior caso. Averiguamos a situação quando os
valores nas condições if-else fazem com que o número máximo de instruções seja
executado.
Quando o código é muito complexo para considerar todos os casos if-else,
verifique se podemos desconsiderar essas instruções complexas, se for possível
desconsiderar.
ANÁLISE DE RECURSÃO

Muitos algoritmos são de natureza recursiva. Quando os analisamos,


obtemos uma relação de recorrência para a complexidade do tempo. Esse assunto
é mais complicado, e a melhor forma de entender é assistindo vídeos e lendo mais
na internet.
Segue links para se dar uma olhada:
[1] ​DHANUKA, Manish. ​Analysis of Algorithm | Set 4 (Solving
Recurrences)​. Geeks for Geeks. Disponível em:
https://www.geeksforgeeks.org/analysis-algorithm-set-4-master-method-solving-recu
rrences/​ Acessado em: 16/07/2019.
[2] IME-USP. ​Solução de Recorrências​. IME-USP. Disponível em:
https://www.ime.usp.br/~pf/analise_de_algoritmos/aulas/recorrencias.html Acessado
16/07/2019.

CONHECENDO SOBRE COMPLEXIDADE EM SITES

Quando treinamos para competições de programação em sites, a tarefa


mais difícil é escrever um código com a complexidade desejada, caso contrário o
programa resultará em Time Limit Exceeded.
Para resolver esse problema, a resposta é diretamente relacionado com o
número de operações que podem ser executadas em um segundo. A maioria dos
sites permitem ​10⁸ operações por segundo​, apenas em alguns casos permite-se
10⁷ operações. Depois de descobrir o número de operações que podem ser
realizadas, procure a complexidade certa observando as restrições fornecidas no
problema.
REFERÊNCIAS

[1] ​KOZHUHAROV, Danail. ​Analysis of Algorithms | Set 1 (Asymptotic


Analysis)​. Geeks for Geeks. Disponível em:
https://www.geeksforgeeks.org/analysis-of-algorithms-set-1-asymptotic-analysis/
Acessado em: 15/07/2019.
[2] ​RATHI, Abhay. ​Analysis of Algorithms | Set 2 (Worst, Average and
Best Cases)​. Geeks for Geeks. Disponível em:
https://www.geeksforgeeks.org/analysis-of-algorithms-set-2-asymptotic-analysis/
Acessado em: 15/07/2019.
[3] ​RATHI, Abhay. ​Analysis of Algorithms | Set 3 (Asymptotic Notations)​.
Geeks for Geeks. Disponível:
https://www.geeksforgeeks.org/analysis-of-algorithms-set-3asymptotic-notations/
Acessado: 15/07/2019.
[4] ​GEEKS FOR GEEKS. ​Analysis of Algorithms | Set 4 (Analysis of
Loops)​. Geeks for Geeks. Disponível em:
https://www.geeksforgeeks.org/analysis-of-algorithms-set-4-analysis-of-loops/
Acessado em: 16/07/2019.
[5] DUBEY, Pranshu. ​Knowing the complexity in competitive
programming​. Geeks for Geeks. Disponível em:
https://www.geeksforgeeks.org/knowing-the-complexity-in-competitive-programming/
Acessado: 16/07/2019.

Você também pode gostar