Buscar

Algoritmos Avançados

Prévia do material em texto

Algoritmos Avançados 
Prof. José Luiz Rosa – jose.anjos@estacio.br 
Ementa 
 
 
 Análise de Algoritmo 
 
 Recursividade 
 
 Algoritmos de Ordenação 
 
 Estruturas de dados dos tipos Árvore 
 
 Algoritmos em Grafos 
Bibliografia Básica 
 
ASCENCIO, Ana Fernanda Gomes; ARAÚJO, Graziela Santos de. 
Estruturas de Dados: algoritmos, análise da complexidade e 
implementações em JAVA, C/C++. São Paulo: Pearson Prentice Hall, 
2010. 
 
CORMEN, T. Desmistificando algoritmos. Rio de Janeiro: Elsevier, 
2014. 
 
Koffman, Elliot B.; Wolfgang, Paul A. T. Abstração, Estrutura de 
Dados e Projeto Usando C++. Rio de Janeiro: LTC, 2008. 
Bibliografia Complementar 
 
CELES, Waldemar; CERQUEIRA,Renato;RANGEL,José 
Lucas. Introdução a Estrutura de Dados com técnicas de 
programação em C. Rio de Janeiro : Elsevier, 2004. 
 
DROZDEK, Adam. Estrutura de dados e Algoritmos em C++. São 
Paulo: Pioneira Thomson Learning, 2005. 
 
EDELWEISS, N, GALANTE, R. M., Estrutura de Dados, Volume 18 - 
Série Livros Didáticos Informática UFRGS.1.ed.RS: Bookman, 2009. 
FORBELLONE, André Luiz Villar; EBERSPACHER, Henri 
Frederico. Lógica de Programação: A construção de Algoritmos e 
Estrutura de Dados. 3. ed. São Paulo: Makron Books, 2005. 
 
JEFF, E. Como pensar em algoritmo. Rio de Janeiro: LTC, 2010. 
 
KOFFMAN, Elliot B., WOLFGANG, Paul A.T., Objetos, Abstração, 
Estrutura de dados e Projeto usando C++, 1. ed. Rio de Janeiro: 
LTC,2008. 
 
SZWARCFITER, Jayme Luiz; MARKENZON, Lilian. Estruturas de 
dados e seus algoritmos. 3. ed. Rio de Janeiro, RJ: LTC, 2010. viii, 
302 p. ISBN 978-85-216-1750-1. 
Análise de Algoritmo 
 
- Revisão de C++ 
 
 . Array 
 . Registro (struct) 
 . Ponteiro 
 . Função. 
 
- Complexidade (notação O) 
Revisão de C++ 
 
Tipos 
 
 . int (inteiro): 10 
 . float (ponto flutuante de precisão simples): 10.0 
 . double (ponto flutuante de precisão dupla): 10.0 
 . char (caractere): 'A' 
 . bool (lógico): true/false 
 . void (especifica que uma função não retorna valor) 
Enumeração (contém um conjunto de valores especificados pelo 
usuário): 
 
enum binario {zero, um}; 
int valor; 
 
cout << "Numero: "; 
cin >> valor; 
if (valor == zero) 
 cout << "Zero" << endl; 
else 
 if (valor == um) 
 cout << "Um" << endl; 
 else 
 cout << "Valor fora de limite!" << endl; 
Outro exemplo: 
 
enum graus {c = 100, f = 212}; 
int temperatura; 
 
cout << "Temperatura em que a água pura ferve: "; 
cin >> temperatura; 
cout << "A água ferve a " << temperatura; 
if (temperatura == c) 
 cout << " graus Celsius." << endl; 
else 
 cout << " graus Fahrenheit." << endl; 
Typedef (declara um nome para um tipo): 
 
 
typedef float pontoFlutuantePrecisaoSimples; 
typedef double pontoFlutuantePrecisaoDupla; 
 
pontoFlutuantePrecisaoSimples f; 
pontoFlutuantePrecisaoDupla d; 
 
cout << "Tamanho do tipo float : " << sizeof (f) << endl; 
cout << "Tamanho do tipo double: " << sizeof (d) << endl; 
Constante (não permite a redefinição de um objeto após ser 
iniciado): 
 
const int limite = 100; 
 
A tentativa de redefinir “limite”, gera erro de compilação: 
 
limite = 50; 
[Error] assignment of read-only variable 'limite' 
 
Definir “limite” sem iniciá-la, gera erro de compilação: 
 
const int limite 
[Error] uninitialized const 'limit' 
Array 
 
Um array é um agregado de elementos de mesmo tipo. Declara-se 
um array em C++ como: 
 
tipo nome_array [quantidade_elementos] 
 
Por exemplo, a declaração: int v[3]; define um array de elementos 
“int”, de nome “v” e tamanho “3” (possui 3 elementos). 
Os elementos são indexados, sendo que o primeiro elemento 
está na posição 0 (zero), e o último, na posição “tam – 1”. 
 
int v[3]; 
 
v[0] = 1; 
v[1] = 10; 
v[2] = 100; 
Um array pode ter mais de uma dimensão (multidimensional): 
 
double localizacao [10][15]; 
 
Sendo válida a atribuição: 
 
localizacao [7][12] = 3.45 
 
Um array pode ser iniciado por uma lista de valores: 
 
char vogal [ ] = { 'a' , 'e' , 'i' , 'o' , 'u' } ; 
 
Fazer: cout << "Segunda vogal: " << vogal [1]; resulta em: 
Segunda vogal: e 
Estrutura (struct) 
 
Uma struct é um agregado de elementos que podem ser de tipos 
diferentes. 
 
struct letra 
{ 
 char simbolo; 
 int ascii; 
}; 
 
 
Observe o ponto-e-vírgula após a chave de fechamento. 
O acesso aos membros da struct se faz por meio do operador 
ponto ( . ): 
 
struct letra letra_A; 
 
letra_A.simbolo = 'A'; 
letra_A.ascii = 65; 
 
Pode-se definir um array de struct: 
 
struct letra vogais[5]; 
 
vogais[0].simbolo = 'A'; 
vogais[0].ascii = 65; 
Para evitar ter de escrever “struct letra”, pode-se utilizar typedef: 
 
typedef struct letra str_letra; 
 
struct letra 
{ 
 char simbolo; 
 int ascii; 
} ; 
 
str_letra letra_A; 
 
Ponteiro 
 
Uma variável do tipo ponteiro armazena um endereço de memória. 
Declara-se uma variável ponteiro, colocando-se um asterisco 
entre o tipo e o nome da variável: 
 
int *p; 
 
Uma forma de iniciar a variável “p”, é atribuir à mesma o endereço 
de outra variável por meio do operador de endereço &: 
 
