Você está na página 1de 43

Análise de Algoritmos

Aula 1

Prof. João Paulo R. R. Leite


joaopaulo@unifei.edu.br
Universidade Federal de Itajubá
Este curso será focado
especialmente no estudo de Teremos duas avaliações principais,
algoritmos, e análise e discussão de nos dias 11/10 e 13/12, que serão
seus desempenhos, passando pela provas escritas individuais sem
notação adequada, por algoritmos consulta.
de grafos, ordenação, alguns
paradigmas de solução de Além disso, no decorrer do
problemas, estruturas de dados, etc. semestre, provavelmente teremos
alguns trabalhos e/ou listas de
O único pré-requisito é o exercícios para complementar a
conhecimento da linguagem de nota.
programação C/C++ e noções
básicas de álgebra.
CLRS | Cormen | Toscani | Papadimitriou
Um estudioso da era de ouro de
desenvolvimento científico do mundo
islâmico (séculos VIII a XIII), chamado Al-
Khwarizmi, viveu em Bagdá no século IX.

Escreveu um livro, estabelecendo os passos


de métodos básicos para adicionar,
multiplicar e dividir números, além de muitas
outras coisas como extrair raiz quadrada e
calcular os dígitos do Pi.

Al-Khwarizmi → Algoritmo!
O que são algoritmos?
Sequência de instruções ordenadas de forma lógica para a solução
de um determinado problema ou realização de alguma tarefa bem
especificada.
• Precisos, não ambíguos, mecânicos, eficientes, corretos.
• Receita de bolo, manual passo a passo.

Podemos trazer o conceito para o campo da computação, criando


uma série de passos que resolvem computacionalmente um
problema, traduzindo os passos em instruções de uma linguagem
de programação.

Algoritmo, no nosso caso, é um procedimento computacional bem


definido que toma algum valor ou conjunto de valores como
entrada e produz algum valor ou conjunto de valores como saída.
Onde são utilizados?
Onde são utilizados?

Internet: Mecanismos de Busca, roteamento de pacotes,


compartilhamento de arquivos, mapas e caminhos.
Biologia: Projeto Genoma, diagnóstico, vacinas.
Computadores: Leiaute de circuitos, sistemas de arquivos,
compiladores, agendamento de processos.
Computação Gráfica: Filmes, jogos, realidade virtual.
Segurança: Celulares, comércio eletrônico.
Multimídia: MP3, MP4, JPEG, HDTV, Reconhecimento facial.
Redes Sociais: Recomendações, feeds de notícias, anúncios.
Física: Simulações gravitacionais, colisão de partículas.
Inteligência Artificial: Redes neurais, aprendizado por reforço.
Mas... análise?
Analisar um algoritmo significa prever os recursos de que o
algoritmo necessita (frequentemente, tempo de computação).
Tem muito a ver com a palavra “desempenho”

Vamos aprender a analisar as alternativas ou algoritmos


candidatos para a solução de um problema e escolher o mais
eficiente, ou verificar quais deles são viáveis.

Para uma entrada de tamanho n, quanto tempo gastaria um


determinado algoritmo para resolver o problema? 10 segundos? E
se eu dobrar o tamanho da entrada? Resolve em 20 segundos? Ou
demora 100, 1000? Veremos! :)
• Algoritmos são o cerne da computação
– Procedimento, ou conjunto de regras, codificando o
problema em uma série de operações que geram uma
saída correta para cada entrada.
– Problema real → Programa de computador
– Algoritmo é considerado correto se, para toda instância da
entrada, ele gera uma saída correta → resolve o problema.
• Qualquer algoritmo correto é aceitável?
– Na prática, é fundamental que a utilização de recursos seja
razoável (memória, tempo, etc.).
• Exemplo: Sistemas de Equações Lineares
Para a resolução de sistemas de equações lineares, o Método de Cramer
é viável. No entanto, o consumo de tempo cresce com ordem fatorial
(n!), diferente do Método de Gauss, que cresce mais lentamente (n3).

Ordem da Matriz (n) Tempo Tempo


(Método de Cramer) (Método de Gauss)
2 22 μs 50 μs
3 102 μs 159 μs
4 456 μs 353 μs
5 2,35 ms 666 μs
10 1,19 min 4,95 ms
20 15225 séculos 38,63 ms
40 5x1033 séculos 0,315 s
6000

