Você está na página 1de 12

UNIVERSIDADE S.

TOMÁS DE MOÇAMBIQUE
CURSO DE TECNOLOGIAS DE SISTEMAS DE
INFORMAÇÃO
Estrutura de Dados e Algoritmos
Ano 2007

Aula 11: Complexidade Algorítmica

Conteúdo

1. Conceito de Complexidade Algorítmica


1. 1 Avaliação de Algoritmos
2. Notações O-Grande, Omega e Theta
2.1 Notação O-Grande
2.2 Notação Ómega
2.3 Notação Theta
3. Crescimento Assimptótico de Funções
3. 1 Principais Classes de Complexidade
3. 2 Análise da Complexidade de Algoritmos
3. 3 Regras para a Análise de Algoritmos
4. Exercícios práticos

1
1. Conceito de Complexidade Algorítmica

Diferentes algoritmos podem ser construídos para resolver um determinado problema.


Esses algoritmos podem variar na forma de buscar os dados de entrada, processar e
imprimir os dados de saída. Assim sendo, estes podem ter diferenças significativas em
termos de performance e utilização de espaço. Entender como analisar algoritmos e como
medir a eficiência algorítmica ajuda bastante na escolha do melhor algoritmo a ser usado
(melhor optimizado).

No entanto, para desenvolver algoritmos realmente eficientes, não basta conhecer


técnicas e alternativas para problemas comuns. O programador deve ter a capacidade de
prever, ao desenvolver um algoritmo, qual será o seu comportamento, quer com poucos
ou com muitos dados de entrada.

A tentativa de prever o comportamento do algoritmo consiste em avaliar sua


complexidade. Para isso, são feitos cálculos, que podem ser simples ou complexos.
Neste contexto teremos como objectivos para análise de algoritmos:
 Avaliar um algoritmo para verificar se ele é eficiente ou não,
 Verificar se um algoritmo é correcto ou incorrecto,
 Comparar vários algoritmos (que resolvem o mesmo problema) para decidir qual
deles é o mais eficiente.

Exemplo: Suponhamos que nos seja colocado um problema para encontrar o maior e o
menor elemento dum array com n elementos, e, que tenhamos como dado de entrada o
referido array.

Resolução:
Primeiro é necessário determinar as instâncias de execução do algoritmo. Neste caso
teremos:

2
Instâncias de Execução de Algoritmos:
Uma instância de um problema consiste de um conjunto de dados de entrada e saída
utilizados durante uma única execução.
Por exemplo, abaixo temos duas diferentes instâncias de execução do mesmo algoritmo,
cujo problema foi especificado acima.

Problema: Encontrar o maior e o menor valor de um vector com n elementos.


Entrada: {1,34,56,32,3,54,3,356,3,2,23,78,65,32,11,1,43,356,66}
Saída:
Menor valor = 1
Maior valor = 356

Problema: Encontrar o maior e o menor valor de um vector com n elementos.


Entrada: {2,54,67,93,54,23,345,67,42,447,4,983,10,76,31,15,57,83,45,794,346,44}
Saída:
Menor valor = 2
Maior valor = 983

A partir do exemplo acima, podemos verificar que em diferentes instâncias de execução,


os dados de entrada podem ser bastante variados, podendo inclusive ter uma grande
variação de volume. Ou seja, na instância apresentada no primeiro quadro o array de
entrada tem 19 valores, enquanto na instância apresentada no segundo quadro o array de
entrada tem 22. Da mesma forma, numa outra instância é possível que se tenha 500
elementos de entrada.
A questão que se levanta então é: dado um algoritmo que funciona de forma eficiente
para 10 elementos, ele continuará funcionando de forma eficiente para 10.000 ou mais?
O algoritmo deve trabalhar correctamente sobre todas as instâncias para as quais foi
projectado para solver. Portanto, um algoritmo para o qual é possível achar uma instância
de dados de entrada para a qual ele não consegue achar uma resposta correcta é um
algoritmo incorrecto. No entanto, provar o contrário, que o algoritmo é correcto para
qualquer instância, não é tão simples. Para isso, o algoritmo deve ser avaliado utilizando
alguns critérios.

