Complex I Dade
Complex I Dade
Complex I Dade
Roberto S. Bigonha
Departamento de Ciência da Computação
Universidade Federal de Minas Gerais
Belo Horizonte – MG
2006
ABSTRACT
This paper presents the main and most popular techniques for
algorithm complexity analysis and associated methodologies for
their application to typical case-studies of a course on Algorithms
and Data Structures.
RESUMO
Este artigo apresenta as principais técnicas mais populares de
análise de complexidade de algoritmos e metodologias para suas
aplicações em estudos de casos tı́picos de uma disciplina de Algo-
ritmos e Estruturas de Dados.
1
1 Análise de Complexidade
Il semble que la perfection soit atteinte
non quand il n’y a plus rien à ajouter,
mais quand il n’y a plus rien à retrancher.1
Antoine de Saint-Exupéry (1900-1944)
2
temáticos apropriados, determinados a partir da estrutura dos al-
goritmos, sem necessidade de sua implementação nem de execuções
experimentais.
3
execução surge com mais frequência no estudo de algoritmos e de
suas aplicações, ela recebe aqui mais atenção.
Define-se, assim, que Função de Custo ou Função de Com-
plexidade f (n) é a medida do tempo necessário para executar
um algoritmo para um problema de tamanho n. Note que o default
é o custo de tempo. Quando tratar-se de consumo de memória,
deve-se explicitá-lo claramente.
O terceiro passo consiste na associação de custo a cada instrução
do algoritmo e então contabilizar o custo de todas as operações que
são usadas na solução de problemas.
3 Cálculo do Custo
4
A função pesquise retorna um inteiro que, se positivo, é o
ı́ndice da chave encontrada, senão retorna −1 para indicar que
a chave não está na tabela. A pesquisa sequencial inicia-se na
posição n da tabela, e, a seguir, testam-se os elementos nas posições
inferiores até que o elemento procurado seja encontrado ou que
toda a tabela tenha sido pesquisada. Assim, do ponto de vista de
custo, há pelo menos três casos a considerar nessa análise:
5
custo, i.e., c7 = c8, o que permite dizer que, nesse algoritmo, o
custo total de ambas é c7, conforme anotado.
O custo do algoritmo deve ser calculado iniciando-se pelo corpo
do comando de repetição mais interno, que na Fig. 1 é a linha (5).
Assim, no caso de a chave não estar na tabela, essa linha, de custo
c5, é executada n vezes, onde n é o número de entradas válidas na
tabela, e a linha (4) é executada n + 1 vezes. Esse é o pior caso.
No melhor caso, linha (5) nunca será executada, sendo 0 seu
custo, e a linha (4) é executada uma única vez. As demais operações
de pesquise são executadas uma única vez, observada a ressalva
acima em relação às linhas (7) e (8).
Em resumo, no melhor caso, o custo de execução do algoritmo
pesquise é:
f (n) = c2 + c3 + c4 + c6 + c7 (1)
que pode ser re-escrita como
f (n) = K (2)
onde K = c2 + c3 + c4 + c6 + c7 é uma constante dependente da
plataforma em que o algoritmo for executado.
E no pior caso, a condição do comando while da linha (4) tem
custo (n + 1)c4, e o do comando de repetição é (n + 1)c4 + nc5, e
o custo do algoritmo completo, nesse caso, é
f (n) = c2 + c3 + (n + 1)c4 + nc5 + c6 + c7 (3)
que reduz-se a
f (n) = K1n + K2 (4)
onde K1 = c4 + c5 e K2 = c2 + c3 + c4 + c6 + c7
Os valores exatos de K, K1 e K2 dependem da plataforma
em que uma implementação do algoritmo pesquise for execu-
tada, portanto, aparentemente, contraria a premissa de que o custo
6
do algoritmo deve ser avaliado independentemente da sua imple-
mentação. Entretanto, a influência de K da Eq. (2) e de K1 e
K2 da Eq. (4) no custo final torna-se insignificante para valores
elevados de n.
Por outro lado, como o custo de cada instrução não é mesmo
conhecido, e a influência das constantes computadas pode ser in-
significante para grandes n, pode-se simplicar o cálculo do custo,
ignorando as operações em que o número de vezes em que são exe-
cutadas não depende do tamanho do problema. Para isso, basta
contabilizar as operações que sejam efetivamente relevantes para
determinação do custo final, como exemplificado a seguir.
Instruções Executadas
O que realmente interessa, para fins de comparação de algoritmos, é
exatamente a determinação do comportamento do custo em relação
ao tamanho da entrada. O relevante para o custo é o número de
vezes que cada instrução é executada em função do tamanho da en-
trada. Nesse contexto, a análise de custos fica bastante simplicada,
como mostram os cálculos anotados na Fig. 2.
Algoritmo Custos
1 int pesquise(int ch, int[] a, int n){ melhor pior
2 int i = n; 1 1
3 a[0] = ch; 1 1
4 while (!(a[i] == ch)) 1 n+1
5 i--; 0 n
6 if (i != 0) 1 1
7 return i; 1 1
8 else return -1;
9 }
7
O custo, nesse caso, é o número total de vezes que as instruções
são executadas. Note que, no melhor caso, a instrução (4) somente
é executada uma vez, e a instrução (5), nenhuma vez. Entretanto,
no pior caso, as frequências de execução dessas instruções são n + 1
e n, respectivamente.
Assim, contabilizando a frequência de execução de todas as ins-
truções de pesquise, chega-se rapidamente às funções finais de
custo, que são:
f (n) = 5 melhor caso
f (n) = 2n + 5 pior caso
Custo Médio
Mesmo com o método simplificado de análise de custos, o custo
médio do pesquise pode ser ainda complexo, pois seu cálculo
depende de outros fatores, como a distribuição probabilı́stica das
chaves procuradas.
Para ilustrar esse processo, suponha que toda pesquisa com o
algoritmo da Fig. 3 recupere um registro e que não exista pesquisa
sem sucesso.
Suponha também que pi seja a probabilidade de o i-ésimo re-
gistro, para i no intervalo [1, n], ser procurado, e considerando que
para recuperar o i-ésimo registro são necessárias n − i + 1 com-
parações, então:
1 · pn + 2 · pn−1 + 3 · pn−2 + · · · + n · p1
f (n) = Pn (5)
i=1 pi
9
Nesse caso, obtém-se o custo médio f (n):
1 1 n(n + 1) n+1
f (n) = (1 + 2 + 3 + · · · + n) = ( )= (6)
n n 2 2
Ou seja, a análise do caso esperado, nesse exemplo, com a su-
posição arbitrária de que todos os registros têm a mesma pro-
babilidade de ser acessado, revela que uma pesquisa com sucesso
examina, em média, aproximadamente metade dos registros. En-
tretanto, isso é somente válido no caso de as probabilidades de
acesso aos elementos serem iguais. Em um caso real, pode não ser
possı́vel garantir isso.
10
O processo de determinação do custo de cada instrução que
contém operação destacada como relevante para o custo obedece
as seguintes regras:
• As operações ditas não relevantes têm custo 0 e podem ser
ignoradas no cálculo do custo.
• O custo de um comando de atribuição, de leitura ou de escrita
que contém operação relevante normalmente pode ser conside-
rado 1, exceto quando nele houver chamadas de funções, pois,
nesse caso, seus custos de execução devem ser considerados.
• Se o comando de atribuição dito relevante envolver vetores de
tamanho arbitrariamente grande, deve-se considerar os custos
do processamento dos vetores.
• O tempo de execução de uma expressão relevante é normal-
mente 1, considerando a aplicação dos operadores envolvidos
mais os tempos de avaliação de operandos que sejam cons-
tantes ou variáveis. Entretanto, caso operandos contenham
chamadas de funções, seus custos devem ser considerados.
• O custo de execução de uma sequência de comandos é a soma
dos custos dos comandos relevantes.
• O custo de execução de um comando de decisão é composto
pelo custo de execução dos comandos relevantes executados
dentro do comando condicional, mais o tempo para avaliar a
condição, se ela contiver operação relevante para o custo.
• O custo de um comando de repetição é o somatório do custo
de execução de cada iteração conforme o número de repetições
definido, acrescido do custo do teste de término do laço. O
custo de uma iteração é o custo da condição acrescida do custo
do corpo do comando.
11
• O custo de uma chamada de procedimento é o custo desse
procedimento para um problema cujo tamanho é o efinido por
seus parâmetros implı́citos ou explı́citos.
12
n
X n[n + 1]
Dado que i=
i=1
2
tem-se que
n−2
X n−2
X n−2
X
Pn−1
i=2 i= (k + 1) = k+ 1 (7)
k=1 k=1 k=1
ou
Pn−1 (n − 2)[(n − 2) + 1]
i=2 i= + (n − 2) (8)
2
Portanto,
Pn−1 n2 n
i=2 i = − −1 (9)
2 2
Assim, o custo no pior caso do algoritmo de ordenação de n
inteiros pelo método da inserção é:
n2 n
f (n) = − −1 (10)
2 2
O melhor caso desse algoritmo ocorre quando o arranjo dado já
estiver ordenado, pois a comparação x < a[j] somente será feita
uma vez para cada valor de i, produzindo assim o custo:
n−1
X
f (n) = 1=n−2 (11)
i=2
13
Algoritmo ——– pior caso ——–
1 void ordena(int[ ] a, int n){ - -
2 for (i = 1; i <= n-1; i++) { -
3 min = i; -
for (j = i+1; j <= n; j++) {
P
4
n−1
n−i i=1 (n−i)
5 if (A[j] < A[min]) min = j;
}
6
7 x=A[min]; A[min]=A[i]; A[i]=x; -
8 } -
9 } - -
14
Algoritmo ——– pior caso ——–
1 void ordena(int[ ] a, int n){ - -
2 for (i = 1; i <= n-1; i++) { -
3 min = i; -
4 for (j = i+1; j <= n; j++) { -
P
n−1
5 if (A[j] < A[min]) min = j; - i=1 1
}
6
7 x=A[min]; A[min]=A[i]; A[i]=x; 1
8 } -
9 } - -
15
de 2. Isso facilita os cálculos e não representa perda alguma de
generalidade, pois o que interessa é que n seja um valor grande.
A função de custo do algoritmo é f (n) = k, onde k é o número de
vezes que operação de comparação cmp = a[i].compareTo(ch); da
linha (6) é executada no pior caso, que é quando o valor procurado
não se encontra no arranjo.
A atividade central do algoritmo é o comando de repetição
do-while, no qual, a cada iteração, o tamanho da parte da ta-
bela a pesquisar é dividido ao meio.
Assim, inicialmente, quando a primeira comparação é realizada,
o tamanho do arranjo a ser pesquisado é n. Na segunda iteração,
esse tamanho torna-se n/2, e assim sucessivamente, até o elemento
procurado ser encontrado ou o tamanho for reduzido a 1, como
ilustra o quadro da Fig. 8.
#teste Tamanho da Tabela
1 n
2 n/2
3 n/4
4 n/8
··· ···
k−1
k n/2 =1
16
5 Análise de Algoritmos Recursivos
17
com um pequeno grupo de sete esquemas, cada qual gerando um
tipo de equações de recorrência.
Nos esquemas apresentados a seguir, void p(n) representa a
declaração do cabeçalho de um procedimento de nome p que re-
cebe uma lista de parâmetros que caracterizam um problema de
tamanho n, os quais não são explicitados, pois seu conhecimento é
irrelevante para a análise de custo.
Em todos os esquemas apresentados a seguir, a operação re-
levante, determinante do custo, é processa um elemento, que
tem custo 1, ou então processo x elementos, de custo x.
1 void p(n) {
2 if (n <= 0) termina
3 else {processa um elemento; p(n-1);}
4 }
18
Para resolver esse sistema de equações, pode-se usar a própria
recorrência
f (n) = 1 + f (n − 1) (14)
para calcular o termo f (n − 1) do lado direito, produzindo
f (n − 1) = 1 + f (n − 2)
E novamente usar Eq. (14) para calcular
f (n − 2) = 1 + f (n − 3)
E assim, concluir que se assim fosse feito sucessivamente, para um
dado n, chegar-se-ia a
f (1) = 1 + f (0)
Agrupando essas equações, tem-se:
f (n) = 1 + f (n − 1)
f (n − 1) = 1 + f (n − 2)
n equações
··· = ···
f (1) = 1 + f (0)
Portanto, f (n) = n
19
Algoritmos Recursivos Tipo II
A segunda classe de procedimentos recursivos é formada por aque-
les que, para um problema de tamanho 0, terminam imediata-
mente, e com problemas de tamanho n > 0, processam n elementos
e chamam-se recursivamente com parâmetros de tamanho n − 1,
conforme mostra Fig. 10.
1 void p(n) {
2 if (n <= 0) termina
3 else {processa n elementos; P(n - 1);}
4 }
20
Para obter a função de custo desejada, basta computar o so-
matório presente na definição de f (n), isto é:
Xk
f (n) = (n − i) + f (0) e n − k − 1 = 0
i=0
k
X k
X
f (n) = n+ i+0
i=0 i=0
k(k + 1)
f (n) = (k + 1)n + (veja Seção 10)
2
n+n−k
f (n) = ( )(k + 1) + 0, onde k = n − 1
2
n+n−n+1
f (n) = ( )(n − 1 + 1)
2
n2 + n
Assim, tem-se a expressão final de custo: f (n) =
2
1 void p(n) {
2 if (n <= 0) termina
3 else {processa um elemento; p(n/2);}
4 }
21
Esses algoritmos têm natureza logarı́tmica, pois dividem o espaço
de trabalho em duas metades e processam recursivamente um ele-
mento em cada metade até que o espaço tenha tamanho 1.
Para o cálculo de sua complexidade, supõe-se que n seja um in-
teiro potência de 2, que p(n) tenha custo f (n), e, assim, o compor-
tamento desse algoritmo é descrito pelas equações de recorrência:
f (n) = 1 + f (n/2), para n > 1
f (1) = 1
f (n) = 0, para n ≤ 0
2
f
(n/2)
= 1+ f(n/2
)
f 2 )
(n/2 = 1+ f 3 )
(n/2
f 3 )
(n/2 = 1+ f 4 )
(n/2
··· = ···
k−1
) = 1 + f (n/2k )
f
(n/2
Xk
f (n) = 1 + f (n/2k )
|{z}
i=1 1
f (n) = k + f (1), onde k = lg n e f (1) = 1
Assim, obtém-se a expressão final de custo: f (n) = lg n + 1
1 void p(n) {
2 if (n <= 0) termina
3 else {processa n elementos; P(n/2);}
4 }
23
A solução desse sistema é obtida via uma conveniente mudança de
variável: no lugar de f (n) = n + f (n/2), usa-se f (2k ) = 2k +
f (2k−1), trocando-se n por 2k .
A solução da nova equação de recorrência fica assim:
f (2k ) = 2k + f(2k−1 )
··· = ···
f(21) = 21 + f (20)
X k
f (2k ) = 2i + f (1)
i=1
f (2k ) = 2k+1 − 2 + 1
f (2k ) = 2.2k − 1
Portanto, f (n) = 2n − 1
1 void p(n) {
2 if (n <= 0) termina;
3 else {processa 1 elemento; P(n/2); P(n/2);}
4 }
24
Supondo que p(n) tenha custo f (n), o comportamento desse
algoritmo é descrito pelas equações:
f (n) = 1 + 2f (n/2), para n > 1
f (1) = 1
f (n) = 0, para n ≤ 0
Assim,
k−1
X
f (n) = 2i + 2 k
i=0
f (n) = 2 − 1 + 2k , onde 2k = n
k
f (n) = n − 1 + n
Portanto, f (n) = 2n − 1
25
n > 0, processam n elementos e depois chamam-se recursivamente
duas vezes com parâmetros de tamanho n/2, conforme Fig. 14.
1 void p(n) {
2 if (n <= 0) termina
3 else {processa n elementos; P(n/2); P(n/2);}
4 }
1 void p(n) {
2 if (n <= 0) termina;
3 else {processa 1 elemento; P(n-1); P(n-1);}
4 }
27
Conclusão
Os algoritmos recursivos mais comuns têm um dos sete formatos
apresentados ou alguma variação deles. Evidentemente, outras
estruturas são perfeitamente possı́veis, e a metodologia utilizada
deve ser adaptada para tratar esses casos não previstos.
6 Complexidade Assintótica
28
Quando trabalha-se com problemas de pequeno porte, as de-
cisões de conduta de análise listadas acima podem invalidar to-
talmente o custo apurado. Entretanto, quando o tamanho do pro-
blema assume altas proporções, os comportamentos dos algoritmos
tendem a se diferenciarem, separando-se em classes de complexi-
dade.
O comportamento limite da complexidade quando a entrada
cresce é chamado de complexidade assintótica de tempo ou de
espaço, ou simplesmente, complexidade assintótica, a qual define
o limite de tamanho dos problemas que um dado algoritmo pode
resolver.
Ressalta-se que independentemente do aumento da velocidade
de cálculo dos computadores, o que realmente determina o limite
do tamanho dos problemas que podem ser resolvidos com um dado
algoritmo é a sua complexidade.
Para facilitar o tratamento das aproximações listadas e permitir
a eliminação de detalhes que são irrelevantes para a análise do custo
de algoritmos, usa-se a notação O(g(n)) (leia-se ordem de g(n) ou
ordem de no máximo g(n)), a qual denota uma quantidade que
não é explicitamente conhecida, mas que define com clareza seu
comportamento.
Isto é, a notação O(g(n)) propõe substituir a real expressão da
função de custo, que frequentemente não é conhecida, por uma
regra de comportamento bem definida e mais simples, que permite
fazer comparações dos custos de algoritmos.
Diz-se que um algoritmo tem custo O(g(n)), mas deve-se estar
ciente de que as técnicas de análise de custo de algoritmos apre-
sentadas permitem calcular a função g(n), a qual serve para inferir
o comportamento da real e desconhecida função de custo f (n), e
nunca, sua exata expressão.
29
Dominação Assintótica
Sejam f (n) a real função de custo desconhecida de um algoritmo,
e g(n), sua função de custo computada, considerando n o tamanho
do problema.
30
No gráfico da Fig. 16, a curva em vermelho seria a função de
custo real e desconhecida, mostrando que g(n) é um limite as-
sintótico superior para f (n).
g(n) = 20n2
no qual, para todo n > 1, tem-se que |10n2| ≤ |20n2|, portanto,
g(n) domina assintoticamente f (n), e que |20n2| ≤ 2|10n2|, signi-
ficando que f (n) também domina g(n).
31
Se as funções de custo f e g dominam assintoticamente uma a
outra, então os algoritmos associados têm custos assintoticamente
equivalentes. Nesses casos, o comportamento assintótico não serve
para compará-los.
Obtenção da Ordem
A vantagem do uso de O(f (n)) é que ordem de crescimento per-
mite uma caracterização mais simples da função de custo. A sim-
plificação, sem perda de precisão, da expressão de custo O(f (n))
é amparada pelas seguintes regras:
• se f (n) for uma soma de vários fatores, pode-se descartar todos
exceto o que tiver maior taxa de crescimento.
• se o único termo resultante da simplificação acima contiver
coeficientes constantes, pode-se trocá-los pela constante 1.
Para exemplificar a aplicação dessas regras, considere a expressão
O(5n4 − 10n3 + 4n2 + n + 1000), na qual claramente o termo 5n4 é
o que tem a maior taxa de crescimento. Assim, pelas regras acima,
essa expressão reduz-se a O(n4).
Por essas mesmas regras, o algoritmo de ordenação pelo método
de inserção, cujo custo no pior caso é f (n) = n2/2 − n/2 − 1,
tem uma ordem de crescimento de sua complexidade assintótica
definida por O(n2).
Note que seria um abuso dizer que o tempo de execução do
algoritmo de ordenação por inserção é O(n2), haja vista que tudo
que se sabe é que o tempo real de execução é apenas limitado
superiormente por cn2, para algum c. O que se quer dizer quando
se fala que o tempo de execução é O(n2) é que no pior caso o
tempo de execução é proporcional a n2, para n suficientemente
grande.
32
Ressalta-se, contudo, que para n pequeno, outros fatores têm
maior influência na comparação de desempenho de algoritmos, nes-
ses casos, outros modelos de apuração de custo devem ser usados.
Expressões de Complexidade
No processo de análise da complexidade de um algoritmo, custos
de trechos do algoritmo em análise devem ser combinados para
compor o seu custo final. Nesse processo, expressões com a notação
O devem ser combinadas e avaliadas, segundo as regras próprias,
que estão listadas abaixo, onde, c é uma constante, n denota o
tamanho do problema, f (n), uma função de custo, h(n), outra
função de custo, e t(n, i), uma função intermediária de custo que,
além de n, depende do valor de uma variável i do algoritmo.
As regras básicas de operação com a notação O são:
33
• f (n) = O(g(n)) + O(g(n)), então f (n) = O(g(n)):
a combinação de dois trechos de algoritmo de custos individu-
ais O(g(n)) tem o custo combinado O(g(n)) + O(g(n)), que é
o mesmo que O(g(n)).
• f (n) = c.O(g(n)), então f (n) = O(g(n)):
a execução c vezes de um trecho de algoritmo de custo O(g(n))
tem custo final c.O(cg(n)), que reduz-se à O(g(n)).
• f (n) = bi=a(O(t(n, i)), então f (n) = O( bi=a t(n, i)):
P P
34
1 void ordena(int[ ] a, int n){
2 for (i = 1; i <= n-1; i++) {
3 min = i;
4 for (j = i+1; j <= n; j++) {
5 if (A[j] < A[min])
6 min = j;
7 }
8 x=A[min]; A[min]=A[i]; A[i]=x;
9 }
10 }
35
Como o número de iterações da repetição é n − i, o tempo
total gasto nesse comando interno de repetição é (n − i) × 1 + 1,
considerando o teste final de saı́da. Portanto, o custo do comando
for interno acima é 1+(n−i), que pode ser simplicado para (n−i),
pois O(1 + (n − i)) é mesmo que O(n − i), e (n − i) ∈ O(n − i).
Portanto, n2 n
f (n) = O( − )
2 2
f (n) = O(n2)
36
7 Ordens de Complexidade
37
Complexidade Logarı́tmica: f (n) = O(lg n)
Um algoritmo de complexidade O(lg n) é dito ter complexidade
logarı́tmica, sendo seu tempo de execução f (n) ≤ K lg n, onde
K é uma constante de tempo definido para cada processador.
Esse tempo de execução ocorre em algoritmos que resolvem um
problema pela sua divisão iterativa em duas partes e buscando a
solução em uma das partes.
Esses algoritmos são muito eficientes, pois é pequena a influência
do tamanho da entrada no custo final. Observe o efeito no custo
do crescimento do tamanho n:
• quando n é 1.000, lg n ≈ 10
• quando n é 32.000, lg n ≈ 16
• quando n é um milhão, lg n ≈ 20
38
tempo de execução f (n) ≤ Kn lg n, onde K é a constante do pro-
cessador. Observe que quando n é um milhão, n lg n é cerca de 20
milhões. e quando n é dois milhões, n lg n é cerca de 42 milhões,
apenas um pouco mais do que o dobro.
39
o termo de maior crescimento adnd sempre prevalece sobre os de-
mais, no lugar de O(p(n)) escreve-se apenas O(nd), para d ≥ 0.
Essa classe de complexidade inclui as ordens de complexidade para
polinômios de grau zero [O(1)], grau um [O(n)], grau dois [O(n2)]
e grau três [O(n3)].
Os algoritmos polinomiais geralmente resultam de um entendi-
mento mais profundo da estrutura do problema, que evita o uso de
força bruta na busca de sua solução. De um ponto de vista teórico,
um problema é considerado intratável se para ele não existe um
algoritmo polinomial capaz de resolvê-lo.
Dito de outra forma, um problema é considerado bem resolvido
quando existe um algoritmo polinomial para resolvê-lo. Ou então
que um algoritmo só pode se considerado eficiente se ele executa
em tempo polinomial.
41
t = KA.nA
t = KB .nB
Portanto,
KA.nA = KB .nB
KA.nA = K16A .nB
Donde, tem-se que nB = 16nA
t = KA.n2A
t = KB .n2B
42
Portanto,
KA.n2A = KB .n2B
KA.n2A = K16A .n2B
n2B = 16.n2A
Donde, tem-se que nB = 4nA
t = KA.2nA
t = KB .2nB
Portanto,
KA.2nA = KB .2nB
KA.2nA = K16A .2nB
2n B = 16.2nA
2n B = 24+nA
Donde tem-se que nB = nA + 4
43
Resumo
Tabela 21 resume a influência do velocidade processador no tama-
nho do problema que um dado algoritmo pode resolver, quando
fixado o tempo máximo de execução. Claramente, vê-se a comple-
xidade é o fator determinante do tamanho máximo do problema
que se pode resolver.
Tamanho máximo Tamanho máximo
Complexidade para uma dada para velocidade
Algoritmo de Tempo Velocidade 16 vezes maior
A1 lg n N N 16
A2 n N 16N
A3 n lg n N ≈ 16N
A4 n2 N 4N
3
A5 n N 2, 51N
A6 2n N N +4
Notação Θ
Diz-se que f (n) = Θ(g(n)), que se lê f (n) é da ordem exata de
g(n), se existirem constantes positivas c1, c2 e n0 tais que, para
n ≥ n0, o valor de f (n) estiver sempre entre c1g(n) e c2g(n).
Se f (n) = Θ(g(n)), a função g(n) é um limite assintótico firme
ou restrito (asymptotically tight bound) para f (n), pois, g(n)
limita f (n) acima e abaixo.
44
Assim, f (n) = Θ(g(n)) significa que f (n) cresce assintotica-
mente tão rapidamente quanto g(n), como ilustra a Fig. 22.
Enquanto a notação-O trata do limite superior de funções de
custo no pior caso, a notação-Θ está ligada aos limites inferior e
superior do custo médio.
Notação o
A notação o é definida como:
Definição 5 f (n) = o(g(n)), ∀ c > 0 e n0 ≥ 0, tais que
0 ≤ f (n) < cg(n), ∀ n ≥ n0.
Intuitivamente, pela Definição 5, a função f (n) tem um cresci-
mento muito menor que g(n) quando n tende para infinito. Isto
pode ser expresso da seguinte forma:
f (n)
lim =0
n→∞ g(n)
Notação Ω
Diz-se que f (n) = Ω(g(n)), se existirem constantes positivas c e n0
tais que para n ≥ n0, o valor de f (n) é maior ou igual a cg(n). Por-
tanto, g(n) é um limite assintótico inferior (asymptotically lower
bound) para f (n), como sugere a Fig. 23. Formalmente, tem-se:
Definição 6 f (n) = Ω(g(n)), se ∃ constantes c > 0 e n0 > 0,
tais que 0 ≤ cg(n) ≤ f (n), ∀ n ≥ n0.
Observe que quando a notação Ω é usada para expressar o tempo
de execução de um algoritmo no melhor caso, define-se também o
limite inferior do tempo de execução para todas as entradas.
Notação ω
Por analogia, a notação ω está relacionada com a notação Ω da
mesma forma que a notação o está relacionada com a notação O.
Formalmente, a notação ω é definida da seguinte forma:
Definição 7 f (n) = ω(g(n)), se ∀ constante c > 0,
∃ n0 > 0 tal que 0 ≤ cg(n) < f (n), ∀ n ≥ n0
Embora as definições das notações Ω e ω sejam similares, há uma
diferença importante que é que, em f (n) = ω(g(n)), a expressão
0 ≤ cg(n) < f (n) é válida para todas as constantes c > 0.
A relação f (n) = ω(g(n)) implica em
f (n)
lim = ∞,
n→∞ g(n)
se o limite existir.
Relação Entre O, o, Ω, Θ, ω
Com base nas definições apresentadas, pode-se dizer que:
• f (n) = O(g(n)) significa que f (n) cresce assintoticamente
mais lentamente que c.g(n), para alguma constante c
• f (n) = o(g(n)) significa que f (n) cresce assintoticamente mais
lentamente que c.g(n), para toda constante c
• f (n) = Θ(g(n)) significa que f (n) cresce assintoticamente tão
rapidamente quanto g(n)
47
• f (n) = Ω(g(n)) significa que f (n) cresce assintoticamente
mais rapidamente que c.g(n), para alguma constante c
• f (n) = ω(g(n)) significa que f (n) cresce assintoticamente
mais rapidamente que c.g(n), para toda constante c
Os comportamentos descritos pelas notações Θ, Ω e O(n) estão
relacionados conforme o Teorema 1.
Teorema 1 f (n) = Θ(g(n)) se e somente se, para duas funções
quaisquer f (n) e g(n), f (n) = O(g(n)) e f (n) = Ω(g(n)).
A aplicação das notações de complexidade nas expressões de
custo dos algoritmos de ordenação analisados permite afirmar que:
• para o algoritmo de ordenação por inserção, o melhor caso tem
custo Ω(n), o pior caso O(n2), e o custo do caso médio é de
difı́cil cálculo, e, com certeza, não é Θ(n2).
• para o algoritmo de ordenação por seleção, o melhor caso tem
custo Ω(n2), o pior caso, O(n2), e o caso médio, Θ(n2).
Reflexividade
1. f (n) = Θ(f (n))
2. f (n) = O(f (n))
3. f (n) = Ω(f (n))
Simetria
1. f (n) = Θ(g(n)) se e somente se g(n) = Θ(f (n))
Simetria de Transposição
1. f (n) = O(g(n)) se e somente se g(n) = Ω(f (n))
2. f (n) = o(g(n)) se e somente se g(n) = ω(f (n))
10 Fórmulas Importantes
Logaritmos
• logb(xy) = logb x + logb y
• logb(xy ) = y logb x
• y logc x = xlogc y
• blogb x = x
Produtos
• Produto dos n termos a1, a2, · · · , an de uma Progressão Geomé-
trica de razão q:
√ n+1 ×n
Pn = an1 × ann ou Pn = an+1 1 × q 2
Somatórios
• Soma dos n termos a1, a2, · · · , an de uma Progressão Aritmética:
n
X a1 + an
ai = ×n
i=1
2
50
• Soma dos n termos a1, a2, · · · , an de uma Progressão Geométrica
de razão q:
n
X a1(q n − 1)
ai = (q > 1)
i=1
q − 1
n−1
X n(n − 1)
• (n − i) =
i=1
2
n
X n(n − 1)
• (n − i) =
i=1
2
n
X n(n + 1)
• i=
i=1
2
n
X n(n + 1)(2n + 1)
• i2 =
i=1
6
n
X
k nk+1
• i = + termo de menor ordem
i=1
k+1
n
X
i 1 − xn+1
• ax = a × (x > 1)
i=0
1−x
n
X 1 1
• i
= 2 − n
i=0
2 2
n
X
i an+1 − 1
• a = (a > 1)
i=0
a−1
n
X
• 2i = 2n+1 − 1
i=0
51
n−1
X i n+1
• i
= 2 − n−1
i=0
2 2
n
X i 2n − 1
• = 2 −
i=1
2i 2n−1
n−1
X
• i2i = 2 + (n − 2)2n
i=0
n
X
i x − nxn + (n − 1)xn+1
• ix = (x > 1)
i=0
(1 − x)2
n
X
• lg i = lg n!
i=1
11 Notas Bibliográficas
Referências
53