Apostila algoritmo

13
Universidade Santa Úrsula APOSTILA I DA DISCIPLINA ANÁLISE DE ALGORITMOS Alex Avellar

Transcript of Apostila algoritmo

Page 1: Apostila algoritmo

Universidade Santa Úrsula

APOSTILA I DA DISCIPLINA ANÁLISE DE ALGORITMOS

Alex Avellar

Page 2: Apostila algoritmo

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:

Figura 1 : Exemplo de elementos de um algoritmo

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.

Page 3: Apostila 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.

Figura 2 . Exemplo de instância da execução de um algoritmo

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:

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

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

Page 4: Apostila algoritmo

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

Page 5: Apostila algoritmo

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.

0

1000

2000

3000

4000

5000

6000

7000

0,001 0,01 0,05 21 3 4 5 60,1 0,5

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))

Page 6: Apostila algoritmo

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.

Page 7: Apostila algoritmo

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

Page 8: Apostila algoritmo

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).

Page 9: Apostila algoritmo

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:

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:

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:

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

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

Page 10: Apostila algoritmo

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

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:

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.

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;

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

Page 11: Apostila algoritmo

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:

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;

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

Page 12: Apostila algoritmo

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

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;

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;

Page 13: Apostila algoritmo

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