Desenvolvimento

14 abr, 2009

Análise assintótica de algoritmos

Publicidade

Muitas vezes
precisamos otimizar códigos para aumentar a robustez e velocidade de sistemas.
Podemos ter vários fatores influenciando nesse ponto. O que vou explicar nesse
artigo diz respeito a uma das principais formas de análise da lógica envolvida
nos algoritmos, a análise assintótica.

Esse termo é
bem comum quando estudamos estrutura de dados, visto que é um estudo paralelo,
quando não é um pré-requisito do mesmo.

Sabemos que
dentro da área de informática as disciplinas mais complexas e com maior índice
de reprovação nos cursos superiores são algoritmos e estrutura de dados e, na
maioria das vezes, um bom conhecimento de análise assintótica resolveria o
problema ou facilitaria a compreensão de temas mais avançados.

O que vem a ser
análise assintótica?

Fazer uma análise
assintótica é se preocupar com valores grandes de entrada para o processamento
do algoritmo, com o intuito de calcular o tempo total de processamento e
viabilidade para determinados casos. Muitas vezes com isso podemos ser capazes
de saber se é necessária a utilização de outra metodologia ou ferramenta para a
realização de uma tarefa.

Vale lembrar
que, mesmo sendo uma área de extrema importância na informática, não é comum à
maioria dos ‘profissionais’ do mercado atual praticá-la. Alguns dos motivos
para isso são:

  • profissionais com boa capacidade desse tipo de análise são caros
    para a maioria das empresas
  • gerentes de projeto não vêem o tempo gasto para
    isso como um tempo útil e preferem ver resultados imediatos
  • profissionais
    estudam essa disciplina no ensino superior apenas como uma disciplina
    complementar e também preferem matérias mais práticas ou mais fáceis (visto
    que, no geral, matemática é uma disciplina complexa)

Pensar
assintoticamente requer uma visão mais além do projeto, visualizando até onde o
sistema pode chegar. Isso parece óbvio, mas não é bem assim, já que muitas
vezes é difícil de simular valores grandes no ambiente de teste, então os
testes são feitos com limites pequenos. Por outro lado não podemos simplesmente
fazer o contrário, ou seja, criar sistemas com suporte ao dobro do que
realmente precisaria. Tudo é questão de fazer um bom estudo, e caso a caso.

Junto com o
estudo de análise assintótica, temos o conceito de notação assintótica, que é a
representação matemática criada por Paul Bachmann no século XIX. Nela, temos
três notações comuns para classificar as ordens das funções, essas ordens
determinam a equivalência das funções, ou seja podemos ter uma função que seja
to tipo n²,
essa função é equivalente à função 400n². São equivalentes assintoticamente
falando, lembrando que sempre nos preocupamos com valores de entrada grandes
para n. As três principais classificações de ordem são: Ordem O, Ordem Omega e Ordem
Theta.

A explicação
matemática das ordens é bem complexa, existem muitos livros que explicam seus
conceitos, recomendo: The Art of Computer Programming (de Knuth). Mas
resumidamente, para colocarmos o assunto dentro do contexto de sistemas,
podemos dizer que a ordem O (O) analisa o pior caso, ou seja, pega ele analisa o
limite superior de entrada, esse é o mais comum de todos e o mais amplamente
usado e serve para testar o qual o máximo que será utilizado de processamento. A Ordem Omega é o inverso, analisa a entrada mais baixa e serve para testar
o qual o mínimo que sempre será usado de processamento. Por último temos a Ordem
Theta, que é utilizada para testar os valores médios de entrada, ela é
juntamente usada com bases estatísticas de dados e requer melhor conhecimento
das entradas.

Para
exemplificar isso vou fazer uma simulação simples: um algoritmo para ler n
valores inteiros e, dentre eles, escolhe o maior.

programa escolheMaiorDigito:
         declara i, maior, n, valorNumeros[] : inteiros
         ler: n
         para i de 0 a n-1 faça
                     ler valorNumeros[i]
         fim-para
         maior: 0
         para i de 0 a n-1 faça
                     se valorNumeros[i] > maior entao
                                 maior = valorNumeros[i]
                     fim-se
         fim-para
         mostra maior
fim-programa

Nesse algoritmo
acima, poderíamos já ter o vetor de valores, então não contamos o tempo de
entrada. No algoritmo principal (após a leitura), temos um laço de repetição
que vai de 1 a n, invariavelmente, pois para sabermos se tem algum número maior
que o atual a cada iteração precisamos consultar os outros valores, então tanto
no melhor caso, como no pior e também no caso médio, temos sempre um tempo
constante que é o valor de “N”, podemos dizer que esse algoritmo é O(n).

Agora, se
tivéssemos o seguinte problema: pegar o valor do índice 5 (posição 6) do vetor
do código anterior. Que complexidade esse algoritmo tem? Simples!!! Para pegar
o valor do índice 5 basta uma linha de código que é a seguinte:

mostra
valorNumeros[5]

Logo, para
consultar em uma tabela de valores um índice direto, a complexidade do pior
caso é O(1).

Essas duas
ordens e algumas outras mais comuns normalmente são nomeadas. Seguem abaixo as
principais ordens com suas respectivas nomenclaturas.

  • O(1) = ordem
    constante
  • O(log n)
    = ordem logarítmica
  • O(n) =
    ordem linear
  • O(n²) =
    ordem quadrática
  • O(n³) =
    ordem cúbica
  • O(nc)
    = ordem polinomial (c é um valor constante qualquer)
  • O(cn)
    = ordem exponencial (c é um valor constante qualquer)

Assim, os
algoritmos citados acima tem complexidade linear (O(n)), e complexidade
constante (O(1)).

Essa área de
estudo é muito importante e vale a pena um bom estudo sobre o assunto,
inclusive quando precisa-se utilizar um algoritmo pronto para determinadas
situações, podemos analisar a complexidade assintótica consumida pelo mesmo fim de viabilizar ou não o seu uso, possivelmente pode-se substituí-lo por um
solução mais adequada ao momento.