int a; 
p = &a 
Não se pode atribuir um valor inteiro à variável “p”, pois a mesma 
não é do tipo “int”, e sim, um ponteiro para “int” (armazena o 
endereço inicial de memória onde está armazenado um valor do 
tipo int. Por exemplo, fazer: 
 
p = 10; 
 
gera a seguinte mensagem de erro na compilação: 
 
[Error] invalid conversion from 'int' to 'int*' 
 
A operação básica sobre um ponteiro é a derreferência (também 
chamada de indireção), pela qual faz-se referência ao objeto 
apontado pelo ponteiro. 
Esta operação utiliza o operador de derreferência *. Por exemplo: 
 
*p = 10; 
Fazer *p = 10; faz com que o endereço apontado por p (variável a) 
receba o valor 10 (lembre que “a” é uma variável inteira, e que 
“p” contém o endereço desta variável). 
 
int a , *p; 
p = &a 
 
*p = 10; 
 
cout << "*p = " << *p << endl; 
cout << "a = " << a << endl; 
 
Exibirá o valor 10, pois ambas as instruções exibem o conteúdo 
da mesma posição de memória. 
Porém, executar: 
 
cout << "p = " << p << endl; 
 
Produz como resultado: 
 
p = 0x6ffdec 
 
 
Observando que o endereço é mostrado em hexadecimal, e pode 
variar de sistema para sistema no qual o código seja executado. 
Ponteiros e arrays são intimamente relacionados. O nome de um 
array é um ponteiro para seu elemento inicial. Seja: 
 
int *p; 
int v[ ] = {1,2}; 
 
Fazer: 
 
p = v; 
 
é o mesmo que fazer: 
 
p = &v[0]; 
Aplicar um dos operadores aritméticos: + , - , ++ ou -- a ponteiros, 
faz com que o ponteiro aponte para uma posição de memória 
posterior ou anterior tantas posições quantas forem o tamnho do 
tipo definido para o ponteiro. Em uma plataforma na qual o valor 
do tipo int ocupe 4 bytes de memória, executar: 
 
int a; 
cout << "Tamanho do tipo int: " << sizeof (a) << endl; 
 
Faz com que seja exibido: 
 
Tamanho do tipo int: 4 
 
Então, as execuções abaixo produzem: 
 
int a, *p; 
p = &a ; 
 
cout << "p = " << p << endl; // exibe: p = 0x6ffdec 
p++; 
cout << "p = " << p << endl; // exibe: p = 0x6ffdf0 
p = p + 3; 
cout << "p = " << p << endl; // exibe: p = 0x6ffdfc 
p--; 
cout << "p = " << p << endl;// exibe: p = 0x6ffdf8 
Deve-se atentar para o fato de as variáveis “p” e “a” estarem 
alocadas em diferentes posições de memória: 
 
int a, *p; 
p = &a ; 
 
cout << "Endereço de p = " << &np << endl; // exibe: p = 0x6ffdf0 
cout << "Endereço de a = " << &na << endl; // exibe: p = 0x6ffdfc 
 
Referência 
 
Uma referência é um nome alternativo para uma um objeto. Ao 
fazer: 
 
int i; 
int &j = i; 
 
tem-se que “i” e “j” referenciam a mesma posição de memória. 
 
Dessa forma, fazer: i = 10; é o mesmo que fazer: j = 10; ; da 
mesma forma, fazer: cout << i; é o mesmo que fazer: cout << j; 
 
 
Adicionalmente, conforme mostra a saída do código abaixo: 
 
int x; 
int &y = x; 
 
cout << “Endereço de x: " << &x << endl; // exibe: 0x6ffdec 
cout << “Endereço de y: " << &y << endl; // exibe: 0x6ffdec 
 
Tem-se que “x” e “y” são a mesma variável de memória. 
Ponteiro para estrutura 
 
Utiliza-se o operador “new” para alocar memória. O endereço 
retornado por “new” pode ser atribuído a um ponteiro. 
 
struct Livro { 
 string nome; 
 string autor; 
 string editora; 
 int ano_publicacao; 
}; 
 
typedef struct Livro TLivro; 
TLivro *pLivro; 
pLivro = new TLivro; 
Para manipular os membros da struct, é utilizado o operador 
“derreferência a ponteiro para estrutura”, cujo símbolo é: ->. 
 
struct Livro { 
 string nome; 
 string autor; 
 string editora; 
 int ano_publicacao; 
}; 
typedef struct Livro TLivro; 
TLivro *pLivro; 
pLivro = new TLivro; 
 
pLivro->nome = "A Linguagem de Programação C++"; 
pLivro->autor = "Bjarne Stroustrup"; 
pLivro->editora = "Bookman"; 
pLivro->ano_publicacao = 2000; 
Um dos campos da estrutura, pode ser um ponteiro para a 
própria estrutura, permitindo uma iteração entre as diversas 
alocações, ou seja, cria uma lista encadeada. 
Por exemplo, seja a declaração abaixo: 
 
typedef struct Agenda TAgenda; 
 
struct Agenda { 
 string nome; 
 TAgenda *prox; 
}; 
 
TAgenda *a, *antes, *prim; 
Vamos alocar a estrutura na memória: 
 
a = new TAgenda; 
 
Agora, vamos armazenar o endereço da próxima estrutura. 
Porém, como não há, ainda, uma próxima estrutura alocada, o 
próximo endereço apontará para NULL: 
 
a->prox = NULL; 
 
O próximo passo é armazenar o nome: 
 
a->nome ="José"; 
 
Quando alocarmos memória para a próxima estrutura, o endereço 
da estrutura anterior se perderá. Então, deve-se guardar essa 
informação antes da próxima alocação. Para tal, será utilizada a 
variável “antes”: 
 
antes = a; 
 
Uma vez que “antes” e “a” são ponteiros para a struct “Tagenda”, 
tanto “antes” como “a” possuem, a partir de agora, o mesmo 
conteúdo, ou seja, o endereço de memória que aponta para a 
estrutura alocada atualmente. 
Como foi a primeira alocação, vamos armazenar o endereço da 
primeira alocação: 
 
prim = a; 
Graficamente, teríamos: 
nome: "José" 
 
*prox 
prim 
a 
antes 
Pronto! Podemos alocar o próximo endereço: 
 
a = new TAgenda; 
 
O que se tem após essa nova alocação é: 
 
 
 
 
 
 
 
 
 
Observe que não há ligação entre as estruturas. 
nome: "José" 
 
*prox 
prim antes 
a 
nome: 
 
*prox 
Para preencher o campo “nome” da nova alocação, será utilizado 
o ponteiro “a”, que possui o endereço desta alocação: 
 
a->nome = "Luiz"; 
 
 
 
 
nome: "José" 
 
*prox 
prim antes 
a 
nome: "Luiz" 
 
*prox 
 
O endereço da alocação atual ( “a” ) será armazenado no campo 
“prox” da alocação passada. Quem armazena o endereço da 
última alocação é o ponteiro “antes”. Faz-se, então: 
 
antes->prox = a; 
 
 
nome: "José" 
 
*prox 
prim 
a 
antes 
nome: "Luiz" 
 
*prox 
 
O endereço da próxima alocação ainda não existe. Então, o 
campo “prox” da alocação atual deve apontar para NULL: 
 
a->prox = NULL; 
 
 
nome: "José" 
 
*prox 
prim 
a 
antes 
nome: "Luiz" 
 
*prox 
Novamente, quando alocarmos memória para a próxima 
estrutura, o endereço da estrutura anterior se perderá. Então, 
deve-se guardar essa informação antes da próxima alocação. 
Para tal, será utilizada a variável “antes”: 
 
antes = a; 
 
 
nome: "José" 
 
*prox 
prim 
a 
antes 
nome: "Luiz" 
 
*prox 
Para exibir as informações armazenadas nas diversas estruturas 
alocadas em memória, é necessário retornar ao endereço da 
primeira alocação. Faz-se isso, atribuindo ao ponteiro “a” o 
conteúdo do ponteiro “prim”: 
 
a = prim; 
 
Antes de continuar, devemos nos certificar que houve, pelo 
menos, uma alocação, ou seja, devemos testar se o conteúdo de 
“a” é diferente de NULL. 
 
if (a != NULL) 
 
Se houve alocação, deve-se imprimir o campo “nome” enquanto 
o campo “prox” for diferente de NULL: 
 
do 
{ 
 cout << a->nome << " "; 
 a = a->prox; 
} 
while (a != NULL); 
 
Quando o campo “prox” for igual a NULL, a iteração é encerrada. 
Para finalizar, utiliza-se o operador “delete” na desalocação da 
memória alocada com “new” : 
 
delete a; 
O código completo fica como: 
 
#include <iostream> 
using namespace std; 
 
typedef struct Agenda TAgenda; 
struct Agenda { 
 string nome; 
 TAgenda *prox; 
}; 
 
int main(int argc, char** argv) { 
 TAgenda *a, *antes, *prim; 
 
 a = new TAgenda; 
 a->prox = NULL; 
 a->nome = "Jose"; 
 antes = a; 
 prim = a; 
 a = new TAgenda; 
 a->nome = "Luiz"; 
 antes->prox = a; 
 a->prox = NULL; 
 antes = a; 
 
 a = prim; 
 if (a != NULL) 
 do 
 { 
 cout << a->nome << " "; 
 a = a->prox; 
 } 
 while (a != NULL); 
 
 delete a; 
 
 return 0; 
 
} // fim da função main 
Função 
 
Uma função é uma estrutura de programa que possui o tipo de 
valor devolvido (tipo da função), um nome e, entre parênteses, 
nenhum ou muitos argumentos. Se especificados, os argumentos 
devem ser fornecidos na chamada da função. 
Então, a assinatura de uma função é como: 
 
tipo_valor_de_retorno nome_da_função (tipo1, tipo2, ..., tipon); 
 
Uma função do tipo “void” é uma função que não retorna valor. 
Na declaração da função, são apresentados somente os tipos dos 
argumentos, se existirem. Já na definição da função, os 
argumentos devem conter, além dos tipos, seus nomes 
respectivos. Uma função não declarada como “void”, deve 
retornar, ao final da definição, por meio do comando “return”, o 
valor produzido pela execução de seu código. 
Por exemplo, segue a declaração e a definição da função soma: 
 
int soma (int, int); // declaração 
 
int soma (int a, int b) // | início da definição 
{ // | 
 return a+b; // | 
} // | fim da definição 
A chamada de uma função é realizada dentro do corpo principal 
do programa (função “main”), ou dentro de outra função definida 
pelo programador: 
 
int main(int argc, char** argv) { 
 
 int x, y, s; 
 
 x = 1; 
 y = 2; 
 s = soma (x,y); 
 cout << "Soma: " << s << endl; 
 
 return 0; 
} 
Juntando todo o código: 
 
int soma (int, int); // declaração 
 
int main(int argc, char** argv) { 
 
 int x, y, s; 
 
 x = 1; 
 y = 2; 
 s = soma (x,y); 
 cout << "Soma: " << s << endl; 
 
 return 0; 
} 
 
int soma (int a, int b) // | início da definição 
{ // | 
 return a+b; // | 
} // | fim da definição 
Passagem de parâmetros que envolvem variáveis como “int” ou 
“double” é chamada de passagem por valor.Quando a passagem 
envolve ponteiro, diz-se tratar de passagem por referência. No 
exemplo anterior, ao término da execução da função soma, as 
variáveis “a” e “b” definidas como argumentos da função, tem 
seu ciclo de vida iniciando na chamada da função e terminando 
ao final da execução da função. Não há como acessar os valores 
de “a” ou de “b”. Já os valores das variáveis “x” e “y” continuam 
intactos. 
Já em uma passagem por referência, são os endereços que são 
passados para a função. Desta forma, ao término da execução da 
função, os valores das variáveis envolvidas na passagem de 
argumentos, passam a conter as alterações porventura ocorridas 
no corpo da função. 
 
 
Por exemplo, seja a declaração da função troca: 
 
void troca (int*, int*); 
 
E sua declaração: 
 
void troca (int* i, int* j) 
{ 
 int t; 
 
 t = *i; 
 *i = *j; 
 *j = t; 
} 
Vamos invocar (chamar) a função troca: 
 
 
int main(int argc, char** argv) { 
 
 int a = 5, b = 10; 
 
 cout << “a = “ << a << “ b = “ << b << endl; 
 troca (&a,&b); 
 cout << “a = “ << a << “ b = “ << b << endl; 
 
 return 0; 
} 
O código poderia ser reescrito, utilizando-se o operador de 
endereço & na definição da função: 
 
void troca (int &, int &); 
 
int main(int argc, char** argv) { 
 int a , b; 
 a = 5; 
 b = 10; 
 
 cout << "a = " << a << "b = " << b << endl; 
 troca (a,b); 
 cout << "a = " << a << "b = " << b << endl; 
 
 system ("pause"); 
 return 0; 
} 
void troca (int &i, int &j) 
{ 
 int t; 
 
 t = i; 
 i = j; 
 j = t; 
} 
Complexidade de Algoritmo 
 
Um algoritmo é uma sequência de ações executáveis cuja 
finalidade é resolver um dado problema por meio de abstrações 
do mundo real. 
Uma vez que sejam definidos os tipos que representarão os 
objetos do mundo real e que estejam dentro do contexto da 
resolução do problema, deve-se obter uma medida de quão 
otimizado é o algoritmo em termos de tempo de execução. 
Em havendo mais de uma forma de se processar os dados na 
resolução de um problema em particular, saber o quanto um 
algoritmo é melhor do que o outro, responde à pergunta sobre 
qual algoritmo apresenta melhor desempenho, ou seja, possui 
menor custo, para resolver o mesmo problema. 
Uma das formas que se pode utilizar, é a medida do tempo de 
execução de cada um dos algoritmos. 
Essa abordagem é falha, uma vez que é dependente da 
arquitetura de software (sistema operacional, compilador) e de 
hardware (processador, quantidade de memória, gpu etc.). 
 
Outra forma, seria medir o custo da resolução de um problema 
por um algoritmo, via modelo matemático, especificando-se as 
operações e seus custos de execução. 
Como todo modelo, deve-se fazer algumas simplificações, de 
forma a facilitar a obtenção de uma solução que, se não for a 
ideal, que pelo menos seja muito próxima dela. 
 
E essa aproximação se faz ignorando-se o custo de algumas 
operações comuns, levando-se em consideração, somente, as 
operações mais custosas. 
Para que se possa obter uma medida de custo de execução de 
algoritmos, permitindo a comparação entre eles, utiliza-se uma 
função denominada função de complexidade f. Pode-se dizer, 
então, que f(n) representa o tempo necessário que uma parte 
determinante do algoritmo leva para resolver um problema de 
tamanho n. Atente para o fato de que se trata de uma medida de 
esforço, de custo computacional, e não de tempo 
verdadeiramente. 
 
Como utilizaremos função, passemos à revisão de algumas 
funções que serão úteis no aprendizado desta disciplina. 
Função constante: f(n) = c 
 
É a função mais simples de todas, e, não importando qual seja o 
valor de n, f(n) sempre será igual à constante c. Por exemplo, o 
dobro de um valor sempre será obtido em um passo apenas (não 
importando que valor seja esse). 
 
Uma função cujo valor seja constante e igual a 1 é considerada 
fundamental. Sendo assim, dada uma função qualquer g(n) = 1 e 
f(n) = c, tem-se que: f(n) = cg(n). Esta função é útil na análise de 
algoritmos em virtude de caracterizar o número de passos 
necessários para executar uma operação básica, tal como uma 
operação de atribuição ou de comparação entre dois valores. 
Abaixo, tem-se a representação gráfica da função constante f(n) = 1: 
 
Função logaritmo: f(n) = log b n 
 
Para todo n maior do que zero, produz como resultado um valor x 
tal que: 
 
- n = 1 => x = 0. 
- n > 1 => log b n = x, se e somente se: b
x = n 
 
Então, log 2 8 = 3, pois, 2
3 = 8 
Sua representação gráfica é: 
 
0 
10 
20 
30 
40 
50 
60 
70 
80 
90 
100 
Função Logaritmo: f(n) = log(n) 
Função Linear: f(n) = n 
 
Para todo valor de n, f(n) é igual a n. Uma operação que compara 
um valor qualquer com os elementos de uma coleção, requer n 
operações de comparação. Se a coleção possuir 10 elementos, 
serão 10 comparações. Se forem 50 elementos, serão 50 
comparações. 
 
Sua representação gráfica é: 
 
0 
10 
20 
30 
40 
50 
60 
70 
80 
90 
100 
1 2 3 4 5 6 7 8 9 10 11 
Função Linear: f(n) = n 
Função n log n: f(n) = n log b n 
É uma função que cresce mais rápido que a função linear. 
Sua representação gráfica é: 
 
0 
10 
20 
30 
40 
50 
60 
70 
80 
90 
100 
1 2 3 4 5 6 7 8 9 10 11 
Função: f(n) = n log n 
Função quadrática: f(n) = n2 
É uma função que possui uma taxa muito alta de crescimento 
conforme o valor de n aumenta. 
Sua representação gráfica para poucos dados é: 
 
0 
20 
40 
60 
80 
100 
120 
1 2 3 4 5 6 7 8 9 10 
Função quadrática: f(n) = n2 
Porém, conforme a quantidade de dados se torna enorme, a 
representação gráfica toma a forma: 
 
0 
10 
20 
30 
40 
50 
60 
70 
80 
90 
100 
1 2 3 4 5 6 7 8 9 10 11 
Função quadrática: f(n) = n2 
Antes de prosseguir, uma revisão de matemática. 
Seja a progressão: 
a1 . a2 ... an-1 . an 
Cuja soma é: 
 s = a1 + a2 + ... + an-1 + an 
 Invertendo-se a ordem dos termos: 
 s = an + an-1 + ... + a2 + a1 
 Somando as duas expressões, vem: 
 2s = (a1+an) + (a2+an-1) + … + (an-1+a2) + (an+a1) 
 obtido pela soma de cada termo de uma expressão com o termo 
de mesma posição da outra expressão. 
Ou seja, 
 2s = (a1+an) + (a2+an-1) + … + (an-1+a2) + (an+a1) 
 
foi obtido como abaixo: 
 
(a1 + a2 + ... + an-1 + an) 
(an + an-1 + ... + a2 + a1) 
(a1 + an) + (a2 + an-1) + … + (an-1 + a2) + (an + a1) 
 
Se é uma progressão, tem-se que: 
 
(a1 + an) = (a2 + an-1) = ... = (an-1 + a1) 
 
tal como descoberto pelo matemático Carls Gauss, que obteve, 
rapidamente, a soma de todos os números inteiros entre 1 e 100. 
+ 
Então: 
2s = (a1 + an) + (a1 + an) + ... + (a1 + an) 
 
Como são n somas: 
 2s = n (a1 + an) 
 
O que resulta em: 
 
 
Ou seja: 
 
 s = 
 
n (a1 + an) 
 2 
 
i = 1 
i = n 
 ai = 
n ( a1 + n) 
 2 
 
Aplicando a teoria. 
 
Seja o trecho de algoritmo contendo dois laços aninhados: 
 
para i de 1 até n passo 1 
 para j de 1 até i passo 1 
 comando 
 
O número de operações executadas pelo laço mais interno é: 
1 + 2 + 3 + ... + (n-1) + n. 
Pela soma de Gauss, tem-se que: 
 
 
 
 
Ou seja, o número de operações é: 
 
 
Isto significa que a taxa de crescimento tem comportamento 
quadrático. 
i = 1 
i = n 
i = 1 + 2 + 3 + ... + (n - 1) +n = 
n ( 1 + n ) 
 2 
 
n + n2 
 2 
 
Função cúbica: f(n) = n3 
É uma função que possui uma taxa de crescimento ainda mais 
alta que a função quadrática. 
Sua representação gráfica é: 
0 
10 
20 
30 
40 
50 
60 
70 
80 
90 
100 
1 2 3 4 5 6 7 8 9 10 11 
Função cúbica: f(n) = n3 
Função Exponencial: f(n) = an 
 
Uma função exponencial possui uma taxa de crescimento bem 
mais alta que a cúbica. 
Muito comum em computação é a representação de um número 
por meio de um somatório de termos cuja base é 2 e o expoente 
varia de 0 a n. 
Por exemplo: 
 
15 = 20 + 21 + 22 + 23 
15 = 1 + 2 + 4 + 8 
 
 
Para um valor qualquer nesta base, temos: 
 
 
De forma geral: 
 
i = 0 
i = 3 
 2i 15 = 
i = 0 
i = n 
2i = 20 + 21 + 22 + ... + 2n 
i = 0 
i = n 
ai = a0 + a1 + a2 + ... + an = 
an+1 - 1 
a - 1 
 
Sua representação gráfica é: 
 
0 
10 
20 
30 
40 
50 
60 
70 
80 
90 
100 
1 2 3 4 5 6 7 8 9 10 11 
Função Exponencial: f(n) = an 
Juntando as representações gráficas de todas as funções, 
podemos comparar suas taxas de crescimento: 
 
 
 
 
 
 
 
 
 
 
 
Do gráfico obtemos: 
constante < logaritmo < linear < n log n < quadrática < cúbica < exponencial 
0 
10 
20 
30 
40 
50 
60 
70 
80 
90 
100 
1 2 3 4 5 6 7 8 9 10 11 
Comparativo entre todas as funções 
f(n) = c f(n) = log n f(n) = n f(n) = n log n 
f(n) = n^2 f(n) = n^3 f(n) = e^n 
Polinômio 
 
Vimos que um valor pode ser representado em termos de uma 
função polinomial, cuja base é 2, com o expoente variando de 
0 a n. Ou seja: 
f(n) = a0 + a1 + a2 + ... + an 
 
 Vimos que o número 15 é formado pela sequência: 
 
 15 = 20 + 21 + 22 + 23 
 
 Antes dele vem o 7: 20 + 21 + 22 
 
E, depois, vem o 31: 20 + 21 + 22 + 23 + 24 
Mas, como representar o valor 10? Simples, basta desconsiderar 
os termos: 20 e 22, considerando, apenas, os termos 21 e 23. Ou 
seja: 
 
10 = 0 x 20 + 1 x 21 + 0 x 22 + 1 x 23 
10 = 0 + 2 + 0 + 8 = 2 + 8 
 
No exemplo dado, pode-se dizer que: 
 
f(2) = 0 x 20 + 1 x 21 + 0 x 22 + 1 x 23 
 
é uma função polinomial, e que as constantes 0 e 1 que 
multiplicam os termos de base 2, são denominados coeficientes 
desse polinômio. 
Isto significa dizer que uma função polinomial tem a seguinte 
forma: 
f(n) = a0 + a1 n + a2 n
2 + ... + ad n
d 
 sendo que: 
 
 - Os valores a0, a1, a2, ... , ad são chamados de coeficientes do 
polinômio. 
 
- ad é diferente de zero. 
 
- O valor d é chamado grau do polinômio. 
Análise Assintótica 
 
 A análise de um algoritmo visa caracterizar seu tempo de 
execução por meio de uma função f(n) que leva em consideração 
o tamanho da entrada, mas que independe do software e do 
hardware. 
Por meio desta análise, algoritmos podem ser, relativamente, 
comparados, em termos de suas eficiências na resolução de um 
dado problema. 
 
Então, para se obter o tempo estimado de execução de um 
algoritmo, basta que se obtenha o número de Operações 
Primitivas executadas, tais como: 
- Operações de Atribuição 
- Operações Aritméticas 
- Operações Relacionais 
- Operações Lógicas 
- Invocação de uma função 
- Retorno de uma função 
- Desvio condicional 
- Acesso ao membro de uma estrutura 
- Acesso a uma posição de um array 
- Manipulação de conteúdo via ponteiro 
Essas operações correspondem a instruções de baixo nível com 
um tempo de execução constante. Contando-as, tem-se uma 
estimativa do tempo de execução do algoritmo em análise. 
Trata-se de estimativa, pois um algoritmo pode executar mais ou 
menos rapidamente em função do tamanho da entrada. 
Dentro deste conceito, algoritmos que executem, em função de n, 
uma quantidade de operações igual a: 
 
Algoritmo a: f(n) = n2 
Algoritmo b: f(n) = n2 + n 
Algoritmo c: f(n) = 10 n2 
 
são equivalentes, ou seja, pertencem à mesma ordem (n2), e seus 
tempos crescem, praticamente, à mesma velocidade, pois todos 
têm comportamento quadrático, isto é, o crescimento do número 
de operações cresce quadraticamente conforme aumenta o 
tamanho da entrada. 
Exemplificando, seja um programa contendo o trecho abaixo: 
 
 
time_t inicio, fim; 
 
time(&inicio); 
for (int i=0; i<n;i++) 
 for (int j=0; j<1E+3; j++); 
time(&fim); 
cout << "Tempo: " << difftime(fim,inicio); 
 
Executando-o por meio de um programa na linguagem C++, em 
um computador com CPU Intel® Core™ i7, 2.2GHz com 8GB de 
memória RAM, obtém-se os seguintes tempos de execução: 
 
n = 1E+3 : tempo = 0s 
n = 1E+6 : tempo = 2s 
n = 1E+7 : tempo = 23s 
n = 1E+8 : tempo = 231s 
n = 1E+9 : tempo = 2.328s 
A representação gráfica dos tempos de processamento é como 
segue: 
 
0 
500 
1000 
1500 
2000 
2500 
1 2 3 4 5 
Evolução do tempo de execução do algoritmo 
Apesar dos poucos dados observados de tempo de execução, 
nota-se, pela taxa de crescimento apresentado, tratar-se de uma 
função quadrática, ou seja, f(n) = n2: 
 
 
 
 
 
 
Isto significa que, qualquer algoritmo que tenha característica de 
uma função quadrática, terá seus tempos de execução variando 
conforme o gráfico apresentado acima. 
Já dois algoritmos cuja quantidade de operações seja: 
 
Algoritmo A: f(n) = n2 
Algoritmo B: f(n) = n + 1.000 
 
Não pertencem à mesma ordem, pois o primeiro tem 
comportamento quadrático, e o segundo, comportamento linear. 
 
Se n = 2, a quantidade de operações executadas por cada algoritmo 
é: 
 
Algoritmo A: 102 = 100 operações 
Algoritmo B: 10 + 1.000 = 1.010 operações 
 
Ou seja, o algoritmo A é 10 vezes mais rápido do que o algoritmo B. 
 
Entretanto, se n = 1.000, a quantidade de operações executadas é: 
 
Algoritmo A: 1.0002 = 1.000.000 
Algoritmo B: 1.000 + 1.000 = 2.000 
 
Ou seja, o algoritmo A é 500 vezes mais lento do que o algoritmo B. 
 
Seria interessante analisar o algoritmo levando-se em 
consideração uma entrada cujo tamanho fosse a média de todas 
as possíveis entradas. Porém, obter esta quantidade requer 
cálculos probabilísticos complexos, o que inviabiliza essa 
abordagem. 
 
Então, leva-se em consideração, na análise do algoritmo, um 
tamanho de entrada n correspondente ao pior caso, partindo do 
princípio que, analisando o pior caso, o algoritmo tenha tempos 
de execução menor para as demais entradas. 
A análise do pior caso baseia-se em valores enormes de n (n->∞), 
cujo comportamento da função é chamado assintótico. Neste 
cenário, os termos inferiores e as constantes multiplicativas 
podem ser descartados, uma vez que pouco ou nada contribuem 
na comparação entre as taxas de crescimento do tempo de 
resolução do problema. 
Na análise de algoritmos, nos interessa estudar funções 
assintoticamente não negativas, o que significa dizer que existe 
um valor no tal que f(n) > 0 para todo n > no. 
 
Contando operações primitivas de um algoritmo 
 
Como foi dito anteriormente, o tempo de execução de um 
algoritmo é diretamente relacionado à quantidade de operações 
primitivas executadas por ele. Então, deve-se obter essa 
quantidade visando-se comparar dois algoritmos. 
Também deve ser observado que, um algoritmo não 
necessariamente é sempre pior (ou melhor) do que o outro, o que 
dependerá da quantidade de dado a ser processado. Um 
algoritmo pode ter desempenho melhordo que outro para 1000 
dados, porém, ter desempenho pior do que o outro para bases 
com mais de 1000 elementos. 
Seja o algoritmo abaixo, que retorna o maior valor lido de um 
array de n posições: 
 
#include <iostream> 
using namespace std; 
 
int maiorElemento (int [], int n); 
int main(int argc, char** argv) { 
 
int a[5]; 
 a[0]=10; 
a[1]=35; 
a[2]=2; 
a[3]=5; 
a[4]=11; 
cout << "Maior:" << maiorElemento(a,5); 
return 0; 
} 
 
int maiorElemento (int arr[], int n) 
{ 
 int maiorElemento = arr[0]; 
 for (int i=1; i<n; i++) 
 if (arr[i] > maiorElemento) 
 maiorElemento = arr[i]; 
 return maiorElemento; 
} 
Vamos analisar o trecho que contém a função: 
 
int maiorElemento (int arr[], int n) 
{ 
 int maiorElemento = arr[0]; 
 for (int i=1; i<n; i++) 
 if (arr[i] > maiorElemento) 
 maiorElemento = arr[i]; 
 return maiorElemento; 
} 
Assumindo que as 
instruções primitivas 
possuem o mesmo 
custo de processamento, 
tem-se: 
Instrução Número de execuções 
int maiorElemento = arr [ 0 ] ; 1 
for (int i = 1; i < n; i++) 2n 
--------------------- 
1ª execução: 
 int i = 1 : 1 
 i < n : 1 
Demais execuções: 
 i++ : n - 1 
 i<n : n - 1 
Resultado: 
 1+1+n-1 +n-1 = 2n 
 if ( arr [ i ] > maiorElemento ) n-1 
--------------------- 
1 instrução “n-1” vezes 
 maiorElemento = arr [ i ] ; n-1 
--------------------- 
1 instrução. 
Como a condição é sempre true, a 
instrução será executada “n-1” 
vezes 
return maiorElemento; 1 
int maiorElemento (int [] arr, int n) 
{ 
} 
Observação1: 
 
O “for” inicia “i” com “1” (i=1) e testa a condição (i<n) somente 1 
vez, ou seja, essas duas instruções são executadas 
sequencialmente. Depois, o “for” repete as instruções “i++” e 
“i<n” por “n-1” vezes. 
 
Por exemplo, se n for igual a 5: 
Sequencial : i=1 ; i<5 (i é 1 ; i < 5 => true) 
Repetição 1 : i++ ; i<5 (i é 2 ; i < 5 => true) 
Repetição 2 : i++ ; i<5 (i é 3 ; i < 5 => true) 
Repetição 3 : i++ ; i<5 (i é 4 ; i < 5 => true) 
Repetição 4 : i++ ; i<5 (i é 5 ; i < 5 => false) 
Calculando o custo 
 
No pior caso, tem-se que o maior elemento encontra-se na última 
posição e o array está ordenado: 
 
f(n) = 1 + 2n + n-1 + n-1 + 1 
f(n) = 4n 
Instrução Número de execuções 
int maiorElemento = arr [ 0 ] ; 1 
for (int i = 1; i < n; i++) 2n 
--------------------- 
1ª execução: 
 int i = 1 : 1 
 i < n : 1 
Demais execuções: 
 i++ : n - 1 
 i<n : n - 1 
Resultado: 
 1+1+n-1 +n-1 = 2n 
 if ( arr [ i ] > maiorElemento ) n-1 
--------------------- 
1 instrução “n-1” vezes 
 maiorElemento = arr [ i ] ; 0 
--------------------- 
1 instrução. 
Como a condição é sempre false, a 
instrução nunca será executada. 
return maiorElemento; 1 
Porém, se o elemento 
estiver na primeira 
posição do array, 
a instrução após o “if” 
nunca será executada. 
O custo de execução: 
 
f(n) = 1 + 2n + n-1 + 0 + 1 
f(n) = 4n + 1 
 
É obtido como mostrado 
ao lado. 
Se o “for” iniciar a 
variável “i” com 0, 
o custo de execução 
para o pior caso 
resulta em: 
 
f(n) = 1 + 2n + 2 + n + n + 1 
f(n) = 4n + 4 
 
 
 
Instrução Número de execuções 
int maiorElemento = arr [ 0 ]; 1 
for (int i = 0; i < n; i++) 2n+2 
--------------------- 
1ª execução: 
 int i = 1 : 1 
 i < n : 1 
Demais execuções: 
 i++ : n 
 i<n : n 
Resultado: 
 1+1+n +n = 2n + 2 
 if (arr [ i ] > maiorElemento) n 
 maiorElemento = arr [ i ] ; n 
return maiorElemento; 1 
Definindo-se o tamanho do array (n), obtemos a quantidade de 
operações executadas: 
 
 
 
 
Tanto “4n” como “4n+4” são funções de complexidade de tempo 
do algoritmo. Conforme a quantidade de elementos cresce (tende 
ao infinito), a adição de 4 operações no termo “4n+4” deixa de ser 
relevante. Sendo assim, conforme o tamanho de entrada (n) 
cresce muito, pode-se descartar certos termos na função de 
custo, mantendo-se, apenas, os termos importantes para a 
obtenção desse custo. 
 4n 4n+4 
2 8 12 
100 400 404 
1E10 4E10 4E10 
Vamos fazer outro exemplo. Suponha que se deseja conhecer o 
maior valor de cada linha de um array numérico. Suponha, 
também, tratar-se de uma matriz quadrada (a qual possui o 
número de linhas igual ao número de colunas). 
O programa completo é como mostrado abaixo: 
#include <iostream> 
using namespace std; 
int * maiorElementoPorLinha (int [][3], int n); 
 int main(int argc, char** argv) { 
 int a[3][3], * me; 
 
 a[0][0]=10; 
 a[0][1]=35; 
 a[0][2]=46; 
 a[1][0]=11; 
 a[1][1]=19; 
 a[1][2]=121; 
 a[2][0]=97; 
 a[2][1]=86; 
 a[2][2]=12; 
 me = maiorElementoPorLinha(a,3); 
 for (int i=0; i<3; i++) 
 cout << me[i] << endl; 
 return 0; 
 } 
 
 
 int *maiorElementoPorLinha (int arr[][3], int n) 
 { 
 int *maiorElemento; 
 maiorElemento = new int[n]; 
 
 for (int i=0; i<n; i++) { 
 maiorElemento[i] = arr[i][0]; 
 for (int j=0; j<n; j++) 
 if (arr[i][j] > maiorElemento[i]) 
 maiorElemento[i] = arr[i][j]; 
 } 
 return maiorElemento; 
} 
 
Analisando a função “maiorElementoPorLinha”. 
Novamente, vamos assumir que as instruções primitivas 
possuem o mesmo custo de processamento. 
 
 
 
 
 
 
 
 
 
 
 
Instrução Número de execuções 
maiorElemento = new int [ n ] ; 1 
for (int i = 0; i < n; i++) { 2n+2 
 maiorElemento [ i ] = arr [ i ] [ 0 ] ; n 
 for (int j = 0; j < n; j++) n(2n+2) 
 if ( arr [ i ] [ j ] > maiorElemento [ i ] ) n2 
 maiorElemento [ i ] = arr [ i ] [ j ] ; n2 
} 
return maiorElemento; 1 
int *maiorElementoPorLinha (int arr[][3], int n) 
{ 
 
} 
No pior caso (no qual a condição do “if” é sempre true): 
f(n)=1+2n+2+n+2n2+2n+n2+n2+1=4n2+5n+4 
 
No melhor caso (no qual a condição do “if” é sempre false): 
f(n)=1+2n+2+n+2n2+2n+n2+1=3n2+5n+4 
Vamos, agora, somar todos os elementos que estão abaixo da 
diagonal principal de uma matriz, para exemplificar os casos em 
que o “for” interno tem por limite o valor da variável de controle 
do “for” “externo”. 
Os elementos da Diagonal Principal (DP) estão em células cujas 
coordenadas são iguais, ou seja, o “i” é igual ao “j”, como 
destacado abaixo: 
 
(0,0) (0,1) (0,2) (0,3) 
(1,0) (1,1) (1,2) (1,3) 
(2,0) (2,1) (2,2) (2,3) 
(3,0) (3,1) (3,2) (3,3) 
Assim sendo, para somar todos os elementos abaixo da diagonal 
principal de uma matriz, devemos somar todos os elementos 
“mat (i,j)” cujo valor de “j” seja menor do que o valor de “i”: 
 
(0,0) (0,1) (0,2) (0,3) 
(1,0) (1,1) (1,2) (1,3) 
(2,0) (2,1) (2,2) (2,3) 
(3,0) (3,1) (3,2) (3,3) 
Seja a matriz 4 x 4: 
 
 
 
 
 
 
 
 
A soma dos elementos abaixo da DP é 66 (5 + 9 + 10 + 13 + 14 + 15). 
1 2 3 4 
5 6 7 8 
9 10 11 12 
13 14 15 16 
Vamos criar um programa para obter essa soma, cujo código é 
como segue: 
 
#include <iostream> 
using namespace std; 
const int n = 4; 
int somaElementosAbaixoDaDP (int [ ] [ n ] ) ; 
int main(int argc, char** argv) { 
 int mat [ n ] [ n ] ; 
 mat[0][0] = 1; mat[0][1] = 2; mat[0][2] = 3; mat[0][3] = 4; 
 mat[1][0] = 5; mat[1][1] = 6; mat[1][2] = 7; mat[1][3] = 8; 
 mat[2][0] = 9; mat[2][1] = 10; mat[2][2] = 11; mat[2][3] = 12; 
 mat[3][0] = 13; mat[3][1] = 14; mat[3][2] = 15; mat[3][3] = 16; 
 cout <<"S= " << somaElementosAbaixoDaDP (mat) << endl; 
 
 return 0; 
} 
int somaElementosAbaixoDaDP (int mat [ ] [ n ] ) 
{ 
 int s = 0 ; 
 for (int i = 0 ; i < n ; i++) 
 for (int j = 0 ; j < i ; j++) 
 s += mat [ i ] [ j ] ; 
 return s; 
} 
Vamos analisar somaElementosAbaixoDaDP, função que realiza 
tal procedimento. Assuma que as instruções primitivas possuem 
o mesmo custo de processamento. 
Como o “for j” tem seu limite superior alterado a cada passo do 
“for i”, ele executará um número variável de vezes e, em 
consequência, a linha de comando executada por “for j” também 
terá um número variável de execuções. 
Vamos fazer o “computador chinês” para descobrir a quantidade 
de vezes que o laço “for (j=0; j<i; j++)” e a linha de comando 
“s += mat[i][j];” são executados. 
 
 
 
 
 
 
 
No “for j” tem-se uma PA de razão 1: 
PA: 1 . 2 . 3 ... n 
i j for (j=0; j<i; j++) s += mat[i][j]; 
0 0..0 1 0 
1 0..1 2 1 
2 0..2 3 2 
... ... ... ... 
n-1 0..n-1 n n-1 
A soma desta PA dá: n(1+n)/2 execuções de laço. Porém, esta 
soma deve ser multiplicada por dois, pois o “for” possui duas 
operações (“i++” e “i<n”). Então, o número total de operações 
executadas pelo for é: 2n(1+n)/2 = n(1+n) = n + n2. 
A soma ”s += mat[i][j];” trata-se, igualmente, de uma PA, cujo 
somatório é: n(0 + n-1)/2 = (n2 - n)/2. 
 
Instrução Número de execuções 
int s = 0; 1 
for (int i = 0 ; i < n ; i++) 2n+2 
 for (int j = 0 ; j < n ; j++) n + n2 
 s += mat [ I ] [ j ] ; (n2 - n)/2 
return s; 1 
int somaElementosAbaixoDaDP (int arr[][n]) 
{ 
} 
Aqui, o melhor caso é igual ao pior caso, e o custo de execução 
do algoritmo é: 
f(n) = 1 + 2n + 2 + n + n2 + (n2 - n) + 1 
 
 
f(n) = 2 + 4n + 4 + 2n + 2n2 + n2 - n + 2 
 
 
f(n) = 3n2 + 5n + 8 
2 
2 
2 
Notação O 
 
A análise da complexidade de um algoritmo permite projetar 
algoritmos mais eficientes, ou, tendo duas soluções para o 
mesmo problema, pode-se calcular o custo de cada uma delas e 
escolher a de menor custo computacional. 
Por exemplo, sejam dois algoritmos para somar os elementos de 
um array. A primeira solução, utilizando um array bidimensional. 
A segunda solução, utilizando um array unidimensional. 
Solução 1: array bidimensional 
 
s = 0; // custo = 1 
for (int i=0; i<n; i++) // custo = 2+2n 
 for (int j=0; j<n; j++) // custo = n(2+2n) 
 s += mat[i][j]; // custo = n 
 
Custo: 1 + 2 + 2n + 2n + 2n2 + n = 2n2 + 5n + 3 
 
Solução 2: array unidimensional 
 
s = 0; // custo = 1 
for (int i=0; i<n; i++) // custo = 2+2n 
 for (int j=0; j<n; j++) // custo = n(2+2n) 
 s += mat[n*i + j]; // custo = 3n 
 
// operação 1: multiplicação (n*i) 
// operação 2: soma com j 
// operação 3: soma e atribuição a “s” 
// as três operações são executadas “n” vezes 
 
Custo: 1 + 2 + 2n + 2n + 2n2 + 3n = 2n2 + 7n + 3 
A primeira solução tem um custo menor do que a segunda 
solução. 
Em ambas, o melhor caso é igual ao pior caso. 
A análise da complexidade de um algoritmo, então, nos permite 
projetar algoritmos mais eficientes. Porém, devemos sempre 
levar em consideração, o tamanho “n” dos dados de entrada, 
pois nem sempre um algoritmo A que resolva o mesmo problema 
que um algoritmo B, executará menos operações do que o 
segundo. 
Por exemplo, sejam as funções: 
Algoritmo A: f(n) = n + 10000 
Algoritmo B: g(n) = 2n2 + 3n 
 
 n = 10 n = 100 n = 1000 
Algoritmo A 10010 10100 11000 
Algoritmo B 230 20300 2003000 
Dessa forma, para grandes quantidades de dados deve-se 
preferir o algoritmo A, pois ele tem desempenho melhor que B. 
A partir das funções estudadas anteriormente, obtém-se os 
vários tempos de execução para uma determinada configuração 
de máquina. Se, por hipótese, uma operação primitiva levar 1 ms 
para sua execução, tem-se os vários tempos de execução (em 
segundos), em função da taxa de crescimento da função: 
 
n f(n) = n f(n) = log(n) f(n)=nlog(n) f(n) = n2 f(n)=n3 f(n) = 2n 
10 0,01 0,003 0,033 0,1 1 1,024 
100 0,1 0,007 0,664 10 16,667 1,27E+27 
1000 1 0,01 9,966 1,00E+03 1,00E+06 1,10E+298 
Observe que para uma população de 1000 indivíduos, um 
algoritmo O(2n) leva 1,10E+298 segundos, ou 10288 séculos, para 
processar os dados, ao passo que um algoritmo O(nlog(n)) leva 
aproximadamente 10s para obter a solução do mesmo problema 
e um algoritmo O(n2) leva 17 minutos (aproximadamente). 
Conforme n tende ao infinito (n->∞) tem-se que o comportamento 
da função é chamado de assintótico, e os termos inferiores do 
polinômio que representam a função, bem como as constantes 
multiplicativas tornam-se irrelevantes, por contribuírem muito 
pouco na comparação entre os algoritmos e podem ser 
descartados. 
Tomando-se como exemplo os algoritmos anteriores, o 
importante é observar que f(n) tem crescimento linear, ao passo 
que g(n) tem crescimento quadrático. 
Dizemos que o algoritmo A é da ordem O(n) e o algoritmo B é da 
ordem O(n2). 
Pelo exposto, não faria sentido dizer que A é O(n + 10000) ou que 
B é O(2n2 + 3n). 
 
Isto posto, se f(n) é um polinômio de grau “d”, isto é: 
f(n) = ao + a1n + a2n
2 + ... + adn
d 
então, f(n) é O(d). 
 
Outros exemplos: 
O(n) : 4 n + 1000 
O(n2) : 3 n2 + n log n + 3 
O(2n) : 2n+3 + 3 n 
 
Uma função constante tem somente o primeiro termo do 
polinômio (n0). Diz-se, neste caso, que uma função constante tem 
ordem O(1), pois todo número elevado a zero é igual a 1. 
Assim sendo, dada uma função g(n) que mapeia inteiros não 
negativos em números reais, denotamos por O(g(n)) o conjunto 
de funções f(n), onde: 
0 ≤ f(n) ≤ cg(n) 
sendo “c” uma constante real maior que zero (c > 0) e “no“ uma 
constante inteira maior ou igual a um (no ≥ 1), para todo inteiro 
n ≥ no. 
Desta forma, dizemos que f(n) é O de g(n). 
E a representação gráfica é como: 
cg(n) 
f(n) 
no Tamanho da base (n) 
tempo de 
execução 
Tem-se, neste caso, que f(n) cresce a 
uma taxa proporcional à g(n). Podemos 
dizer que f(n) é de complexidade g(n), ou, 
simplesmente, que f(n) é O de g(n). 
A função g expressa um limite superior 
para valores assintóticos de f. Assim 
sendo, O(g(n)) tem como valor uma 
quantidade muito grande, não 
explicitamente definida, que não excede 
“c * g(n)”, para um “n” muito grande 
(n>no), sendo no o ponto a partir do qual 
f(n) é menor que g(n). 
 
cg(n) 
f(n) 
no n 
t 
Regras para Cálculo de Complexidade 
 
 
f(n) = O(f(n)) 
c * O(f(n)) = O(f(n)) * c 
O(f(n)) + O(f(n)) = O(f(n)) 
O(O(f(n))) = O(f(n)) 
O(f(n)) + O(g(n)) = O(max(f(n), g(n))) 
O(f(n)) * O(g(n)) = O(f(n) * g(n)) 
f(n) * O(g(n)) = O(f(n) * g(n)) 
Assim sendo, a complexidade de um algoritmo será dada pela 
maior das complexidades encontradas. Por exemplo, o trecho 
abaixo tem complexidade O(n2): 
 
cin >> a; 
cin >> b; 
s = 0; 
if (a > b) 
 for(i = 0; i < n; i++) // O(n) 
 s += i; 
else 
 for(i = 0; i < n; i++) // O(n2) 
 for(j = 0; j < n; j++) 
 s += i*j; 
O(max(O(n),O(n2))) = O(n2) 
Outro exemplo: Multiplicação de matrizes 
 
Sejam duas matrizes “A” e “B”, ambas 3x3: 
a(0,0) a(0,1) a(0,2) 
a(1,0) a(1,1) a(1,2) 
a(2,0) a(2,1) a(2,2) 
A = 
b(0,0) b(0,1) b(0,2) 
b(1,0) b(1,1) b(1,2) 
b(2,0) b(2,1) b(2,2) 
B = 
a(0,0) a(0,1) a(0,2) 
a(1,0) a(1,1) a(1,2) 
a(2,0) a(2,1) a(2,2) b(0,0) b(0,1) b(0,2) 
b(1,0) b(1,1) b(1,2) 
b(2,0) b(2,1) b(2,2) 
Obtém-se a multiplicaçãodas matrizes pela soma das 
multiplicações dos elementos de cada da linha de “A” pelos 
elementos de cada coluna de “B”: 
A solução tem complexidade O(n3): 
 
for(i = 0; i < n; i++) // O(n2) 
 for(j = 0; j < n; j++) 
 c[i][j] = 0; 
for(i = 0; i < n; i++) // O(n3) 
 for(j = 0; j < n; j++) 
 for(k = 0; k < n; k++) 
 c[i][j] += a[i][k] * b[k][j]; 
 
 
 
 
O(max(O(n2),O(n3))) = O(n3)

Continue navegando