5000

4000

3000
Cramer

2000

1000
Gauss

0
1 2 3 4 5 6 7
• Exemplo: Sequência de Fibonacci
O matemático italiano do século XV Leonardo Fibonacci é
amplamente conhecido por sua sequência de números:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...

Onde:
F(n-1) + F(n-2), se n > 1
F(n) = 1, se n = 1
0, se n = 0
• Exemplo: Sequência de Fibonacci
Como implementar o código para o cálculo do enésimo
número da sequência de Fibonacci?
1) Implementação intuitiva: Recursiva

Repare que cada chamada da função


dispara outras duas chamadas, fazendo
com que o tempo para a execução do
algoritmo aumente exponencialmente
(2n) com a entrada n.
• Exemplo: Sequência de Fibonacci
Como implementar o código para o cálculo do enésimo
número da sequência de Fibonacci?

2) Implementação iterativa Repare que


• O evento mais custoso da
função é um laço interno (for),
que faz a mesma operação
apenas n-1 vezes e;
• Os resultados intermediários
são armazenados em um
vetor.

Portanto, o tempo gasto sobe


apenas linearmente (n) em
relação ao tamanho da entrada n!
• Exemplo: Sequência de Fibonacci
– Linear versus Exponencial

Para entradas pequenas, o primeiro algoritmo é mais rápido do que o segundo.


No entanto, repare no comportamento para entradas suficientemente grandes.
Exemplo: Transformada de Fourier
Jean Fourier Friedrich Gauss

• Quebra uma forma de onda com N


amostras em componentes
periódicos (tempo → frequência).
• Aplicações: Processamento de
sinais, JPEG, MRI, astrofísica.
• Método convencional: ~N2 passos.
• Algoritmo FFT: ~n*log n passos,
viabilizando a nova tecnologia.
Algoritmos vs. Avanços Tecnológicos

Avanços tecnológicos diminuem a importância da


complexidade?
Máquinas mais rápidas passam a resolver problemas
maiores, mas é a complexidade do algoritmo que
dimensiona o ganho de desempenho. Quanto menor a
complexidade do algoritmo, maior o impacto da tecnologia.

Uma máquina 100 vezes mais rápida consegue resolver


problemas 100 vezes maiores, utilizando um algoritmo correto
qualquer?
Algoritmos vs. Avanços Tecnológicos
• Consideremos um problema de complexidade linear (n).
Se o tamanho máximo de problema que ele consegue resolver
em um tempo t for L (t ∝ L), em um computador 10 vezes
mais rápido ele conseguirá resolver um problema de tamanho
máximo y = 10*L.

• Agora, um problema de complexidade quadrática (n2):


O tamanho máximo de problema resolvido em um tempo t é L
(t ∝ K2). Para uma máquina 10 vezes mais rápida, o tamanho
máximo y do problema resolvido será k*L, onde k2 = 10 ou
seja, k = sqrt(10) = 3,16. y = 3,16*L.
A busca de algoritmos de menor complexidade torna
problemas antes intratáveis em problemas com
solução viável, além de aumentar o impacto e os
benefícios do avanço tecnológico sobre a resolução
deste mesmo problema.

Até meados de 1965 não havia nenhuma previsão real sobre o


futuro do hardware, quando o então presidente da Intel,
Gordon Earl Moore fez sua profecia, na qual o número de
transistores dos chips teria um aumento de 60%, pelo mesmo
custo, a cada período de 18 meses. Essa profecia tornou-se
realidade e acabou ganhando o nome de Lei de Moore -> Poder
de processamento dobra a cada dois anos!.
Análise de Algoritmos

A apresentação da análise de desempenho e complexidade


de algoritmos como uma tarefa sistemática é necessária
para tornar sua prática parte do dia-a-dia do projetista ou
programador e facilitar sua utilização.

A busca pela maior eficiência deve ser sempre o propósito


do desenvolvedor desde as fases iniciais do projeto. Não é
suficiente conhecer: a análise deve ser utilizada como
ferramenta no dia-a-dia!
Como fazer a análise?
A análise e os cálculos são realizados de maneira
muito particular para cada algoritmo e dependem
de seu funcionamento e sua estrutura.

Nas próximas aulas, serão apresentados os