3
Deste modo, passaremos a ilustrar a avaliação do algoritmo.

1.1 Avaliação de Algoritmos

Algoritmos podem ser avaliados utilizando-se vários critérios. Geralmente o que interessa
é a taxa de crescimento ou espaço necessário para resolver instâncias cada vez maiores de
um problema. Podemos associar um problema a um valor chamado de ‘tamanho’ do
problema, que mede a quantidade de dados de entrada.
O tempo que um algoritmo necessita, expresso como uma função do tamanho do
problema, é chamado de complexidade temporal do algoritmo. O comportamento
assimptótico dos algoritmos (ou funções) representa o limite do comportamento de custo
quando o tamanho cresce. O comportamento assimptótico pode ser definido como o
comportamento de um algoritmo para grandes volumes de dados de entrada.

A complexidade temporal de um algoritmo pode ser dividida em 3 aspectos:


 Melhor caso – o melhor caso representa uma instância que faz o algoritmo
executar utilizando o menor tempo possível.
 Pior caso – o maior tempo demorado pelo algoritmo para executar alguma
instância.
 Caso médio – a média de tempo que o algoritmo demora para executar.

Geralmente, o mais importante é avaliar o pior caso (porque pode inviabilizar o


algoritmo) e o caso médio, porque representa como o programa vai se comportar, na
prática, na maioria dos casos.

Tipos de Avaliação de Algoritmos


A avaliação de algoritmos divide-se em dois tipos a saber:

 Avaliação Empírica: é a forma mais simples de se avaliar um algoritmo e consiste


implementá-lo num computador e executá-lo com várias instâncias do problema.
Define-se então um critério para a eficiência, como por exemplo o tempo gasto para

4
execução. Com base na observação, pode-se calcular o pior caso (a instância de
execução que levou mais tempo), o melhor caso (a instância de execução que
gastou menos tempo) e o caso médio (a média do tempo gasto em todas as
instâncias de execução).
O problema com esse tipo de avaliação é que o tempo gasto vai depender do
computador utilizado, do compilador, da linguagem de programação, etc.

 Avaliação Teórica: é aquela em que consiste em encontrar uma fórmula


matemática que expresse o recurso, por exemplo o tempo necessário para o
algoritmo executar em função do tamanho dos dados de entrada.
Existem algumas notações predefinidas usadas para comparar diferentes funções:

2 Notações O-grande, Omega e Theta


Após obter a função que representa o comportamento de um software, basta analisá-la
para termos uma medida da complexidade do software. Para isso utiliza-se as 3 notações
a seguir: O-grande, Omega e Theta. Elas são utilizadas também para comparar funções de
diferentes algoritmos.

2.1 Notação O-Grande


Esta notação é utilizada para analisar o Caso Pior.
Uma função f(n) é O(g(n)) se  c>0 e n0 tais que f(n) <= c*g(n) para n>=n0.

Explicação: uma função f(n) é da ordem de complexidade de g(n) e escreve-se f(n) =


O(g(n)) se existe uma constante c e um valor n0 tal que para qualquer valor de n maior do
que n0 f(n) é menor ou igual a c*g(n).
Isso significa que:
 g(n) é um limite superior para f(n)
 f(n) denomina assimptoticamente f(n)

5
Propriedades:
 f(n) = O(f(n))
 c. f(n) = O(f(n)), c=constante
 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))

2.2 Notação Omega

Esta notação é utilizada para analisar o melhor caso.


Uma função f(n) é (g(n)) se  c>0 e n0 tais que f(n) >= c*g(n) para n>=n0.

Explicação: uma função f(n) é da ordem de complexidade de g(n) e escreve-se f(n) =


(g(n)) se existe uma constante c e um valor n0 tal que qualquer valor de n maior do que
n0, f(n) é maior ou igual a c*g(n).
Isso significa que:
 g(n) é um limite inferior para f(n)

2.3 Notação Theta

Esta notação é utilizada para analisar o caso médio.


