Você está na página 1de 13

Universidade Santa Úrsula

APOSTILA I DA DISCIPLINA
ANÁLISE DE ALGORITMOS

Alex Avellar
Universidade Santa Úrsula 2
Disciplina: Análise de Algoritmos

1 Eficiência e Corretude de Algoritmos


O objetivo da Análise de Algoritmos é inicialmente possibilitar que o aluno aprenda
algoritmos básicos e diferentes técnicas utilizadas para resolver problemas
computacionalmente. Em seguida, o objetivo é que ele consiga utilizar esse conhecimento
para fazer algoritmos que sejam os mais eficientes possíveis.
No entanto, para desenvolver algoritmos realmente eficientes, não basta conhecer
técnicas e alternativas para problemas comuns. O programador deve ter a capacidade de
prever, ao desenvolver um algoritmo, qual será o seu comportamento, quer com poucos ou
com muitos dados de entrada.
A tentativa de prever o comportamento do algoritmo consiste em avaliar sua
complexidade. Para isso, são feitos cálculos, que podem ser simples ou complexos. Como
esses cálculos envolvem definições e notações específicas, como analisá-las, para só então
vermos como proceder para analisar a complexidade de um software.
Temos então três objetivos para a análise de algoritmos:
1. Avaliar um algoritmo para verificar se ele é eficiente
2. Verificar se um algoritmo é correto ou incorreto
3. Comparar vários algoritmos (que resolvem o mesmo problema) para decidir qual é o
mais eficiente.

1.1 Definições

1.1.1 O que é um algoritmo?


Um algoritmo é definido pela matemática como um “processo de cálculo, ou de
resolução de um grupo de problemas semelhantes, em que se estipulam regras formais para
a obtenção do resultado, ou da solução do problema”. Em computação, é comum
definirmos um algoritmo como “um conjunto de passos necessários para resolver um
problema”. Outra definição é a de que “um algoritmo, intuitivamente, é uma seqüência
finita de instruções ou operações básicas(...), cuja execução, em tempo finito, resolve um
problema computacional”(Salvetti e Barbosa, 1998).
Um algoritmo, na computação, é qualquer procedimento computacional que recebe
como entrada um valor (ou conjunto de valores) e produz como saída outro valor (ou um
conjunto de valores). Finalmente, então, podemos dizer que um algoritmo é uma
seqüência de passos computacionais que transforma entrada em saída.
Esse, portanto, é o objeto que será estudado a partir de agora. Trechos de código só
podem ser considerados algoritmos quando eu consigo definir claramente: 1) o problema,
2) os dados de entrada e 3)os dados de saída.
Por exemplo, eu posso ter um trecho de código no qual eu identifico os seguintes
elementos:

Problema: Encontrar o maior e o menor valor de um vetor com n elementos.


Entrada: Vetor com n elementos
Saída: O elemento com o menor valor de todos e o elemento com o maior valor de
todos.

Figura 1 : Exemplo de elementos de um algoritmo


Universidade Santa Úrsula 3
Disciplina: Análise de Algoritmos

Neste caso, se o trecho tem apenas essa função, pode ser considerado um algoritmo
e é possível analisá-lo individualmente.

1.1.2 Instâncias de execução de algoritmos


Uma instância de um problema consiste de um conjunto de dados de entrada e saída
utilizado durante uma única execução. Por exemplo, as figuras 11 e 12 mostram diferentes
instâncias da execução do mesmo algoritmo, cujo problema foi especificado na Figura 10.

Problema: Encontrar o maior e o menor valor de um vetor com n elementos.


Entrada: {1,34,56,32,3,54,3,356,3,2,23,78,65,32,11,1,43,356,66}
Saída:
Menor valor = 1
Maior valor = 356
Figura 2 . Exemplo de instância da execução de um algoritmo

Problema: Encontrar o maior e o menor valor de um vetor com n elementos.


Entrada: {2,54,67,93,54,23,345,67,42,447,4,983,10,76,31,15,57,83,45,794,346,44}
Saída:
Menor valor = 2
Maior valor = 983
Figura 3 . Exemplo de outra instância da execução de um algoritmo

Por esses exemplos podemos verificar que em diferentes instâncias de execução, os