métodos para o cálculo da complexidade do
algoritmo baseado na análise matemática de cada
uma das partes que o compõem, desde de
estruturas simples como testes booleanos e
comparações até laços aninhados e recursão.
Como fazer a análise?
A análise matemática do algoritmo possui algumas
vantagens sobre a medida empírica de consumo de
recursos (tempo, memória):
Ela é independente de implementação (linguagem,
hardware utilizado, multitarefa, etc.);

E pode ser realizada desde a fase de projeto,


antecipando um indicador de desempenho para
antes da implementação.
Como medir o desempenho?
Quando executamos algum programa,
normalmente queremos que o problema seja
resolvido corretamente e que a resposta seja gerada
no menor tempo possível. Portanto, o tempo é uma
ótima medida de desempenho:
Quanto mais rápido, melhor.

Geralmente esta é a medida mais utilizada e


mais abordada nos livros.
Como medir o desempenho?
Mas existem outras medidas...

Complexidade do algoritmo = Esforço


computacional para sua execução = Quantidade
de trabalho (tempo e espaço)

Outra medida importante e muito conhecida é a utilização


de memória (medida de espaço). Normalmente é desejável
que um programa execute utilizando o mínimo de memória
possível. Quanto menos espaço ocupar, melhor.
Como medir o desempenho?
• A quantidade de trabalho realizado em um algoritmo
quase sempre depende do tamanho da entrada n
(ex: ordenação, multiplicação de matrizes).
• Por este motivo, a complexidade do algoritmo será
expressa não por um valor numérico literal, mas sim
por uma função f(n) do tamanho da entrada.
– Esta função irá refletir e representar o esforço
computacional para executá-lo sobre um conjunto de
entradas de um tamanho n qualquer.
Mas todas as execuções com entradas do mesmo
tamanho serão realizadas no mesmo tempo? NÃO.

Imagine o exemplo de um vetor que deve ser ordenado utilizando


Insertion Sort. Quantas comparações ocorreriam para cada entrada?

8 5 3 1 2

5 8 3 1 2

3 5 8 1 2

... E assim por diante


Podemos verificar que entradas diferentes levariam a
tempos de execução diferentes:

Vetor previamente ordenado


1 2 3 4 5 6 7 8 1. Melhor Caso
Menor número possível de operações

Vetor parcialmente ordenado


3 5 8 2 1 4 6 7 2. Caso Médio
Número médio de operações. A execução
leva, em geral um tempo próximo a este para
terminar

Vetor em ordem inversa


8 7 6 5 4 3 2 1 3. Pior Caso
Caso em que a execução leva mais tempo
para ser executada: Limite superior
Agora imagine um algoritmo de busca sequencial em um vetor:

