Complexidade de Algoritmos
Um algoritmo é um conjunto definido de instruções a serem executadas para resolver um problema.
Considerando as diversas soluções algorítmicas para um dado problema, como identificar a melhor? E melhor sob qual perspectiva? Podemos, a depender do objetivo, considerar critérios de análise de um algoritmo como: tempo, espaço, tamanho do código, eficácia, legibilidade, precisão e outros.
Avaliar a complexidade de uma algoritmo constitui-se em determinar os recursos exigidos (tempo, espaço, etc.):
Métodos de Análise de Algoritmos
Podemos calcular/estimar o tempo de processamento de um algoritmo através de 2 métodos:
Qual método usar? O empírico tem como vantagem o fato de considerar condições reais. Já o analítico conta com a garantia da neutralidade em relação aos efeitos do tradutor e do hardware que podem mascarar a eficiência do algoritmo em análise. Assim, normalmente usamos o teórico.
Pelo método analítico, partimos do pressuposto de que toda e qualquer instrução contida no algoritmo é executada em tempo constante e buscamos definir uma função t = f(n), onde t é o total de tempo de processamento e n é o tamanho da entrada. Vale salientar que é usual ignorarmos o custo de algumas instruções e considerar apenas as mais significativas.
Exemplo 1: Cálculo da complexidade de um algoritmo que soma N números dados
Instruções |
Custo |
readln(N); | 1 |
Cont:=1; | 1 |
Soma:=0; | 1 |
while Cont<=N do begin readln(Num); Soma:=Soma+Num; inc(Cont); end; |
3n |
Total |
3 + 3n |
Exemplo 2: Cálculo da complexidade de um algoritmo que calcula o somatório da série geométrica: 1 + x1 + x2 + x3 + ... + xn
Instruções |
Custo |
readln(X); | 1 |
readln(N); | 1 |
Soma:=0; | 1 |
I:=0; | 1 |
while I<=N do begin Produto:=1; J:=0; while J<I do begin Produto:=Produto*X; inc(J); end; Soma:=Soma + Produto inc(I); end; |
(N2 + N) / 2 |
Total |
(N2 + N) / 2 + 4 |
Para chegar à quantidade de vezes que o laço while mais interno é executado, podemos que perceber que para qualquer valor de N, ele é executado um número de vezes igual ao somatório de uma progressão aritmétrica cujo termo inicial é 0, a razão é 1 e último termo é N.
Considerando que o valor de N informado é igual a 5, teríamos:
Quando I é igual a | While mais interno é executado |
0 | 0 |
1 | 1 |
2 | 2 |
3 | 3 |
4 | 4 |
5 | 5 |
Considerando que o somatório de uma PA é calculado através da fórmula S = n * (u1 + un) / 2, para N = 5, teríamos o somatório igual a 15.
Já que u1 = 0 e un = N, podemos chegar à fórmula abaixo:
Uma outra implementação possível para o cálculo é mostrada abaixo:
Instruções |
Custo |
readln(X); | 1 |
readln(N); | 1 |
Soma:=0; | 1 |
I:=0; | 1 |
while I<=N do begin Soma:=Soma*X+1; inc(I); end; |
2 * N |
Total |
2 * N + 4 |
Comparando esta implementação com anteriormente mostrada, pode-se perceber que é bem mais eficiente.
Cálculo de Complexidade
Para simplificar o cálculo de complexidade algorítmica, fazemos algumas simplificações:
Desta forma surge a notação O:
Complexidade | Notação O |
2n + 2 | O(n) |
(n2 + n)/2 | O(n2) |
log n + 4 | O(log n) |
Com base nestas regras, usamos algumas denominações de acordo com a complexidade ordem (O):
Ordens |
|
O(k), sendo k constante | constante |
O(log n) | logarítmica |
O(n) | linear |
O(n2) | quadrática |
O(n3) | cúbica |
Kn | exponencial |
As ordens estão dispostas na tabela acima em ordem crescente de complexidade, isto é, a de menor complexidade é a constante e a de maior complexidade é a exponencial.
Na tabela abaixo simulamos possíveis valores para n e verificamos como se comportam as Ordens de complexidade apresentadas:
n | O(log n) | O(n) | O(n2) | O(n3) |
1 | 0,00 | 1 | 1 | 1 |
2 | 1,00 | 2 | 4 | 8 |
3 | 1,58 | 3 | 9 | 27 |
4 | 2,00 | 4 | 16 | 64 |
5 | 2,32 | 5 | 25 | 125 |
6 | 2,58 | 6 | 36 | 216 |
7 | 2,81 | 7 | 49 | 343 |
8 | 3,00 | 8 | 64 | 512 |
9 | 3,17 | 9 | 81 | 729 |
10 | 3,32 | 10 | 100 | 1000 |
Graficamente, podemos visualizar melhor como a complexidade de uma função cúbica aumenta muito mais do que uma linear ou quadrática:
Melhor caso, Pior caso e caso Médio
Há algoritmos em que o custo (tempo de execução), não é uniforme para as entradas de tamanho n.
Por exemplo, no caso do algoritmo que determina o maior elemento de um vetor de inteiros:
Em outras palavras, a forma como os dados de entrada encontram-se dispostos, exerce influência no comportamento de alguns algoritmos tornando o cálculo de complexidade não exato.
Assim, ao apresentarmos o cálculo de complexidade de algoritmos, devemos esclarecer se este refere-se ao melhor caso, ao caso médio ou ao pior caso de arranjo de entrada, onde:
O cálculo de complexidade de soluções algorítmicas não é só aplicado para identificação da melhor solução dentre diversas, mas também para predizer o comportamento não exato, relativo ao tempo de execução de um algoritmo, independente de sua implementação efetiva.