Você está na página 1de 13

Elementos de complexidade computacional

David Déharbe∗
DIMAp/UFRN
19 de agosto de 2005

Sumário
1 Introdução 1

2 O que é análise de algoritmos ? 2


2.1 Dicas para avaliar a complexidade de um algoritmo. . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.1.1 Algoritmos iterativos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.1.2 Algoritmos recursivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.1.3 Considerações . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.2 Comparação de algoritmos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

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

4 Exemplo: O problema da soma da maior subseqüência 8


4.1 Um algoritmo cúbico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
4.2 Um algoritmo quadrático . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
4.3 Um algoritmo linear . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
4.4 Conseqüências práticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

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

• A velocidade de execução do programa é também um critério extremamente importante, especialmente


em aplicações cientı́ficas onde computações pesadas occorrem.
• A quantidade de memória utilizada pelo programa durante sua execução. Essa quantidade de memória,
assim também como a velocidade de execução, são, em geral, diretamente ligados à quantidade de dados
processados.
Para a empresa que produz um programa, além de satisfazer seus usuários, outros critérios são usados para
medir a qualidade de um programa. Esses critérios têm principalmente a ver com a produtividade da empresa
e são:

• A portabilidade do código entre várias plataformas.


• A clareza, ou lisibilidade, do código é extremamente importante, pois quanto mais acessı́vel, mais fácil
fazer evoluir o código. Para possibilitar a manutenção de código, é crucial prever uma documentação do
programa especı́fica para programadores.
• A reutilisabilidade do código permite que porções de um programa sejam reaproveitadas para desenvolver
outros produtos e permitem ganhos óbvios de produtividade. Existe programadores que se especializam
na produção de bibliotecas de código que são suficientemente genéricas para ser aproveitadas para várias
aplicações. Linguagens modernas vêm com bibliotecas de funções de uso geral (a biblioteca padrão de
stdlib para C, STL para C++, JavaBeans para Java).
Neste capı́tulo introduziremos uma ferramenta formal para medir a o tempo e a memória gastos por algorit-
mos. Apresentaremos um problema que pode ser resolvido com algoritmos de velocidade diferentes. A área da
computação que estude esse problema é chamada de Complexidade de algoritmos e é o objeto de uma disciplina
do seu curso.

2 O que é análise de algoritmos ?


O tempo de execução de um algoritmo e a quantidade de memória que ele utiliza geralmente dependem do
tamanho da entrada que ele deve processar. Assim, esperamos que ordenar 10.000 números leva mais tempo
que ordenar 10 elementos. Na prática, estamos acostumados a observar que um editor de texto leva mais tempo
e consume mais memória quando é aberto com um grande arquivo que com um pequeno, ou que um programa
de edição gráfica utiliza mais memória e leva mais tempo para aplicar um filtro a uma imagem grande que a
uma imagem pequena. Portanto, em geral, a memória alocada e o tempo de execução de um algoritmo são
funções do tamanho da entrada.
Para analisar a complexidade de um algoritmo, existem duas abordagens: a abordagem experimental, e a
abordagem teórica.
A abordagem experimental analisa o desempenho do algoritmo através da medição do seu tempo de execução
sobre diferentes conjuntos de dados de entrada. A abordagem experimental pode ser útil para detectar diferenças
de desempenho que uma análise teórica, que não tende a levar em conta otimizações de codificação, não permite
perceber. O primeiro passo para realizar uma análise experimental é o desenvolvimento de uma implementação
correta e cuidadosa do algoritmo. O segundo passo é definir um conjunto de dados de entrada para realizar a
medição. São três tipos de entradas: entradas reais, entradas malignas, e entradas randômicas. Entradas reais
representam dados com padrões similares aos que o programa deverá tratar efetivamente. Entradas randômicas
são geradas aleatoriamente e apenas devem satisfazer os pré-requisitos do algoritmo. Finalmente, entradas
malignas são dados que correspondem a situações anormais, ou não esperadas.
2.1 Dicas para avaliar a complexidade de um algoritmo. 3

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.

2.1 Dicas para avaliar a complexidade de um algoritmo.


