Escolar Documentos
Profissional Documentos
Cultura Documentos
David Déharbe∗
DIMAp/UFRN
19 de agosto de 2005
Sumário
1 Introdução 1
3 Definições formais 7
3.1 Limite superior: a notação O . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.2 Limite inferior: a notação Ω . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.3 Complexidade exata: a notação Θ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
3.4 Limite superior estrito: a notação o . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1 Introdução
Um usuário mede a qualidade de um programa através de vários critérios:
• A interface do programa deve facilitar o quanto mais possı́vel o seu uso por um usuário, levando em
conta que usuário com diferentes nı́veis de experiência tem comportamentos e exigências diferentes. Por
exemplo, um novo usuário irá descobrir as várias funcionalidades de um programa através de uma interface
amigável (como o mouse ou a voz), enquanto por razões de produtividade, um usuário experiente prefere
ter acesso aos comandos direitamente através do teclado.
• A compatibilidade do programa com outros programas, ou outras versões do mesmo programa.
∗ Copyright
2001,2003
c David Deharbe and Universidade Federal do Rio Grande do Norte. Todos os direitos reservados.
1
2 O que é análise de algoritmos ? 2
Uma outra alternativa de análise de algoritmos é a abordagem teórica, que não necessita que seja realizada
uma implementação. É importante enfatizar que sempre tentamos estabelecer uma aproximação da complexi-
dade, pois ela depende de muitos fatores e é uma tarefa quase impossı́vel fazer uma predição totalmente exata
de quanto tempo vai durar a execução, ou da quantidade exata de memória que será utilizada. A complexidade
de um algoritmo é um indicativo de como implementações do algoritmo irão se comportar quando serão execu-
tados num ambiente computacional. É recomendável complementar uma análise teórico com um levantamento
experimental.
O tempo de execução e o espaço alocado são os dois fatores formando a complexidade computacional,
ou simplesmente complexidade, de um algoritmo. Mais precisamente, um algoritmo tem duas medidas de
complexidade:
1. a complexidade temporal, que é aproximativamente o número de instruções que ele executa, e
2. a complexidade espacial, que é a quantidade de memória que ele utiliza durante sua execução.
Ambas complexidades são funções que tem como parâmetro o tamanho da entrada tratada.
Finalmente, além da complexidade computacional de algoritmos, é também de interesse a complexidade
computacional de problemas. Exemplos de problemas são: busca de um elemento em uma seqüência, busca
de um elemento em uma seqüência ordenada, ordenação de uma seqüência, etc. Problemas também tem uma
complexidade computacional que corresponde a um número mı́nimo de operações que tem que ser feitas para
resolvê-los, independentemente do algoritmo que for empregado.
Algoritmo 2 (máximo)
1 funct máximo(v : array (t); n : N) : t ≡
2 var i : N
3 max : t
4 end
5 begin
6 if n = 0
7 then error(”maximo chamada com vetor vazio”)
8 else
9 max ← v[1]
10 for i ← 2 to n step 1 do
11 if v[i] > max
12 then max ← v[i]
13 fi
14 od
15 fi
16 result ← max .
O caso do número de elementos n ser nulo caracteriza um erro de utilização da função máximo, que não
é definida para vetores nulos. Nos casos normais de operação, a avaliação do tempo de execução pode ser
realizada através de uma análise do tempo de execução, o que requer ter uma ideia do funcionamento interno
dos computadores, e em particular dos microprocessadores:
• O teste da linha 6 que se executa em tempo constante k1 .
• A atribuição de inicialização da linha 9, também executado em tempo constante k2 .
• O laço das linhas 10 até 14 é executado n − 1 vezes, e realiza os seguintes passos:
– Atribui à variável de iteração i o valor 2, ou o valor i + 1. No pior caso, cada atribuição é realizada
num tempo máximo constante k3 (independente de n).
– Realiza o teste da linha 11, em tempo constante k4 .
– Se a condição do teste for verdadeiro, é realizada na linha 12 uma atribuição, que é realizada em
tempo constante k5 .
Consequentemente, o tempo total de execução t satisfaz:
t ≤ k1 + k2 + (n − 1) × (k3 + k4 + k5 )
t ≤ n(k3 + k4 + k5 ) + (k1 + k2 − k3 − k4 − k5 )
t ≤ k × n, onde k é uma certa constante
Neste caso a complexidade temporal é k.n, uma constante multiplicada pelo tamanho do vetor, e digamos que
a complexidade temporal é linear.
Em relação ao espaço ocupado por esta função, observamos que, além dos parâmetros, são usados apenas
duas variáveis. Logo, a complexidade espacial também é linear.
Exercı́cio 1 Definir uma função que, dada uma matriz quadrada, e n o tamanho da matriz. Avaliar a com-
plexidade temporal desta função.
2.1 Dicas para avaliar a complexidade de um algoritmo. 5
Algoritmo 3 (soma)
1 funct soma(l : listNN) : N ≡
2 begin
3 if l = ()
4 then result ← 0
5 else result ← primeiro(l) + soma(resto(l))
6 fi .
Cada chamada recursiva de soma diminui de um comprimento da lista passada em parâmetro, com a exceção
do caso do parâmetro ser uma lista vazia, em qual caso o algoritmo termina. Se n é o comprimento inicial,
então o número total de chamadas será n + 1. A cada chamada serão realizadas as seguintes operações:
• O teste da linha 3 é executado em tempo constante k1 .
• Dependendo do resultado deste testo, são executados
– ou a atribuição da linha 4, que é realizada em tempo constante k2 ,
– ou a atribuição da linha 5, que, desconsiderando o tempo das chamadas recursivas, é calculado em
tempo constante k3 .
Logo, a complexidade temporal t deste algoritmo satisfaz:
t ≤ (n + 1) × k1 + k2 + n × k3
t ≤ n × (k1 + k3 ) + k1 + k2
t ≤ n × k, , onde k é uma constante.
O algoritmo tem uma chamada recursiva que diminui de 1 o tamanho da lista, então haverá n chamadas, e o
algoritmo tem complexidade temporal linear.
Para avaliar a quantidade de memória necessária, é preciso ter um entendimento ainda melhor de como
funcionam os micro-processadores. Cada chamada da função é realizada colocando em uma porção da memória,
chamada a pilha de execução, certas informações, incluindo os parâmetros da função chamada. No caso da
função soma, vimos que podia haver n + 1 chamadas recursivas, cada uma dela tendo como parâmetro uma
lista de tamanho decrescente. A somatória dos comprimentos das listas passadas em parâmetro é:
n
X
n + (n − 1) + (n − 2) + . . . + 1 + 0 = i
i=0
n×n+1
=
2
≤ k × n2
Neste caso, a complexidade espacial é uma função quadrática do tamanho da entrada. Na prática, compiladores
podem otimizar esse tipo de código e gerar programas com complexidade espacial inferior à calculada.
2.2 Comparação de algoritmos 6
Exercı́cio 2 Seja um algoritmo recursivo com duas chamadas recursivas que diminuem em 1 o comprimento
da lista. A cada chamada, é executado um número fixado de operações constantes. Quais são as complexidades
temporal e espacial deste algoritmo?
2.1.3 Considerações
Geralmente, quando se avalia a complexidade de um algoritmo, a análise é feita de forma menos precisa.
As razões desta simplificação são que, primeiro é muito trabalho trabalhar contabilizando cada instrução, e
segundo o custo de executar cada instrução varia grandemente entre diferentes versões do código executável
(fatores sendo as capacidades de otimização do compilador, o tipo de micro-processador utilizado, a freqüência
do relógio cadenceando o micro-processador, e uma variedade de outras caracterı́sticas). Assim, considera-se
que um bloco de execuções que se executam uma única vez é considerado como um custo unitário. O custo
de executar um laço é considerado como sendo o custo de executar o bloco dentro do laço multiplicado pelo
número de vezes que o bloco é executado. Assim, se temos c bloco de iterações aninhadas, e que cada bloco
efetua um número de iterações dependendo de n, a complexidade temporal é aproximativamente nc .
Se temos c bloco de iterações aninhadas, e que cada bloco efetua um número de iterações dependendo de n,
a complexidade temporal é aproximativamente nc .
Em geral, quando temos um problema a resolver, queremos obter uma solução de complexidade menor
possı́vel: é melhor ter um algoritmo de complexidade polinomial, que exponencial.
Um pouco de terminologia
A tabela seguinte recapitula os termos utilizado para qualificar a complexidade de um algoritmo em função de
n, o tamanho de sua entrada.
constante um número constante de operações
logarı́tmico log n
linear n
quadrático n2
cúbico n3
polinomial nk , onde k é constante
exponencial en
3 Definições formais
Quando estuda-se algoritmos, é importante poder estimar a complexidade destes. Se o algoritmo é complicado,
deve se usar uma abordagem formal para provar qual é sua complexidade computacional.
A notação O(F (n)) diz que existe um ponto n0 tal que para todos os tamanhos de entrada n superiores a n0 ,
o custo é inferior a algum múltiplo de F (n), ou seja, F (n) cresce mais rapidamente que C(n).
Por exemplo dizer que o espaço alocado pela execução de um algoritmo é O(n2 ), significa dizer que a
quantidade de memória usada é no máximo uma função quadrática da entrada. É importante observar que, na
notação O, os termos de menor peso e os fatores podem sempre ser eliminados; assim O(4 × n3 + 10 × n2 ) e
O(n3 ) são sinônimos, mas o segundo termo é mais simples e deve ser utilizado.
Quando a complexidade de um algoritmo A é O(f (n)), é comum dizer simplesmente que A é O(f (n)), e
neste caso, entende-se que é a complexidade temporal.
Observa que se uma função é O(n) então ela é também O(n2 ), mas a afirmação O(n) é mais precisa.
Exercı́cio 5 Responder as seguintes perguntas. Justificar.
1. n2 + n + 1 é O(1) ? O(n) ? O(n2 ) ? O(n3 ) ? O(n × log n) ?
2. As complexidades temporal e spacial do algoritmo 3 são O(n) ? são O(n2 ) ?
3. Para o algoritmo 3, a complexidade temporal é O(n) e a espacial é O(n2 ) ?
3.2 Limite inferior: a notação Ω 8
Exercı́cio 6 • Mostrar que se um algoritmo é o(F (n)), então ele é O(F (n)).
• Existe um algoritmo seqüêncial de ordenação que tenha uma complexidade o(n log n) ?
Por exemplo, para a entrada −2, 11, -4, 13, −5, 2 a resposta é 20, correspondendo a subseqüência do segundo
ao quarto elemento. Para a entrada 1, −3, 4, -2, -1, 6 a resposta é 7, correspondendo a subseqüência os quatro
últimos itens.
4.1 Um algoritmo cúbico 9
Algoritmo 4 (subseqüênciaSomaMáxima)
1 funct subseqüênciaSomaMáxima(v : array Z, n : N) : Z ≡
2 var soma, somaMáxima : Z
3 i, j, k : N
4 end
5 begin
6 somaMáxima ← 0
7 for i = 1 to n step 1 do
8 for j = i to n step 1 do
9 soma ← 0
10 for k = i to j step 1 do
11 soma ← soma + v[k]
12 od
13 if soma > somaMáxima then somaMáxima ← soma fi
14 od
15 od
16 result ← somaMáxima
17 end .
O espaço alocado pela execução deste algoritmo é o custo de representação dos parâmetros mais as variáveis
locais. Logo, o custo espaço é então O(1). Para avaliar a complexidade em termo de tempo deste algoritmo,
precisamos avaliar quantas vezes cada instrução é executada. O algoritmo é basicamente composto de 3 laços
aninhados. As instruções as mais frequentemente executadas e, portanto, o termo dominante no tempo de
execução deste algoritmo, são as das linhas 10 a 12. O número de vezes que as expressões compondo este laço
são executadas é
n X
X j
n X
1 = n(n + 1)(n + 2)/6
i=1 j=i k=i
= O(n3 ).
Algoritmo 5 (subseqüênciaSomaMáxima)
1 funct subseqüênciaSomaMáxima(v : array Z, n : N) : Z ≡
4.3 Um algoritmo linear 10
Algoritmo 6 (subseqüênciaSomaMáxima)
1 funct subseqüênciaSomaMáxima(v : array Z, n : N) : Z ≡
2 var somaSufixo, somaMáxima : Z; i, j : N
3 end
4 begin
5 somaMáxima ← 0
6 somaSufixo ← 0
7 for i = 1 to n step 1 do
8 if v[i] + somaSufixo > somaMáxima
9 somaSufixo ← v[i] + somaSufixo
10 somaMáxima ← somaSufixo
11 elsif (v[i] + somaSufixo > 0)
12 somaSufixo ← v[i] + somaSufixo
13 else
14 somaSufixo ← 0
15 fi
16 od
17 result ← somaMáxima
18 end .
4.4 Conseqüências práticas 11
Exercı́cios
√
1. Calcule os valores de log N , N , N log N , N (log N )2 , N 3/2 , N 2 e N 3 para N igual a 10, 100, 1000, 10000,
100000, 1000000.
2. Supondo que a execução de um trecho de programa leva 10−6 segundos, quanto tempo levam 102 , 104 ,
106 , 101 0, 1015 execuções desse trecho?
3. Quais são as diferentes notações apresentadas para definir e comparar a complexidade de algoritmos ?
Dar a definição de cada uma dela.
4. Revisar os principais algoritmos de ordenação que você conhece e determina a complexidade de cada um
utilizando notações formais.
5. Mostrar por que o número de vezes que as expressões do algoritmo 4 é n(n + 1)(n + 2)/6.
6. Suponha C1 (n) = O(F (n)) e C2 (n) = O(F (n)). Qual das relações seguintes é verdadeira ?
(a) C1 (n) + C2 (n) = O(F (n))
(b) C1 (n) − C2 (n) = O(F (n))
(c) C1 (n)/C2 (n) = O(1)
(d) C1 (n) = O(C2 (n))
7. (Adaptado de Sedgewick) Mede o tempo que leva a execução do seguinte programa no seu ambiente de
programação:
int i, j, k, count = 0;
for (i = 0; i < N; i++)
for (j = j; j < N; j++)
for (k = 0; k < N; k++)
count++;
para N igual a 10, 50, 100, 500, 1000. Verifique o impacto das opções de otimização do computador nesse
tempo de execução.
Repete a experiência em uma outra linguagem de programação.
4.4 Conseqüências práticas 12
Leituras recomendadas
• Os capı́tulos 1 e 2 do livro de Robert Sedgewick com o tı́tulo Algorithms in C++, Parts 1–4, Fundamentals,
Data Structures Sorting, Searching; Third Edition.