dados de entrada podem ser bastante variados, podendo inclusive ter uma grande variação
de volume. Ou seja, na instância apresentada na Figura 11 o vetor de entrada tinha 19
valores, enquanto na instância apresentada na Figura 12 o vetor de entrada tinha 22. Da
mesma, em uma outra instância é possível que eu tenha 500 elementos de entrada.
A questão que se levanta então é: dado um algoritmo que funciona de forma
eficiente para 10 elementos, ele continuará funcionando de forma eficiente para 10.000 ou
mais? O algoritmo deve trabalhar corretamente sobre todas as instâncias para as quais foi
projetado para solver. Portanto, um algoritmo para o qual é possível achar uma instância de
dados de entrada para a qual ele não consegue achar uma resposta correta é um algoritmo
incorreto. No entanto, provar o contrário, que o algoritmo é correto para qualquer
instância, não é tão simples. Para isso, o algoritmo deve ser avaliado utilizando alguns
critérios.

1.1.3 Avaliação de Algoritmos


Algoritmos podem ser avaliados utilizando-se vários critérios. Geralmente o que
interessa é a taxa de crescimento ou espaço necessário para resolver instâncias cada vez
maiores de um problema. Podemos associar um problema a um valor chamado de
‘tamanho’ do problema, que mede a quantidade de dados de entrada.
O tempo que um algoritmo necessita expresso como uma função do tamanho do
problema é chamado de complexidade temporal do algoritmo. O comportamento
assintótico dos algoritmos (ou funções) representa o limite do comportamento de custo
quando o tamanho cresce. O comportamento assintótico pode ser definido como o
comportamento de um algoritmo para grandes volumes de dados de entrada.
A complexidade temporal de um algoritmo pode ser dividida em 3 aspectos:
Universidade Santa Úrsula 4
Disciplina: Análise de Algoritmos

1. Melhor caso – o melhor caso representa uma instância que faz o algoritmo executar
utilizando o menor tempo possível.
2. Pior caso – o maior tempo demorado pelo algoritmo para executar alguma instância.
3. Caso médio – a média de tempo que o algoritmo demora para executar.

Geralmente, o mais importante é avaliar o pior caso (porque pode inviabilizar o


algoritmo) e o caso médio, porque representa como o programa vai se comportar, na
prática, na maioria dos casos.

1.1.3.1 Avaliação Empírica


A forma mais simples de se avaliar um algoritmo é implementá-lo em um
computador e executá-lo com várias instâncias do problema. Define-se então um critério
para a eficiência, como por exemplo o tempo gasto para execução. Esse tipo de avaliação é
chamada de empírica. Com base na observação, pode-se calcular o pior caso (a instância de
execução que levou mais tempo), o melhor caso (a instância de execução que gastou menos
tempo) e o caso médio (a média do tempo gasto em todas as instâncias de execução).
O problema com esse tipo de avaliação é que o tempo gasto vai depender do
computador utilizado, do compilador, da linguagem de programação, etc.

1.1.3.2 Avaliação Teórica


Na avaliação teórica, que é a que vai ser focalizada aqui, consiste em encontrar uma
fórmula matemática que expresse o recurso (por exemplo, o tempo) necessário para o
algoritmo executar em função do tamanho dos dados de entrada.

1.1.4 Associando uma função a um algoritmo


Para encontrar uma fórmula matemática que expresse quanto tempo será necessário
para cada volume de dados de entrada, podemos utilizar primeiramente uma avaliação
empírica. Para isso, deve-se montar uma tabela relacionando volumes de dados com seus
respectivos tempos de execução. Considere por exemplo um programa fictício Raiz, que
recebe como entrada um vetor de inteiros e devolve como saída um vetor com a raiz
quadrada de cada um dos elementos do vetor. Suponha que o programa é executado várias
vezes, com diferentes números de elementos de entrada, tendo seu tempo cronometrado,
como apresentado na Tabela 1.

Número de elementos no vetor Tempo gasto para execução


(segundos)
1 0,001
10 0,01
50 0,05
100 0,1
500 0,5
1.000 1
5.000 5
10.000 10
50.000 50
100.000 100
Tabela 1: Relação volume de dados de entrada x tempo de execução
Universidade Santa Úrsula 5
Disciplina: Análise de Algoritmos

Se colocarmos esses valores em um gráfico (vide Gráfico 1), veremos que esses
valores são representados por uma reta. Lembrando um pouco das aulas de geometria,
temos que uma reta pode ser representa por uma função linear do tipo ax+b.

7000

6000

5000

4000

3000

2000

1000

0
0,001 0,01 0,05 0,1 0,5 1 2 3 4 5 6
Grafico 1

Assim, se conseguirmos encontrar uma fórmula como essa que represente o