A complexidade tanto espacial quanto temporal de um algoritmo é estimada em função do tamanho da entrada.
Logo é importante estabelecer de que forma o tamanho da entrada influi no comportamento do algoritmo.
Assim, se o algoritmo não é recursivo, não contém iterações e também não utiliza algoritmos que tem essas
caraterı́sticas, então o número de passos é independente do tamanho da entrada e digamos que a complexidade
(temporal) é constante. Por exemplo o algoritmo 1 que acessa o ii-èsimo elemento de um vetor v de tamanho
n não possui iterações, nem é recursivo. O número de testes realizados sempre é um (linha 4), mas o número
de atribuições pode ser um ou zero. Neste caso, o tempo de execução é independente do tamanho dos dados
de entrada, e como o tempo de execução vai variar dentro de uma faixa constante, é considerada constante a
complexidade temporal.
Algoritmo 1 (acesso)
1 N); i, n : N) : N ≡
funct acesso(v : array (N
2
3 begin
4 if n > i
5 then error(”acesso realizado fora dos limites do vetor”)
6 else
7 result ← v[i]
8 fi .

2.1.1 Algoritmos iterativos


Se o algoritmo tem como parâmetro um vetor, então a complexidade do algoritmo será em função de n,
o tamanho do vetor. Por exemplo, suponha que o algoritmo consiste em uma iteração sobre os elementos do
vetor, e que a cada passo da iteração seja executado um número fixo k de instruções. Como exemplo analizamos
o algoritmo 2 que, dado um vetor v e o tamanho n deste vetor retorna o maior elemento deste vetor.
2.1 Dicas para avaliar a complexidade de um algoritmo. 4

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

2.1.2 Algoritmos recursivos


Para avaliar a complexidade de uma função escrita de forma recursiva, é preciso analizar quantas vezes a função
deve-ser chamada para chegar ao resultado esperado, e quantas operações acarretam cada chamada.
Se um algoritmo tem como parâmetro uma lista, então a complexidade do algoritmo será uma função de n,
o comprimento da lista. Por exemplo, consideramos o algoritmo 3 que, dado uma lista de números naturais,
retorna a soma dos elementos da lista.

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 .

2.2 Comparação de algoritmos


O tempo de execução de um programa depende de vários fatores: a velocidade do computador no qual está
rodando, a qualidade do compilador que foi usado e, em alguns casos, a qualidade do algoritmo. Se n é o
tamanho da entrada, as funções comumente encontradas em análise de programas são k × n (linear), n × log n,
k × n2 (quadrática), k × n3 (cúbica), k1 × ek2 ×n (exponencial). Essa ordem é a ordem de preferência: um custo
tempo e/ou espaço de complexidade linear é muito melhor que um custo cúbico. Enquanto programadores,
nosso escopo de trabalho é a qualidade dos algoritmos empregados no programa. Veremos que para um mesmo
problema, existe vários algoritmos possı́veis, de complexidade diferentes. O bom programador deve saber
projetar e/ou escolher o melhor algoritmo disponı́vel.
Para comparar dois algoritmos, deve se estudar a complexidade desses algoritmos. Supondo que já calcu-
lamos as funções dando o custo espaço e tempo desse algoritmos. Para comparar essas funções, podemos por
exemplo desenhar o gráfico delas. No caso geral, várias configurações são possı́veis. Seja F e G as duas funções
de custo que queremos comparar:
• Se F é sempre inferior a G, ou seja, o gráfico de F fica sempre em baixo do gráfico de G, então a escolha
para o algoritmo correspondente a F é óbvia.
• Se F as vezes é inferior a G, e vice-versa, e os gráficos de F e G se intersetam em um número infinito de
pontos. Neste caso, consideramo que há empate, e a função custo não ajuda a escolher um algoritmo.
• Se F as vezes é G inferior a G, e vice-versa, e os gráficos de F e G se intersectam em um número finito de
pontos. Portanto, a partir de um certo valor de n, F é sempre superior a G, ou é sempre inferior. Neste
caso, consideramos melhor aquele algoritmo que é inferior ao outro para grandes valores de n.
Portanto, no caso geral, nem sempre podemos dizer que F (n) < G(n). A abordagem escolhida é de comparar
as taxas de crescimentos destas funções e seu comportamento quando o tamanho da entrada é muito grande.
Quando estudamos o comportamento do gráfico das funções consideradas, observamos que a taxa de crescimento
é o fator mais importante para relacionar essas funções quando n é grande.
Por exemplo, se temos um algoritmo A que tem uma complexidade de CA (n) = 1000 × n2 e um algoritmo
B com uma complexidade de CB (n) = 0, 1 × n3 , consideraremos que B é mais complexo que A. Uma primeira
observação é que a análise de algoritmos assim como a estudamos só é útil para comparar algoritmos que
manipulam grandes quantidades de dados.
3 Definições formais 7

Exercı́cio 3 A partir de qual valor de n, CA torna se inferior a CB ?

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.

Exercı́cio 4 Para um determinado problema P , temos algoritmos a, b, c, e d com as seguintes complexidades


Ca (n) = 100 × n × log n, Cb (n) = 1000 × n, Cc (n) = 4 × n2 e Cd (n) = 10−5 × en . Classificar esses algoritmos
do melhor até o pior, em termo de complexidade.

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.

3.1 Limite superior: a notação O


A notação mais comumente usadas para medir algoritmos é O, que da um limite superior da complexidade:
Definição 1 (Grande O) A função custo C(n) é O(F (n)) se existe constantes positivas c e n0 tais que

C(n) ≤ c.F (n) quando n ≥ n0 .

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

3.2 Limite inferior: a notação Ω


A notação Ω é usada para especificar o limite inferior da complexidade de um algoritmo:
Definição 2 (Grande Omega) C(n) = Ω(F (n)) se existe constantes positivas c e n0 tais que C(n) ≥ c.F (n)
quando n ≥ n0 .
Essa definição diz que, a partir de um certo valor n0 , o custo C(n) é maior que F (n), multiplicado por um certo
fator constante. Assim C(n) cresce mais rapidamente que F (n).
Um teorema importante de algorı́tmica diz que, em um processador seqüêncial, o tempo de execução de
qualquer algoritmo de ordenação é Ω(n log n). Isso quer dizer que não existe algoritmos seqüênciais de ordenação
que tenham uma complexidade temporal inferior a n log n.

3.3 Complexidade exata: a notação Θ


A notação Θ é usada para especificar exatamente a complexidade de um algoritmo:
Definição 3 (Grande Theta) C(n) = Θ(F (n)) quando C(n) = O(F (n)) e C(n) = Ω(F (n)).
Se um algoritmo A é Θ(F (n)), ele é ao mesmo tempo O(F (n)) e Ω(F (n)). Portanto, sua complexidade cresce
tão rapidamente quanto a função F (n). F é uma medição exata da taxa de evolução da complexidade do
algoritmo A. Por exemplo, o algoritmo 2 é Θ(n).
Em muitas ocasiões, a notação O é empregada onde a notação Θ seria adequada. Observa que isso não
caracteriza um erro, mais simplesmente uma imprecisão.

3.4 Limite superior estrito: a notação o


Enfim a notação o é usada para especificar que a complexidade de um algoritmo e inferior estritamente a uma
certa função:
Definição 4 (Pequeno O) C(n) = o(F (n)) quando C(n) = O(F (n)) e C(n) 6= Ω(F (n)).

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) ?

4 Exemplo: O problema da soma da maior subseqüência


Problema 1 Seja uma seqüência finita de inteiros (possivelmente negativos) a1 , a2 , . . . an . Calcular o valor de
Pj
k=i ai . Se todos os inteiros são negativos, a maior subseqüência é a subseqüência vazia e a sua soma é 0.

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

4.1 Um algoritmo cúbico


O algoritmo 4 é o mais simples e direto que se pode imaginar. Consiste em fazer uma busca exaustiva de todas
as possı́veis subseqüências. O tamanho da entrada deste algoritmo é n, a quantidade de elementos no vetor
parâmetro.
Dois laços iteram sobre os limites de todas as subseqüências possı́veis linhas(linhas 7 e 8), O valor de cada
subseqüência é computado (linhas 10 a 12. Se esse valor for superior a da soma máxima computado até agora,
armazenada na variável SomaMáxima, a soma máxima é atualizada (linha 13) Enfim, a função retorna o valor
computado (linha 16)

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 ).

4.2 Um algoritmo quadrático


O algoritmo cúbico é muito simples porém muito ineficiente. O algoritmo 5 mostra um algoritmo quadrático
resolvendo esse problema.

Algoritmo 5 (subseqüênciaSomaMáxima)
1 funct subseqüênciaSomaMáxima(v : array Z, n : N) : Z ≡
4.3 Um algoritmo linear 10

2 var soma, somaMáxima : Z; i, j : N


3 end
4 begin
5 somaMáxima ← 0
6 for i = 1 to n step 1 do
7 soma ← 0
8 for j = i to n step 1 do
9 soma ← soma + v[k]
10 if soma > somaMáxima then somaMáxima ← soma fi
11 od
12 od
13 result ← somaMáxima
14 end .

Esse algoritmo só contém dois laços aninhados, e é O(n2 ) (quadrático).

4.3 Um algoritmo linear


O algoritmo 6 é mais eficiente ainda. A ideia deste algoritmo é de realizar uma única iteração. A cada passo da
iteração, a variável SomaMáxima armazena a maior subseqüência do vetor entre os ı́ndices 0 e i-1, e a variável
SomaSufixo mantém a maior soma de todos os sufixos deste subvetor. Quando todos os sufixos tem valor
negativo, SomaSufixo é nulo.
Este algoritmo só contém um bloco de iteração, que enumera todos os valores de 0 até N-1. Em cada
iteração, existe um número máximo de instruções que serão executadas que é independente de N , e que logo é
constante. Concluı́mos portanto que este algoritmo é O(n).

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

4.4 Conseqüências práticas


Os três algoritmos foram implementados e executados com entradas de tamanho de até 50.000. Os tempos
de execução de cada algoritmo são reportados de forma gráfica na figura 1. Como pode se observar, há uma
diferença enorme de comportamento asintótico. Podemos deduzir que, independentemente da qualidade da
implementação, ou da plataforma onde for portado, a implementação do terceiro algoritmo apresentado levará
vantagem sobre os demais de forma espetacular. O objetivo desse experimento é demostrar o quão importante
é, para o profissional de computação:
• ter consciência que, para o mesmo problema, algoritmos diferentes podem ter custos de execução asinto-
ticamente completamente diferentes;
• conhecer os algoritmos e as estruturas de dados as mais eficientes para os problemas de base;
• quando se depara com um problema novo, saber procurar na literatura os algoritmos os mais eficientes
que existam.

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

Figura 1: Plotagem dos tempos de execução dos diferentes algoritmos


4.4 Conseqüências práticas 13

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.

Você também pode gostar