Caso o vetor seja {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, temos tempos de execução


diferentes, dependendo da chave da busca:
Melhor Caso
1. Chave é o primeiro elemento (0), tempo constante.
Caso Médio
2. Chave é um número no meio do vetor (3, 7, etc.)
Pior Caso
3. Chave é o último elemento do vetor (9) ou está ausente.
Faz n comparações.
Todos estes casos me interessam?

Talvez! Mas, geralmente, a análise da complexidade do


algoritmo se concentra apenas na descoberta do tempo de
execução no pior caso. Mas porque?

Estabelece um limite superior, que nos garante


1. que o algoritmo nunca irá demorar mais tempo.
Em algumas aplicações o pior caso acontece
2. com bastante freqüência (Banco de dados)
A análise do caso médio é mais complicada e,
3. em muitos casos, é igual ao pior caso.
Notação Assintótica
Uma análise inteligente é sempre baseada nas simplificações
certas, visando abstrair (deixar de lado) detalhes minuciosos e
concentrar-se nos pontos mais importantes.

O tempo gasto por um passo computacional do seu algoritmo


depende fortemente do processador em questão, e de
especificações da arquitetura, como estratégia de cache,
geração do hardware, etc.

O melhor seria, portanto, buscar uma caracterização mais


simples, independente de máquina, para representar a
eficiência de um algoritmo.
Notação Assintótica
Para medir a complexidade de um algoritmo,
expressaremos o tempo de execução contando o
número de passos básicos de computação como uma
função do tamanho da entrada.
– Refletindo a quantidade de trabalho realizada.

• Exemplos:
– Comparações, operações aritméticas, visita a nó de um
grafo (mudando valor de flag), etc.
Notação Assintótica
Digamos que a contagem do número de execuções da
operação fundamental do algoritmo em função do tamanho
n da entrada tenha chegado a uma expressão do tipo:
3n² + 4n + 8.

Na verdade não estamos interessados na expressão exata.


O que queremos saber é a ordem de crescimento da
função. Como ela se comporta no tempo, especialmente
para entradas que sejam suficientemente grandes.
Gostaríamos de saber seu comportamento assintótico!
– Com o decorrer do tempo, os termos de menor ordem (4n e 8)
vão se tornando cada vez menos significativos, de maneira que
podemos desprezá-los.
0
10000
20000
30000
40000
50000
60000
70000
80000

1
5
9
13
17
21
25
29
33
37
41
45
49
53
57
61
65
69
73
77
81
85
89
93
97
101
105
109
113
117
121
125
129
133
137
141
145
149
Notação Assintótica

• Por isso, escolhemos apenas o termo mais significativo


para o crescimento da função (n2) e desprezamos os
demais termos e as constantes.
– Apenas a ordem de crescimento é suficiente para nossa análise.
• Podemos então dizer que o algoritmo cuja complexidade
foi expressa por 3n² + 4n + 8 depois da contagem de
passos básicos de computação possui um tempo de
execução na ordem de Θ(n²) (Theta de n ao quadrado).

Beleza, mas o que é Θ(f(x)) ?


Notação Assintótica
A ordem de crescimento do tempo de execução
nos apresenta uma caracterização simples da
eficiência do algoritmo e nos permite realizar
comparações entre diferentes soluções para um
mesmo problema.

Várias comparações de complexidade podem ser definidas.


– As mais comuns são O, Θ e Ω.
Notação O (Big O)
Se uma função quadrática g(n) = 3n² + n cresce mais
rapidamente que uma função f(n) = 5n + 2, então
f(n) = O(g(n)).
• Define uma cota assintótica superior para a função
• Analogia: função f(n) é “menor ou igual” a g(n)
A utilização do sinal de igual é, na verdade, um
abuso de linguagem. Leremos sempre f(n) é O(g(n)).
• O sinal de igualdade neste caso não é simétrico.
• Exemplo: n² = O(n³) é verdadeiro, mas n³ = O(n²) é falso.
Notação Ω (Big-Omega)
– Se f(n) = 7n³ + 5 e g(n) = 2n, f(n) cresce menos
rapidamente do que g(n) e dizemos que g(n) = Ω(f(n))
– Define uma cota assintótica inferior para a função
– Analogia: Função g(n) é “maior ou igual” a f(n).

Notação Θ (Theta)
– Se f(n) = 2n² e g(n) = 3n² + n + 7, as duas funções
crescem com ordens equivalentes e dizemos que
f(n) = Θ(g(n))
– Define um limite assintótico exato.
– Analogia: Função g(n) é “igual” a f(n).
Funções que são Θ umas da outras possuem o mesmo
comportamento assintótico (crescem de maneira
semelhante com o tamanho da entrada).
Podemos separá-las em classes de comportamento:
– Linear Θ(n);
– quadrática Θ (n²);
– logarítmica Θ(log2n);
– exponencial Θ(2n), etc.

Θ é um caso particular de O e Ω.
– Se f(n) = O(g(n)) e f(n) = Ω(g(n)), então f(n) = Θ(g(n)).
Matematicamente podemos dizer que para
f(n) = Θ(g(n)), devem existir constantes c1, c2 e
n0 tal que:

c1.f(n) ≤ g(n) ≤ c2.f(n) para todo n ≥ n0

Formalizações semelhantes podem ser


gerada para a notação O e Ω:

f(n) ≤ c.g(n) f(n) ≥ c.g(n)


para todo n ≥ n0 para todo n ≥ n0
Resumindo...
1. Constantes multiplicativas podem ser omitidas:
5n2 se torna n2
2. Polinomiais:
na domina nb se a > b: Por exemplo, n3 domina n2
– n2 = O(n3)
3. Qualquer exponencial domina qualquer polinomial:
3n domina n5 (e domina até 2n)
– n5 = O(3n)
4. Qualquer polinomial domina qualquer logaritmo:
n domina (log n), n2 domina n*log n.
– n*logn = O(n2)
Resumindo...

Você também pode gostar