comportamento temporal do algoritmo em relação ao número de dados de entrada, então
podemos saber o seu comportamento para qualquer volume de dados de entrada! Basta
aplicar o valor na fórmula.

1.2 Notação O, Omega e Theta


Após obter a função que representa o comportamento de um software, basta
analisá-la para termos uma medida da complexidade do software. Para isso se utiliza as três
notações a seguir: O, Omega e Theta. Elas são utilizadas também para comparar funções de
diferentes algoritmos.

1.2.1 Notação O
Esta notação é utilizada para analisar o pior caso.
Uma função g(n) é O(f(n)) se ∃ c>0 e n0 tais que g(n) <= c f(n) para n>=n0.
Explicação: uma função g(n) é da ordem de complexidade de f(n) se existe uma constante c
e um valor n0 tal que para qualquer valor de n maior do que n0 g(n) é menor ou igual a
c.f(n).
Isso significa que:
• f(n) é um limite superior para g(n)
• f(n) denomina assintoticamente g(n)

Propriedades:
• f(n) = O(f(n))
• c. f(n) = O(f(n)), c=constante
• O(f(n)) = O(f(n)) = O(f(n))
Universidade Santa Úrsula 6
Disciplina: Análise de Algoritmos

• O(O(f(n))) = O(f(n))
• O(f(n)) = O(g(n)) = O(max(f(n),g(n))
• O(f(n)) . O(g(n)) = O (f(n) . g(n))

1.2.2 Notação Ω
Esta notação é utilizada para analisar o melhor caso.
Uma função f(n) = Ω(g(n)) se existem constantes c e no, tal que c.g(n) <=f(n) para
n>=no.
Isso significa que:
• f(n) é um limite inferior para g(n)

1.2.3 Notação Θ
Esta notação é utilizada para analisar o caso médio.
Uma função f(n) = Θ(g(n)) se existem constantes c1, c2 e no tais que c1. g(n) <=
f(n) <= c2.g(n) para n>=no.
Universidade Santa Úrsula 7
Disciplina: Análise de Algoritmos

A notação O é a mais utilizada, porque geralmente o mais importante é descobrir o


pior caso, para saber se existe alguma possibilidade de o algoritmo falhar, isto é, não
conseguir executar caso se entre com um volume de dados demasiado grande.
Fazendo uma analogia para melhor compreensão das notações, temos o seguinte,
considerando que f e g são funções que representam o comportamento de dois algoritmos
diferentes.

F(n) = O (g(n)) f<=g


F(n) = Ω (g(n)) f>=g
F(n) = Θ (g(n)) f=g

1.3 Crescimento assintótico de funções


A complexidade assintótica de um algoritmo geralmente determina o tamanho dos
problemas que poderão ser resolvidos por esse algoritmo. Assim, se um algoritmo processa
dados de tamanho n em um tempo cn2 para uma constante c, então dizemos que a
complexidade temporal do algoritmo é O(n2) (leia-se: da ordem de n2).
Por exemplo, suponha os cinco algoritmos abaixo:

Algoritmo Complexidade
A1 n
A2 n log n
A3 n2
A4 n3
A5 2n

A complexidade de tempo é o número de unidades de tempo necessárias para


processar uma entrada de tamanho n. Assim, assumindo por exemplo a unidade de tempo
como sendo um millisegundo, o algoritmo A1 precisa, para processar 1000 entradas, de
1000 millisegundos, ou seja, 1 segundo. Já o algoritmo A3 precisa de 1 milhão de
segundos. Enquanto o A1 consegue processar 1000 entradas em um segundo, o A5 só
consegue processar 9 entradas em 1 segundo.

1.3.1 Principais Classes de Complexidade


A tabela a seguir mostra as principais classes de complexidade de funções. Quanto
mais abaixo na tabela, mais complexo é o algoritmo que a função representa.

Função Nome
1 Constante
log n Logarítmica
log2 n Log quadrático
n Linear
n log n n log n
n2 Quadrática
n3 Cúbica
2n exponencial
Universidade Santa Úrsula 8
Disciplina: Análise de Algoritmos

2 Análise da Complexidade de Algoritmos


2.1 Estimativas de Tempo de Execução
Sabemos que os principais recursos ocupados por um software em execução são
espaço em disco, espaço em memória e tempo. Mas enquanto o espaço é uma questão que
pode ser facilmente resolvida, o tempo que um programa gasta para executar pode
inviabilizar o seu uso. Portanto, vamos focalizar na estimativa de tempo de execução.
Já vimos que o mais importante é tentar avaliar o tempo de execução para grandes
volumes de dados de entrada. Para isso, vamos tentar achar uma fórmula que expresse o
tempo de execução em função do volume de dados de entrada.
Dependendo da fórmula encontrada, podemos classificar e comparar a
complexidade de diferentes algoritmos.

Exercícios
1. Qual é a ordem de complexidade das seguintes funções (utilize a notação O).
a) f(n) = n2 + 2
b) g(n) = 503
c) g(n) = 2 log n + n
d) g(n) = 10. 2n
e) f(n) = n log n + log n

