Complexidade de Algoritmos Eficiência Assintótica

Propaganda
Complexidade de Algoritmos
Eficiência Assintótica
Roberta Geneci Neves Weber
Rafael Coninck Teigão
Fundamentos de Engenharia de Software
Programa de Pós-Graduação em Informática Aplicada
Pontifı́cia Universidade Católica do Paraná
[email protected]
[email protected]
Maio de 2005
Resumo
O presente documento faz uma explanação sobre complexidade de algoritmos. Nele será abordada
uma metodologia para a realização da análise de eficiência assintótica de algoritmos recursivos e
não-recursivos, com ênfase para a notação O, além de uma apresentação ilustrativa das principais
funções de complexidade.
1
1
Introdução
Para um dado problema com solução computacional, é possı́vel criar uma infinidade de algoritmos
que o resolvam. Como podemos definir se um algoritmo é melhor que o outro? Assumindo que todos
sejam eficazes1 , devemos, então verificar qual é o mais eficiente.
Eficiência pode ser definida como a capacidade de se atingir um resultado correto, utilizando a
menor quantidade de recursos e tempo possı́vel. Ou seja, para que um algoritmo seja mais eficiente
que outro, ele deve conseguir o resultado correto em menor tempo e/ou utilizando menos espaço (como
memória e disco rı́gido).
Como atualmente os recursos espaciais podem ser conseguidos de forma barata (em comparação
ao preço de uma ou duas décadas atrás e ao custo de um processador), fica clara a importância
de se analisar outro fator quando se compara dois algoritmos: o tempo 2 de execução, como em
[Koerich, 2005].
Uma das formas de se analisar o tempo de execução de um algoritmo é conhecida como análise
assintótica, ou cáculo da eficiência assintótica, em que se procura encontrar uma tendência no
tempo de execução, quando o volume de dados de entrada do problema tende ao infinito.
Neste documento, abordaremos esta forma de análise, e mostraremos, com exemplos, como ela
pode ser aplicada.
2
Processo de Análise
Quando é considerado todo o conjunto de entradas válidas para um algoritmo, percebe-se que em
muitos casos não é viável testar todos os elementos deste conjunto. E isso é especialmente verdade
para os casos em que a entrada tende ao infinito.
Mas como, então, comparar dois algoritmos com um conjunto muito grande de entradas? Para
isso, foi criado um processo de análise, fundamentado em métodos matemáticos, que fornece uma
modelagem comportamental que permite comparar estes algoritmos.
Outra vantagem clara de se possuir um modelo matemático é a separação do algoritmo da implementação. Um bom algoritmo mal implementado pode ter um resultado inferior a outro menos
eficiente, porém bem implementado.
O processo de análise envolve 3 etapas:
• identificar o conjunto de entrada;
• identificar a operação base, que será executada para cada elemento da entrada; e
• definir que tipo de entrada será estudado.
A definição do tipo da entrada é muito importante, pois define como o processo será conduzido.
Depedendo deste tipo, estaremos estudando:
• o pior caso (notação O);
• o caso médio (notação Θ); ou
• o melhor caso (notação Ω).
Estes casos serão vistos a seguir3 . Para ilustrá-los, considere o seguinte exemplo:
1
resolvem o problema de maneira correta
a palavra ”tempo” é utilizada neste documento como o número de passos necessários para se resolver um algoritmo.
3
as notações o e ω não serão tratadas neste documento
2
2
Dado o vetor de caracteres abaixo, deseja-se buscar três caracteres diferentes: ”A”, ”D”
e ”K”. Sendo um algoritmo de busca sequencial, como o algoritmo 1, que compara um
caractere após o outro, do primeiro ao último, este algoritmo terá o seu pior caso para
encontrar a letra ”D”, melhor caso para a letra ”K”, e caso médio para a letra ”A”.
K E T
G U
O P
A M
G L Q Z C V
D
Algoritmo 1 Busca sequencial pelo elemento e no vetor vet de tamanho n
1: PARA i = 1 até n FAÇA
2:
SE e == vet[i] ENTÃO
3:
pare;
4:
FIM SE
5: FIM PARA
2.1
Notações Θ e Ω
A notação Θ, o caso médio, pode ser definida matematicamente pela equação 1, em que c 1 e c2 são
constantes positivas, n é um elemento do conjunto da entrada, n0 é o primeiro elemento em que este
comportamento se apresenta e g(n) é a função que relaciona a entrada ao tempo de execução.
Θ(g(n)) = {f (n) : ∃ c1 ∧ c2 | ∀n ≥ n0 , 0 ≤ c1 g(n) ≤ f (n) ≤ c2 g(n)}
(1)
A figura 1 representa esta equação graficamente.
c2.g(n)
f (n)
c1.g(n)
n
n0
Figura 1: Representação gráfica de f (n) = Θ(g(n)).
Esta notação representa o comportamento do algoritmo no caso médio e Θ(g(n)) define a classe
das funções f (n) que crescem na mesma taxa de g(n). Isto é, para o caso da busca sequencial, quando
o número de caracteres no vetor tende ao infinito, o tempo para se encontrar um elemento qualquer
neste vetor é, em média f (n) = Θ(g(n)).
Já a notação Ω, o melhor caso ou limite assintótico inferior, pode ser definida matematicamente
pela equação 2 na próxima página.
3
Ω(g(n)) = {f (n) : ∃ c | ∀n ≥ n0 , 0 ≤ cg(n) ≤ f (n)}
(2)
A figura 2 representa esta equação graficamente.
f (n)
c.g(n)
n
n0
Figura 2: Representação gráfica de f (n) = Ω(g(n)).
Ω(g(n)) define a classe das funções f (n) que crescem pelo menos tão rapidamente quanto g(n).
Voltando ao algoritmo 1 na página precedente, vamos aplicar o processo de análise para encontrar
f (n) = Θ(g(n)) e f (n) = Ω(g(n)):
1. Identificar o conjunto de entrada: A entrada mais relevante é o vetor vet, pois é a única cujo
tamanho influencia o tempo de execução. O elemento e também é interessante para podermos
definir os casos médio e melhor.
2. Identificar a operação base: Normalmente a operação base é a que se encontra no loop mais
interno, ou seja, a operação ”SE”.
3. Definir o tipo de entrada que será estudado: vamos estudar os casos em que o elemento e está
na primeira posição do vetor (melhor caso) e ao redor da metade (caso médio).
Para encontrar f (n), deve-se definir quantas vezes a operação base é executada para uma entrada
de tamanho n. Neste caso, a operação ”SE” é executada exatamente uma vez para cada elemento.
Assim:
f (n) = n
(3)
Agora, deve-se encontrar g(n). Para o caso médio, o elemento e estará na metade do vetor: a
n
vezes.
operação base ”SE” será executada
2
n
(4)
g(n) =
2
No cáculo de eficiência assintótica, apenas os termos de mais alta ordem são considerados (i.e. se
g(n) = n2 + n + c, apenas o termo n2 seria considerado). As constantes também são desconsideradas.
Assim:
1
g(n) = ∗ n = n
(5)
2
4
Para provar que f (n) = Θ(n), a equação 1 deve ser satisfeita.
∀n ≥ n0 , 0 ≤ c1 n ≤ n ≤ c2 n
1
sendo c1 = ∧ c2 = 2
2
n
(÷n)
0 ≤ ≤ n ≤ 2n
2
1
0≤ ≤1≤2
(6)
2
As desigualdades da equação 6 são verdadeiras, então, f (n) = Θ(n).
Quando considera-se o melhor caso, o elemento e estará na primeira posição do vetor: a operação
base ”SE” será executada apenas 1 vez.
g(n) = 1
(7)
Para provar que f (n) = Ω(1), a equação 2 deve ser satisfeita.
∀n ≥ n0 , 0 ≤ c.1 ≤ n
sendo
c = 1 ∧ n0 = 1
∀n ≥ 1, 0 ≤ 1 ≤ n
sendo n = 1
0≤1≤1
(8)
Como as desigualdades da equação 8 são verdadeiras, então, f (n) = Ω(1).
Para qualquer notação, quando g(n) = constante (e.g. g(n) = 1), diz-se que o tempo de execução
é constante.
2.2
Notação O
A notação O 4 apresenta o comportamento do algoritmo em seu pior caso ou limite assintótico
superior. É, possivelmente, a mais utilizada, pois em vários problemas, o tempo para o pior caso
pode ser inaceitável, mesmo quando o tempo do caso médio for razoável.
n
Por exemplo, suponha um algoritmo cujo caso médio é Θ(n3 ) e o pior caso é O(2n ). Considerando
1000
n = 1000, o valor 21000
faria que, quando um processamento se aproximasse do pior caso, o algoritmo
iria levar centenas de milhões de anos para obter um resultado.
Por isso é muito importante que se conheça o valor da notação O(g(n)) para um dado algoritmo.
A notação O(g(n)) pode ser definida matematicamente pela equação 9.
O(g(n)) = {f (n) : ∃ c | ∀n ≥ n0 , 0 ≤ f (n) ≤ cg(n)}
(9)
A figura 3 na próxima página representa esta equação graficamente.
O(g(n)) define a classe das funções f (n) que crescem não mais rapidamente que g(n).
Aplicando-se, novamente, o processo de análise no algoritmo 1 na página 3, e considerando f (n) = n
(equação 3 na página precedente), tem-se o pior caso quando percorre-se todos os elementos do vetor.
g(n) = n
(10)
Para provar que f (n) = O(n), a equação 9 deve ser satisfeita.
∀n ≥ n0 , 0 ≤ n ≤ cn
sendo
c = 1 ∧ n0 = 1
∀n ≥ 1, 0 ≤ n ≤ n
sendo n = 1
0≤1≤1
4
lê-se ”O”-grande ou big-Oh
5
(11)
c.g(n)
f (n)
n
n0
Figura 3: Representação gráfica de f (n) = O(g(n)).
Como as desigualdades da equação 11 são verdadeiras, então, f (n) = O(n). Nota-se, neste caso,
que g(n) é, coincidentemente, igual para as notações O e Θ.
3
Algoritmos Não-Recursivos
Quando é aplicado o método apresentado na seção 2 para algoritmos não-recursivos, procura-se, após
encontrar a operação base, construir um somatório que represente a quantidade de vezes que essa
operação é executada, em função do tamanho da entrada.
A função f (n) é encontrada com a simplificação deste somatório. Então, no caso do algoritmo de
busca sequencial, tem-se:
f (n) =
n
X
1
i=1
simplificando
f (n) = n − 1 + 1
f (n) = n
4
(12)
Algoritmos Recursivos
Algoritmos recursivos são aqueles que chamam a si próprios durante sua execução. Diferente dos
algoritmos não-recursivos, não é facilmente deduzı́vel um somatório que represente o número de vezes
que a operação base é executada. Para se encontrar este valor, busca-se uma relação de recorrência.
O algoritmo 2 na página seguinte é um exemplo que utiliza recursão.
A operação base é a multiplicação, cujo número de execuções para n, que será representado por
T (n), é o número de execuções para n − 1, T (n − 1), somado com a execução atual, 1. Assim, tem-se:
T (n) = T (n − 1) + 1
(13)
Para encontrar a relação de recorrência, deve-se buscar a condição de parada. Na linha 2, vê-se
que, quando n é igual a 0, o algoritmo pára a recursão e retorna apenas o valor 1. Com a condição de
parada e a equação 13, pode-se criar a relação de recorrência.
6
Algoritmo 2 Encontra o fatorial do número n
1: Fatorial(n)
2: SE n == 0 ENTÃO
3:
retorne 1;
4: SENÃO
5:
retorne F atorial(n − 1) ∗ n;
6: FIM SE
T (n) =
T (n − 1) + 1
1
para n > 0
para n = 0
(14)
Resolvendo a recorrência, tem-se:
T (n) = T (n − 1) + 1 = T (n − 2) + 1 + 1 = T (n − 3) + 1 + 1 + 1 = · · · = T (n − n) + n = T (0) + n (15)
Substituindo-se a equação 14 na 15, obtem-se:
T (n) = 1 + n
(16)
O resultado da simplificação da relação de recorrência é o valor de f (n). Neste caso, f (n) = n.
Como não é possı́vel, neste algoritmo, diferenciar entre os casos pior e médio, Θ(g(n)) = O(g(n)),
pode-se encontrar apenas um valor para g(n) para esses casos. O melhor caso é quando n = 1, em
que o tempo é constante, Ω(1). Assumindo-se que sempre deve-se passar exatamente n vezes pela
operação base, então g(n) = n. É trivial provar que f (n) = O(n) para este algoritmo.
Porém, nem sempre é trivial encontrar a solução da relação de recorrência. Para tanto, existem
três métodos5 :
Substituição uma solução é arbitrada e verificada matematicamente, através de indução para encontrar as contantes.
Árvore de Recursão a recorrência é convertida em uma árvore e a sua altura é convertida para
uma solução utilizável na Substituição.
n
Mestre para recorrências da forma T (n) = aT ( )+h(n), em que a ≥ 1, b > 1 e h(n) > 0, compara-se
b
nlogb a com h(n).
5
Principais Funções de Complexidade
A tabela 1 na página seguinte mostra as principais funções de complexidade, e o tempo necessário
para serem resolvidas para o tamanho de algumas entradas ilustrativas.
6
Conclusão
Neste documento foi apresentado uma metodologia para analisar a eficiência assintótica de algoritmos
recursivos e não-recursivos. Porém, esta breve introdução cobre apenas uma parte pequena de todo o
universo de análise de complexidade de algoritmos.
Outros tópicos, além da eficiência assintótica, que são de relevância para a análise de complexidade
são os problemas de decisão, as classes de complexidade e tempo polinomial determinı́stico
(P) versus tempo polinomial não-determinı́stico (NP).
Todo esse conjunto de ferramentas de análise fazem parte de uma área de conhecimento que visa
não apenas entender as limitações dos algoritmos, mas, também, melhorar a qualidade e a eficiência
do software desenvolvido. Todo desenvolvedor deveria saber utilizar corretamente estas ferramentas.
5
estes métodos serão citados, mas não serão desenvolvidos neste documento.
7
Tabela 1: Principais funções de complexidade
Nome
Contante
Logarı́tmica
Linear
n log n
Quadrática
Cúbica
Exponencial
Fatorial
Função
1
log n
n
n log n
n2
n3
2n
n!
102
1
2
102
2 ∗ 103
104
106
1.267650600228 ∗ 1030
9.332621544394 ∗ 10157
Referências
[Koerich, 2005] Koerich, A. (2005). Notas de aula.
8
103
1
3
103
3 ∗ 106
106
109
1.0715086072 ∗ 10301
∞
106
1
6
106
6 ∗ 109
1012
1018
∞
∞
Download