Uma função f(n) é (g(n)) se  c1, c2 e n0 tais que c1. g(n) <= f(n) <= c2*g(n) para
n>=n0.

Explicação: uma função f(n) é da ordem de complexidade de g(n) e escreve-se f(n) =


(g(n)) se existem as constantes c1 e c2 e um valor n0 tal que qualquer valor de n maior
do que n0, f(n) é maior ou igual a c1*g(n) e é menor ou igual a c2*g(n)

6
Dentre as notações existentes, a notação O (O-grande) é a mais utilizada porque
geralmente o mais importante é descobrir o pior caso para saber se existe alguma
possibilidade de o algoritmo falhar, isto é, não conseguir executar caso entre com um
volume de dados demasiado grande.

Fazendo uma analogia entre as notações para melhor compreensão das notações, temos o
seguinte, considerando que f e g são funções que representam o comportamento de dois
algoritmos diferentes.

f(n) = O (g(n))  f<=g


f(n) =  (g(n))  f>=g
f(n) =  (g(n))  f=g

3 Crescimento assimptótico de funções

A complexidade assimptótica de um algoritmo geralmente determina o tamanho dos


problemas que poderão ser resolvidos por esse algoritmo. Assim, se um algoritmo
processa dados de tamanho n num tempo c*n2 para uma constante c, então dizemos que a
complexidade temporal do algoritmo é O(n2) (leia-se: da ordem de n2).
Por exemplo, considere os cinco algoritmos abaixo:

Algoritmo Complexidade
A1 n
A2 n log n
A3 n2
A4 n3
A5 2n

A complexidade de tempo é o número de unidades de tempo necessárias para processar


uma entrada de tamanho n. Assim, assumindo por exemplo a unidade de tempo como
sendo 1 millisegundo, o algoritmo A1 precisa, para processar 1000 entradas, de 1000
millisegundos, ou seja, 1 segundo. Já o algoritmo A3 precisa de 1 milhão de

7
millisegundos. Enquanto o A1 consegue processar 1000 entradas em um segundo, o A5
só consegue processar 9 entradas em 1 segundo.

3.1 Principais Classes de Complexidade

A tabela a seguir mostra as principais classes de complexidade de funções. Quanto mais


abaixo na tabela, mais complexo é o algoritmo que a função representa.

Função Nome
1 Constante
log n Logarítmica
log2 n Log quadrático
n Linear
n log n n log n
n2 Quadrática
n3 Cúbica
2n exponencial

3.2 Análise da Complexidade de Algoritmos


Sabemos que os principais recursos requeridos por um software em execução são: espaço
em disco, espaço em memória e tempo. Mas enquanto o espaço é uma questão que pode
ser facilmente resolvida, o tempo que um programa leva para ser executado pode
inviabilizar o seu uso. Portanto, vamos focalizar na estimativa de tempo de execução.

Já vimos que o mais importante é tentar avaliar o tempo de execução para grandes
volumes de dados de entrada. Para isso, vamos tentar achar uma fórmula que expresse o
tempo de execução em função do volume de dados de entrada. Dependendo da fórmula
encontrada, podemos classificar e comparar a complexidade de diferentes algoritmos.

8
3.3 Regras para Análise de Algoritmos

Para diferentes tipos de instruções, existem regras específicas. Nos exemplos abaixo
ilustram-se diferentes casos:

1. Atribuições simples, declarações, etc.


Analisemos os exemplos dos seguintes comandos:

1 int x=1;
2 x= (1+y);
3 System.Out.print(“Digite um número”);

Podemos facilmente notar que qualquer desses comandos, a não ser que esteja dentro de
um ciclo, será executado uma única vez. Ou seja, são comandos que não dependem do
volume de dados de entrada. Portanto, dizemos que esses comandos têm ordem de
complexidade constante ou O(1).

2. Ciclos for, while, do-while