2. Qual dessas funções possui a maior ordem de complexidade?

3. Arranje as seguintes expressões de acordo com a taxa de crescimento (da menor


para a maior): 4n2, n!, log3n, 3n, 20n, 2, log2n.

Vejamos então algumas regras simples para se obter essa fórmula a partir de um
algoritmo.

2.1.1 Regras para Análise de Algoritmos

1 – Atribuições simples, declarações, etc


Analise os exemplos de comandos a seguir:

int x=1;
x= (1+y);
readln( );
writeln(“Digite um número”);

Podemos facilmente notar que qualquer desses comandos, a não ser que esteja
dentro de um laço, será executado uma única vez. Ou seja, são comandos que não
dependem do volume de dados de entrada.
Portanto, dizemos que esses comandos têm ordem de complexidade constante, ou O(1).
Universidade Santa Úrsula 9
Disciplina: Análise de Algoritmos

2 – Laços, For-to-do, while, repeat, ...


O tempo de execução de um laço é, no máximo, a soma dos tempos de execução de
todas as instruções dentro do laço (incluindo todos os testes) multiplicado pelo número de
iterações.
Por exemplo, considere o laço a seguir:

For (i=1;i<=10;i++)
{
vetor[i]=i; O(1)
vetor[i+1]=x; O(1)
}

Para encontrar a complexidade desse trecho de código, devemos inicialmente somar


o tempo de execução de todas as instruções dentro do laço. Temos portanto: O(1) + O(1) =
O(1+1) = O(2).
Agora multiplicamos esse valor pelo número de iterações do laço. Como o laço vai
de 1 até 10, o número de iterações é 10. Portanto, temos: O(10) x O(2) = O(10x2) = O(20).
Ou seja, esse trecho de código vai demorar 20 unidades de tempo para ser executado.
Como o tempo é constante, isto é, independente do volume de dados de entrada,
poderíamos simplificar ainda mais e dizer que a complexidade é simplesmente O(1).

3 – Laços aninhados
Devem ser analisados de dentro para fora. O tempo total de execução de uma
instrução dentro de um grupo de laços aninhados é o tempo de execução da instrução
multiplicado pelo produto do tamanho de todos os laços.
Exemplo:

For(i=1;i<n;i++)
{
For(j=1;j<n,j++)
{
k= k+1; O(1)
}
}

Neste caso, só temos um comando propriamente dito, que é constante e, portanto,


tem ordem O(1). Esse valor corresponde ao tempo total de execução dentro do grupo de
laços. Agora é necessário, portanto, multiplicá-lo pelo produto do tamanho de todos os
laços.
O primeiro laço será executado n vezes e, portanto, tem tamanho n. O mesmo vale
para o segundo laço, que também será executado n vezes.
Portanto, temos: O(n) . O(n) = O(n . n) = O(n2)
O(1) . O(n2) = O(1. n2) = O(n2)

4 – Instruções consecutivas
Simplesmente soma-se a complexidade de cada instrução, sendo que os termos de
menor ordem são ignorados.
Exemplo:
Universidade Santa Úrsula 10
Disciplina: Análise de Algoritmos

for(i=1;i<n;i++){
a[i] = 0;
}
for (i=1;i<n;i++) {
for(j = 1;j<n; j++) {
a[i] = a[j] + 1;
}
}

Se calcularmos, veremos que a ordem de complexidade do primeiro laço é O(n),


enquanto a ordem de complexidade do segundo laço é O(n2). Portanto, como os dois laços
são consecutivos e independentes, simplesmente soma-se a complexidade de cada
instrução.
Assim, a equação que descreve a complexidade desse trecho de código seria n2 +n.
No entanto, como sabemos que o mais importante é o maior tempo, podemos
simplificar essa fórmula. Vimos acima, no item Propriedades, que a soma de duas
complexidades é o maior valor. Portanto:

O(n) + O(n2) = O(max(n, n2)) = O(n2)

5 – If-then-else
Considere o exemplo abaixo:

If condição then
expressão1;
Else
expressão2;

