Vous êtes sur la page 1sur 31

ESTRUTURA DE

DADOS E ALGORÍTMOS

Prof. Doutor Manuel Zunguze


INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS

Algoritmo: sequência de instruções necessárias para a resolução de


problemas passíveis de implementação em computador.

A complexidade (custo) de um algoritmo visa:


• Comparar algoritmos: algoritmos que resolvem o mesmo tipo de
problema;
• Determinar se o algoritmo é “ótimo”: os algoritmos apresentam um
limite inferior para a sua complexidade (se a complexidade é ótima
então está abaixo deste limite).
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Um algoritmo serve para resolver um determinado problema, e todos os
problemas têm sempre uma entrada de dados. O tamanho dessa entrada
(N) tem geralmente efeito direto no tempo de resposta de um algoritmo.
Dependendo do problema a ser resolvido, existem algoritmos prontos
ou que podem ser adaptados.
O problema é qual algoritmo escolher?

Estratégia:
• Especificar (definir propriedades);
• Arquitectura (algoritmo e estruturas de dados);
• Análise de complexidade (tempo de execução e memória);
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
• Implementar (numa linguagem de programação);
• Testar (submeter entradas e verificar observância das propriedades
especificadas).

Existem dois tipos de complexidade de algoritmos:


• Complexidade Espacial: Quantidade de recursos utilizados para
resolver o problema. Espaço de memória que necessita para
executar até ao fim.
S(n) : espaço de memória exigido em função do tamanho n da entrada.
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
• Complexidade Temporal: Quantidade de tempo utilizado para
executar um programa ou algoritmo (tempo de execução).
T(n) : tempo de execução em função do tamanho n da entrada.
A complexidade temporal pode ser vista também como o número de
instruções necessárias para resolver um determinado problema.
Em ambos os casos (complexidade temporal e espacial), a
complexidade é medida de acordo com o tamanho dos dados de
entrada (n).

Complexidade  vs. Eficiência 