O tempo de execução dum ciclo é, no máximo, a soma dos tempos de execução de todas
as instruções dentro do ciclo (incluindo todos os testes) multiplicado pelo número de
iterações.
Por exemplo, considere o ciclo a seguir:
1 for (i=1;i<=10;i++)
2 {
3 vetor[i]=i; O(1)
4 vetor[i+1]=x; O(1)
5 }
Para encontrar a complexidade desse trecho de código, devemos inicialmente somar o
tempo de execução de todas as instruções dentro do ciclo. Temos portanto:
O(1) + O(1) = O(1+1) = O(2).

9
Agora multiplicamos esse valor pelo número de iterações do ciclo. Como o ciclo vai de 1
até 10, o número de iterações é 10. Portanto, temos: O(10) *O(2) = O(10*2) = O(20). Ou
seja, esse trecho de código vai demorar 20 unidades de tempo para ser executado. Como
o tempo é constante, isto é, independente do volume de dados de entrada, poderíamos
simplificar ainda mais e dizer que a complexidade é simplesmente O(1).

3. Ciclos aninhados
Devem ser analisados de dentro para fora. O tempo total de execução de uma instrução
dentro de um grupo de ciclos aninhados é o tempo de execução da instrução multiplicado
pelo produto do tamanho de todos os ciclos.
Exemplo:
1 for(i=1;i<n;i++)
2 {
3 for(j=1;j<n,j++)
4 {
5 k= k+1; O(1)
6 }
7 }

Neste caso, só temos um comando propriamente dito, que é constante e, portanto, tem
ordem O(1). Esse valor corresponde ao tempo total de execução dentro do grupo de
ciclos. Agora é necessário, portanto, multiplicá-lo pelo produto do tamanho de todos os
ciclos.
O primeiro ciclo será executado n vezes e, portanto, tem tamanho n. O mesmo vale para o
segundo ciclo, que também será executado n vezes. Portanto, temos:
O(n) * O(n) = O(n * n) = O(n2)
O(1) * O(n2) = O(1 *n2) = O(n2)

4. Instruções consecutivas
Simplesmente soma-se a complexidade de cada instrução, sendo que os termos de menor
ordem são ignorados.

10
Exemplo:
1 for(i=1;i<n; i++){
2 a[i] = 0;
3 }
4 for (i=1;i<n; i++) {
5 for(j = 1;j<n; j++) {
6 a[i] = a[j] + 1;
7 }
8 }

Se calcularmos, veremos que a ordem de complexidade do primeiro ciclo é O(n),


enquanto a ordem de complexidade do segundo ciclo é O(n2). Portanto, como os dois
ciclos são consecutivos e independentes, simplesmente soma-se a complexidade de cada
instrução. Assim, a equação que descreve a complexidade desse trecho de código seria n 2
+n. No entanto, como sabemos que o mais importante é o maior tempo, podemos
simplificar essa fórmula. Vimos nas propriedades, que a soma de duas complexidades é o
maior valor. Portanto:
O(n) + O(n2) = O(max(n, n2)) = O(n2)

5. Ifs
Considere o seguinte exemplo:
1 if condição
2 {expressão1;}
3 else
4 {expressão2;}

O tempo de execução de um comando do tipo if-else nunca é maior do que o tempo de


execução do teste condicional em si mais o tempo de execução da maior entre as
expressões 1 e 2. Assim, se a expressão1 é O(n3) e a expressão 2 é O(n), então a
complexidade é O(n3) + O(n) = O(n3).

11
4. Exercícios
1. Faça a análise algorítmica das instruções que se seguem e determine a eficiência de
cada código:

int resultado = a[0];


int i = 1;
while (i < n)
resultado = resultado + a[i];

2. Determine a ordem complexa para o algoritmo que se segue:

int i, j, k, count = 0;
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
for (k = 0; k < n; k++)
count++;

3. Calcule a complexidade para algoritmo que se segue:

for (int i = 1; i < n; i++)


{
min = i;
for (int j = i; j< m; j++)
{
if a[j] < a[min]
{
min = j;
x = a[min];
a[min] = a[i];
a[i] = x;
}
}
}

________________________________________________________________________
Docentes regentes:
Rossana Abdul Carimo
Hamilton Isaias Mutaquiha

12

Você também pode gostar