O tempo de execução de um comando do tipo if-then-else nunca é maior do que o


tempo de execução do teste condicional em si mais o tempo de execução da maior entre as
expressões 1 e 2. Assim, se a expressão1 é O(n3) e a expressão 2 é O(n), então a
complexidade é O(n3) + O(n) = O(n3).

6 – Chamadas de Função
A análise segue a mesma regra de laços aninhados: analise tudo de dentro para fora.
Ou seja, para calcular a complexidade de um programa com várias funções, calcule
primeiramente a complexidade de cada uma das funções e depois considere cada uma das
funções como uma instrução, com a complexidade da função.

7 - Recursão
Existem dois tipos de casos. No caso de algoritmos recursivos mais simples, pode-
se simular uma linearização, substituindo-se a chamada recursiva por alguns laços
aninhados ou por uma outra subrotina extra e eventualmente uma pilha para controlá-la.
Neste caso, o cálculo é simples e pode ser feito depois da linearização. O segundo caso é
com algoritmos recursivos mais complexos, quando não é possível realizar a linearização.
Universidade Santa Úrsula 11
Disciplina: Análise de Algoritmos

Neste caso obtemos uma relação de recorrência que tem que ser resolvida e é uma tarefa
matemática menos trivial.
Vejamos então, inicialmente, um exemplo do primeiro caso, que é mais simples.
Exemplo: fatorial recursivo e fatorial linearizado:

Fatorial _recursivo(n);
Início
Se n<= 1 então
retorne 1;
Senão
Retorne (n* fatorial_recursivo(n-1));
Fim

Fatorial_linear(n);
Início
Fatorial 1;
Para i de 2 até n
Fatorial fatorial * i;
Retorne fatorial;
Fim

Como podemos perceber, as duas funções fazem a mesma coisa: isto é, calculam o
fatorial de n. No entanto, o primeiro algoritmo o faz de forma recursiva, já o segundo o faz
de forma linear, com complexidade O(n).

Exercício: analisar a complexidade assintótica dos seguintes algoritmos:

Procedure exemplo1(var a: vetor);


Begin
For i:= 1 to n-1 do
Begin
min:= i;
for j:= i+1 to m do
if a[j] < a[min]
then min:= j;
x:= a[min];
a[min]:= a[i];
a[i] = x;
end;
end;
Universidade Santa Úrsula 12
Disciplina: Análise de Algoritmos

Procedure exemplo2(var a:vetor);


Begin
read(x);
if (x<0)
a[1]:= 0;
else if (x<5)
a[1]:= 1;
else for i:= 1 to n do
begin
a[i]:= i+1;
end;
End;

Procedure exemplo3(var a: vetor);


Begin
For i:= 1 to n do
Begin
For j=1 to 10 do
Begin
min:= i;
for k:= i+1 to m do
if a[k] < a[min]
then min:= k;
x:= a[min];
a[min]:= a[i];
a[i] = x;
end;
end;
end;
Universidade Santa Úrsula 13
Disciplina: Análise de Algoritmos

Bibliografia
Laporte, G., Asef-Vazir, A. And Sriskandarajah, C. Some applications of the generalized
traveling salesman problem. JORS 47, 1996.

Salomon, M., Solomon, M., van Wassenhove, L., Dumas, Y and Dauzère-Pérès, S. Solving
the discrete lotsizing and scheduling problem with sequence dependent set-up costs and
set-up times using the traveling salesman problem with time windows. EJOR, 100, 1997.

Whitley, D., Starkwheather, T. and Shaner, D. The traveling salesman and sequence
scheduling: quality solutions using genetic recombinations. Handbook of Genetics
Algorithms. Edt. L. Davis van Nostrand, 1991.

Bodin, L., Golden, B., Assad, A. and Ball, M. Routing and Scheduling of vechicles and
crews: the state of the art. Special Issue. England: Pergamon Press, 1983.

Chauny, F., Haurie, A., Wagneur, E. e Loulou, R. Punch Operations in a Flexible


Manufacturing Cell a Three-Dimentional Space-Filling Curve Approach. INFOR 25(1),
1987.

Finke, G and Kusiak, A. Network Approach to Modeling of Flexible Manufacturing


Modules and Cells. APPII – 0399-0516. Department of Applied Mathematics Technical
Report. University of Nova Scotia, Nova Scotia, Canadá, 1985.

Fontes da Internet:
http://www.inf.ufpr.br/~andre/Disciplinas/BSc/CI065/michel/Intro/intro.html

Você também pode gostar