Escolar Documentos
Profissional Documentos
Cultura Documentos
Análise de algoritmos
1.1 Primeiras palavras
Analisar um algoritmo permite avaliar seu custo, o que pode ser feito tanto
em termos de tempo quanto de espaço. A análise de tempo se refere a avaliar
como o tempo de execução de um algoritmo específico se comporta em função,
por exemplo, da quantidade de dados que devem ser processados. Por sua vez,
a análise de espaço dá suporte à avaliação da quantidade de memória que é
necessária para que o algoritmo seja executado.
Thomas Fuller já disse, entretanto, que “nada é bom ou ruim, exceto por
comparação”. Para algoritmos, se seu tempo é considerado pesado (ou seja,
ruim), mas este for o único algoritmo disponível para resolver o problema, então
não se pode estabelecer um critério de comparação, de forma que termos como
“melhor” ou “pior” deixam de fazer sentido. Avaliar o tempo de execução de algo-
ritmos serve para permitir que, dados dois ou mais algoritmos que resolvam um
mesmo problema, determinar qual tem comportamento melhor ou pior, ou seja,
qual tende a consumir mais ou menos tempo, dependendo da carga de dados.
Porém, se a maioria das pesquisas for por números que não estejam pre-
sentes no vetor, então a tendência é que todos os valores tenham que ser con-
sultados até que se determine a ausência do item procurado. Esta é a situação de
pior caso, na qual todas as possíveis comparações têm que ser feitas, gerando
o maior custo computacional.
Há, ainda, situações em que existem outras tendências nos dados e, para
elas, devem ser feitas considerações de cunho estatístico.
Para cada análise feita, será definido o que é o tamanho do problema para
o algoritmo. Por convenção, essa característica será indicada pela variável fictí-
cia n. É importante notar que n não é uma variável do algoritmo, mas a indica-
ção do tamanho do problema. Assim, para algoritmos de pesquisa em vetores, n
é o número de elementos presentes no vetor; no caso do cálculo do fatorial, n é
o valor para o qual se calculará o fatorial; em pesquisa de dados em arquivos, n
está relacionado ao número de registros armazenados; em pesquisas em árvores
AVL, n se refere ao número de nós que a árvore possui; em outros problemas, n
pode ser até a distância entre dois valores. Esses são apenas alguns exemplos,
já que tudo depende do algoritmo em questão.
* A notação logarítmica usada neste texto sempre assume base 2. Assim, ao se es-
crever log n, assume-se por padrão log2 n. Quando necessário, outras bases ficarão
18 explícitas.
significado nenhum porque O() não é uma função, mas uma notação específica.
Assim, escreve-se apenas O(n2).
Supondo que T(n) seja dado por T(n) = 50n + 20, é possível mostrar que
T(n) pertence a O(n). Para isso, basta mostrar que existem valores para c e n0 que
façam com que T(n) ≤ cn, para valores n ≥ n0. Assim, a condição 50n + 20 ≤ cn
tem que ser válida para n ≥ n0. Se forem escolhidos c igual a 51 e n0 igual a
20, então a condição se torna verdadeira, o que mostra que a notação pode
ser usada corretamente. Isso significa que o tempo de execução T(n) nunca se
comporta pior que uma reta para valores grandes do tamanho do problema. A
Figura 1 mostra o comportamento das duas funções e como T(n) está abaixo de
51n à medida que n aumenta.
6000
50n + 20
5000
51n
4000
3000
2000
1000
0
0
10
20
30
40
50
60
70
80
90
0
10
Figura 1 Gráfico mostrando que 51n (linha pontilhada) é um limitante superior para
50n + 20 (linha sólida).
120000
10n² + 3n + 4
100000
11n²
80000
60000
40000
20000
0
10
20
30
40
50
60
70
80
90
0
10
Figura 2 Gráfico mostrando que 11n2 (linha pontilhada) é um limitante superior para
10n2 + 3n + 4 (linha sólida).
Por outro lado, seria possível mostrar que T(n) = 10n2 + 3n + 4 é O(n)?
Para isso, é preciso mostrar que 10n2 + 3n + 4 ≤ cn, para n ≥ n0. Não existem,
porém, valores para c e n0 que permitam esta demonstração. Assim, a afirma-
ção que T(n) pertence a O(n) é falsa.
Para finalizar, deve-se notar que dadas as funções T1(n) = n3 + 6n2 +10n + 5
e T2(n) = 5n3 + n2 + 8n + 4, é possível demonstrar que T1(n) é O(T2(n)), assim
como T2(n) é O(T1(n)). Na verdade, ambas são O(n3), o que leva à conclusão
que a notação mais simples é a mais adequada para dizer que ambas as fun-
ções possuem comportamento de crescimento segundo uma função cúbica.
Assim, na escolha de uma função f(n) para a notação O(f(n)), f(n) deve ser dada
na forma mais simples, isto é, sem constantes multiplicativas ou termos de n
menos significativos.
20
Tabela 1 Funções tradicionalmente escolhidas para indicar a complexidade de algorit-
mos, dadas em ordem crescente de complexidade.
1 1
2 log n
3 n
4 nlog n
5 n2
6 n3
7 2n
2500
1
2000 log n
n
n log n
1500
n²
n³
1000 2^n
500
0
1 2 3 4 5 6 7 8 9 10 11
22 * É importante lembrar que a notação log sempre utiliza a base 2 neste texto.
de execução for O(n) e o maior tamanho de problema que puder ser resolvido
em um dado tempo for K, então em um computador 100 vezes mais rápido será
possível resolver um problema de tamanho 100K. E em um supercomputador
um bilhão de vezes mais rápido resolverá um problema 1.000.000.000 vezes
maior, considerando o mesmo intervalo de tempo. Porém, para algoritmos O(n2),
se o computador for 100 vezes mais rápido, o problema resolvido será apenas
10 vezes maior para um mesmo intervalo de tempo. No supercomputador, será
possível resolver um problema 31.623 vezes maior. Os algoritmos exponenciais,
ou seja, aqueles O(2n), possuem ganhos bem mais modestos. Assim, se em um
dado período de tempo é resolvido um problema de tamanho K pelo computador
atualmente disponível, se for comprado um computador 1000 vezes mais rápi-
do, esse mesmo tempo será suficiente para resolver apenas um problema de
tamanho K + 9,9. Isso quer dizer que, se o algoritmo resolver um problema para,
por exemplo, 500 itens em 1 segundo, então no computador 1000 vezes mais
rápido, 1 segundo resolverá apenas o problema para aproximadamente 510
itens; e o supercomputador usará seu 1 segundo para processar 530 itens.
Tabela 3 Tamanho do problema que seria executado em um período fixo de tempo, caso
a velocidade de processamento fosse aumentada.
Algoritmo 1
1 ca + b
Como estes detalhes acabam não sendo relevantes para a análise dos
algoritmos, muitas simplificações podem ser adotadas. Isso evita o esmero desne-
cessário de considerar que multiplicações são mais custosas que somas, ou que
expressões aritméticas inteiras tendem a ser mais eficientemente tratadas que as
de ponto flutuante.
Portanto, nas análises descritas aqui, cada operação, por mais diferente
que seja, será considerada como consumindo exata uma unidade de tempo.
Isso leva a uma forma simples de contar tempo, fazendo que o comando do
Algoritmo 1 consuma 4 unidades de tempo (ut): duas recuperações de dados,
uma soma e uma atribuição.
* A função mais simples para ser usada na notação O(f(n)) é f(n) = 1, que indica o tempo 25
de execução constante.
Para exemplificar, vamos considerar novamente o problema da soma dos
valores de 1 até um valor inteiro positivo dado pelo usuário. O Algoritmo 3 mos-
tra outra solução para o mesmo problema.
Algoritmo 3
1 { soma de positivos até um dado valor ]
2 algoritmo
3 declare i, valor, soma: inteiro
4
5 leia(valor)
6 soma 0
7 para i 1 até valor faça
8 soma soma + i
9 fim-para
10 escreva(soma)
11 fim-algoritmo
26
A partir de agora, outros exemplos são considerados, mas apenas trechos
de código serão apresentados. Independendo do contexto de um problema em
si, apenas os comandos são analisados.
Algoritmo 4
1 leia(n)
2 para i 1 até n/2 faça
3 escreva(i)
4 fim-para
Algoritmo 5
1 leia(n)
2 para i 1 até n faça
3 para j 1 até n faça
4 escreva(i * j)
5 fim-para
6 fim-para
27
A análise deste trecho exige que as repetições sejam consideradas separa-
damente, iniciando-se com a mais interna. Para a repetição das linhas 3 a 5, con-
siderada independentemente do para mais externo, consome os seguintes tem-
pos: 1 ut para iniciação de j, 3n ut para os n incrementos, 3(n + 1) ut para as n + 1
comparações de término e, finalmente, 4n ut para o comando escreva e seus cál-
culos. O tempo de cada para relativo à variável j tem, portanto, Tj(n) = 10n + 4 ut.
Algoritmo 6
1 leia(n)
2 para i 1 até n faça
3 para j i até n faça
4 escreva(i * j)
5 fim-para
6 fim-para
Algoritmo 7
1 para i 1 até n faça
9 i 1
10 enquanto i < n faça
11 { algum processamento de tempo constante }
12 i <- i + k { k constante }
13 fim-enquanto
Algoritmo 8
1 para i 1 até n faça { O(n²) }
2 para j 1 até n faça
Algoritmo 9
1 leia(n)
2 enquanto n ≥ 1 faça
3 escreva(n)
4 n n/2
5 fim-enquanto
Supondo, para facilitar, que o valor digitado seja uma potência de 2, escrita
2m, o Algoritmo 9 produzirá as saídas 2m, 2m-1, 2m-2, 2m-3, ..., 22, 21 e 20. É fácil
deduzir que a repetição é executada m + 1 vezes. Assim, se k é o número de
repetições, então k = m + 1 e T(n) = 7m + 11.
Ainda considerando que a variável n seja inteira, mas o valor lido não seja
uma potência de 2, o raciocínio da análise se mantém apenas com valores mi-
nimamente diferentes para as constantes obtidas, mas ainda T(n) é na forma
T(n) = alog n + b. Ou seja, repetição continua sendo O(log n).
Supondo agora que o valor constante da divisão (linha 4) não seja 2, mas
3, o raciocínio pode ser repetido considerando-se uma entrada 3m, o que leva
a se obter uma função de tempo na forma T(n) = alog3 n + b. Se o valor de n
fosse dividido por 7, a função seria na forma T(n) = alog7 n + b. Para qualquer
31
que seja a situação, a função do tempo de execução é uma função logarítmica,
apenas com base diferente: T(n) = alogp n + b.
Pelo uso de uma abordagem similar, não apresentada neste texto, é tam-
bém viável concluir que, caso a variável de controle do laço de repetição seja
multiplicada por um valor constante, a repetição terá custo também O(log n),
como é o caso dos comandos do Algoritmo 10. Novamente, o valor usado na
multiplicação não é importante, desde que seja constante.
Algoritmo 10
1 leia(n)
2 i1
3 enquanto i < n faça
4 escreva(i)
5 i i * 2
6 fim-enquanto
Algoritmo 11
1 leia(n)
2 i 1
32 3 enquanto i < n faça
4 para j 1 até n faça
5 escreva(i, j)
6 fim-para
7 i i * 5
8 fim-enquanto
Assim, tomando-se como exemplo T1(n) = 1000n + 100 e T2(n) = 2n2 +5n + 1,
sabe-se que T1(n) é O(n) e T2(n) é O(n2). Em princípio, o primeiro é superior ao segun-
do, dado que sua taxa de crescimento é linear, enquanto a do outro é quadrática.
33
800000
T1(n)
700000
T2(n)
600000
500000
400000
300000
200000
100000
0
0
0
0
0
0
0
0
0
0
0
20
60
10
14
18
22
26
30
34
38
42
46
50
54
58
Figura 4 Gráfico com os tempos de processamento de dois algoritmos de ordens de
complexidade diferentes.
Para encerrar, é importante lembrar que a análise fornece uma visão da taxa
de crescimento que o tempo de execução do algoritmo possui. Observar somente
este resultado é uma opção arriscada, pois outros fatores (como o volume de
dados processados) podem interferir no tempo real necessário. Além disso, há
algoritmos O(n) muito melhores ou muito piores que outros algoritmos O(n).
35