Por vezes estima-se a complexidade para o “melhor caso” (pouco útil),
“pior caso” (mais útil) e o “caso médio” (igualmente útil).
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Complexidade temporal
• Uma análise precisa é uma tarefa complicada. Um algoritmo é
implementado numa dada linguagem. A linguagem é compilada e o
programa é executado num dado computador o que torna difícil
prever tempos de execução de cada instrução e antever optimizações.
Muitos algoritmos são “sensíveis” aos dados de entrada e não são
bem compreendidos.
• Para prever o tempo de execução de um programa apenas é
necessário um pequeno conjunto de ferramentas matemáticas.
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Crescimento de funções
O tempo de execução geralmente dependente de um único parâmetro n.
• ordem de um polinómio;
• tamanho de um ficheiro a ser processado, ordenado, etc.;
• ou medida abstracta do tamanho do problema a considerar
(usualmente relacionado com o número de dados a processar)
Quando há mais do que um parâmetro
• procura-se exprimir todos os parâmetros em função de um só;
• faz-se uma análise em separado para cada parâmetro.
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Ordens de complexidade mais comuns
Os Algoritmos têm tempo de execução proporcional a:
• 1 : quando muitas instruções são executadas uma só vez ou poucas
vezes (se isto acontecer para todo o programa diz-se que o seu tempo
de execução é constante);
• Log n : quando o tempo de execução é logarítmico (cresce
ligeiramente à medida que n cresce; quando n duplica log n aumenta
mas muito pouco; apenas duplica quando n aumenta para n2);
• n : quando o tempo de execução é linear (típico quando algum
processamento é feito para cada dado de entrada; situação óptima
quando é necessário processar n dados de entrada, ou produzir n
dados na saída)
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Ordens de complexidade mais comuns
• n log n : típico quando se reduz um problema em subproblemas, se
resolve estes separadamente e se combinam as soluções (se n é igual
a 1 milhão, n log n é perto de 20 milhões);
• n2 : tempo de execução quadrático (típico quando é necessário
processar todos os pares de dados de entrada; prático apenas em
pequenos problemas, ex: produto de vectores);
• n3 : tempo de execução cúbico (para n = 100, n3 = 1 milhão, ex:
produto de matrizes);
• 2n : tempo de execução exponencial (tem pouca aplicação prática;
típico em soluções de força bruta; para n = 20, 2n = 1 milhão; se n
duplica, o tempo passa a ser o quadrado).
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Ordens de complexidade mais comuns
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Análise de complexidade
Existem três perspectivas para análise de complexidade:
• Melhor Caso (Ω)
• Caso Médio (θ)
• Pior Caso (O)
Nas três perspectivas, a função f(n) retorna a complexidade de um
algoritmo com entrada de tamanho n.
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Análise de complexidade
Análise do melhor caso:
• Definido pela letra grega Ω(Ômega);
• Exprime o menor tempo de execução de um algoritmo para uma
entrada de tamanho n;
• É pouco usado, por ter aplicação em poucos casos. Ex.:
• O algoritmo de pesquisa sequêncial em um vetor tem
complexidade f(n) = Ω(1);
• A análise assume que o número procurado seria o primeiro
selecionado na lista.
• Abordagem otimista!
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Análise de complexidade
Análise do caso médio:
• Definido pela letra grega θ (Theta);
• Deve-se obter a média dos tempos de execução de todas as entradas
de tamanho n, ou baseado em probabilidade de determinada
condição ocorrer. Ex.:
• O algoritmo de pesquisa sequêncial em um vector tem
complexidade f(n) = θ(n/2);
• Em média será necessário visitar n/2 elementos do vector até
encontrar o elemento procurado.
• Melhor aproximação;
• Muito difícil de determinar na maioria dos casos.
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Análise de complexidade
Análise do pior caso:
• Representado pela letra grega O (Big O). "Grande O" maiúsculo.
Trata-se da letra grega ômicron maiúscula.
• Baseia-se no maior tempo de execução sobre todas as entradas de
tamanho n (ordem de grandeza);
• É o método mais fácil de se obter (o mais usado).Ex.:
• O algoritmo de pesquisa sequêncial em um vetor tem
complexidade f(n) = O(n);
• No pior caso será necessário visitar todos os n elementos do
vetor até encontrar o elemento procurado.
• Abordagem pessimista!
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Notação de “O grande”
Na prática, é difícil (senão impossível) prever com rigor o tempo de
execução de um algoritmo ou programa.
• Para obter o tempo se usam:
• constantes multiplicativas (normalmente estas constantes são
tempos de execução de operações atómicas);
• parcelas menos significativas para valores grandes de n.
• Identificam-se as operações dominantes (mais frequentes ou muito
mais demoradas) e determina-se o número de vezes que são
executadas (e não o tempo de cada execução, que seria uma constante
multiplicativa).
• Exprime-se o resultado com a notação de “O grande”
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Definição de Notação de “O grande”:
• T(n) = O(f(n)) (lê-se: T(n) é de ordem f(n)) se e só se existem
constantes positivas c e n0 tal que T(n) ≤ c.f(n) para todo o n > n0.
• Esta notação é usada com três objectivos:
• limitar o erro que é feito ao ignorar os termos menores nas
fórmulas matemáticas;
• limitar o erro que é feito na análise ao desprezar parte do
programa que contribui de forma mínima para o
custo/complexidade total;
• permitir-nos classificar algoritmos de acordo com limites
superiores no seu tempo de execução.
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Exemplo:
• ck nk + ck-1 nk-1 +...+ c0 = O(nk)
(ci - constantes)
• log2 n = O(log n)
(não se indica a base porque mudar de base é multiplicar por uma
constante);
• 4 = O(1)
(usa-se 1 para ordem constante)
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Metodologia para determinar a complexidade
Exemplo 1:
Considere-se o seguinte código:
for (i = 0; i < n; i++)
{
Instruções;
}
A contabilização do número de instruções é:
• n iterações e, em cada uma, são executadas um número constante de
instruções  O(n)
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Exemplo 2:
Considere-se o seguinte código:
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
{
Instruções;
}
A contabilização do número de instruções é ainda simples:
• o ciclo interno (for j) é O(n) e é executado n vezes  O(n2)
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Eficiência da Pesquisa Sequencial
int PesquisaSequencial(int x, int V[], int n)
{
int i = 0, k = -1; // k = posição onde se encontra x em V
while ((i < n) && (k == -1))
if (x == V[i])
k = i;
else
if (V[i] < x)
i = i + 1;
else
k = -2;
return (k);
}
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Eficiência da Pesquisa Sequencial
Eficiência temporal:
• A operação realizada mais vezes é o teste da condição de
continuidade do ciclo while, que é no máximo n+1 vezes (no caso
de não encontrar x — o pior caso);
• Se x existir no array, o teste é realizado aproximadamente
• n/2 vezes (no caso médio) e;
• 1 vez (no melhor caso)

Logo, T(n) = O(n) (linear) no pior caso e no caso médio


INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Eficiência da Pesquisa Sequencial
Eficiência espacial:
• Gasta o espaço das variáveis locais (incluindo argumentos);
• Como os arrays são passados “por referência” (na linguagem C, o
que é passado é o endereço do primeiro elemento do array), o
espaço ocupado pelas variáveis locais é constante e independente do
tamanho do array.
Logo, S(n) = O(1) (constante) em qualquer caso
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Eficiência da Pesquisa Binária
int PesquisaBinaria (int x, int V[],int n)
{
int inicio = 0, fim = n – 1, meio, k = -1;
while ((inicio <= fim) && (k == -1))
{
meio = (inicio + fim)/2;
if (x == V[meio])
k = meio;
else
if (x < V[meio])
fim = meio - 1;
else
inicio = meio + 1;
}
return (k);
}
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Eficiência da Pesquisa Binária
Eficiência temporal:
• Em cada iteração, o tamanho do sub-array a analisar é dividido por
um factor de aproximadamente igual a 2;
• Ao fim de k iterações, o tamanho do sub-array a analisar é
aproximadamente igual a n/2k;
• Se não existir no array o valor procurado, o ciclo só termina quando
n/2k  1  log2 n – k  0  k  log2 n;
• No pior caso, o número de iterações é aproximadamente igual a log2
n. Logo, T(n) = O(log n) (logarítmico).
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Eficiência da Ordenação por Borbulagem (Bubblesort)
void Ordenar_Borbulagem (int V[], int n) {
int k, Num_trocas, aux;
do{
Num_trocas = 0;
for (k = 0; k < n-1; k++)
if (V[k] > V[k+1])
{
aux = V[k];
V[k] = V[k+1];
V[k+1] = aux;
Num_trocas++;
}
}while (Num_trocas != 0);
}
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Eficiência da Ordenação por Borbulagem
• No melhor caso:
• Apenas 1 execução do ciclo for : n-1 comparações e 0 trocas;
• No total : (n-1) operações  O(n);
• No pior caso:
• 1ª passagem pelo ciclo for : n–1 comparações e n–1 trocas;
• 2ª passagem pelo ciclo for : n–1 comparações e n–2 trocas
• ...
• (n-1)-ésima passagem pelo ciclo for: n-1 comparações e 1 troca;
• Total de comparações : (n-1)x(n-1) = n2 – 2n + 1
• Total de trocas : (n-1) + (n-2) + ... + 1 = n(n-1)/2 = (n2 – n)/2
• Total de operações : (n2 – 2n + 1) + ((n2 – n)/2)  O(n2)
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Eficiência da Ordenação por Borbulagem
void Ordenar_Borbulagem_Opt (int V[], int n) {
int k, kk, fim = n-1, aux;
do{
kk = 0;
for (k = 0; k < fim; k++)
if (V[k] > V[k+1]) {
aux = V[k];
V[k] = V[k+1];
V[k+1] = aux;
kk = k;
}
fim = kk;
}while (kk != 0);
}
INTRODUÇÃO À COMPLEXIDADE DE
ALGORÍTMOS
Eficiência da Ordenação por Borbulagem
• No melhor caso:
• Apenas 1 execução do ciclo for : n-1 comparações e 0 trocas;
• No total : (n-1) operações  O(n);
• No pior caso:
• 1ª passagem pelo ciclo for : n–1 comparações e n–1 trocas;
• 2ª passagem pelo ciclo for : n–2 comparações e n–2 trocas
• ...
• (n-1)-ésima passagem pelo ciclo for: 1 comparações e 1 troca;
• Total de comparações : (n-1) + (n-2) + ... + 1 = n(n-1)/2;
• Total de trocas : (n-1) + (n-2) + ... + 1 = n(n-1)/2;
• Total de operações : 2x(n(n–1)/2) = n(n-1) = (n2–n)  O(n2).
EXERCÍCIOS
1. Analisar o pior caso de algoritmo a seguir, que calcula o valor de nn.
int func (int n)
{
int i, r;
r = 1;
i = 1;
while (i <= n)
{
r = r*n;
i++;
}
return r;
}
EXERCÍCIOS
1. Resolução.
int func (int n) T(n) = t1 + t2.(n+1) + t3.n
{ = t1 + t2 + t2.n + t3.n
int i, r; = c1 + c2.n
r = 1;
T1 – 1 vez T(n) = O(n)
i = 1;
while (i <= n) T2 – n+1 vezes
{
r = r*n;
T3 – n vezes
i++;
}
return r;
}
EXERCÍCIOS
2. Analisar o pior caso do algoritmo abaixo:
int func (int n)
{
int a, b, c, r;
b = n;
c = n;
r = 1;
while (b >= 1)
{
a = b % 2;
if (a == 1)
r = r * c;
c = c * c * c;
b = b / 2;
}
return r;
}

Vous aimerez peut-être aussi