You are on page 1of 41

Análise e Projeto

de Algoritmos

Uni-BH - Centro Universitário de Belo Horizonte


Departamento de Ciências Exatas e Tecnologia
Curso de Ciência da Computação
Algoritmos
• Os algoritmos fazem parte do dia-a-dia das pessoas. Exs.:
– instruções para o uso de medicamentos;
– indicações de como montar um aparelho;
– uma receita de culinária.
• Um algoritmo corresponde a uma sequência de ações
executáveis para a obtenção de uma solução para um
determinado tipo de problema.
• Segundo Dijkstra, um algoritmo corresponde a uma
descrição de um padrão de comportamento, expresso em
termos de um conjunto finito de ações.
– Ao executar a operação "a + b", percebe-se um padrão de
comportamento, mesmo que a operação seja realizada para
valores diferentes de "a" e "b".
2
Estruturas de dados
• Estruturas de dados e algoritmos estão bem relacionados:
– não se pode estudar estruturas de dados sem considerar os
algoritmos associados a elas;
– a escolha dos algoritmos, em geral, depende da
representação e da estrutura dos dados.

• Para resolver um problema, é necessário:


– escolher uma abstração da realidade, mediante a definição de
um conjunto de dados que representa a situação real;
– escolher, em seguida, a forma de representar esses dados.

• A escolha da representação dos dados é determinada,


geralmente, pelas operações a serem realizadas sobre os
dados.
3
Programas
• Programar é basicamente estruturar dados e construir
algoritmos.
– Segundo Wirth, programas representam uma classe especial
de algoritmos capazes de serem seguidos por computadores.

• Um computador só é capaz de seguir programas em


linguagem de máquina. Assim, é necessário construir
linguagens mais adequadas, que facilitem a tarefa de
programar.
– Segundo Dijkstra, uma linguagem de programação é uma
técnica de notação para programar, com a intenção de
promover a expressão do raciocínio algorítmico e a execução
automática de um algoritmo por um computador.

4
Tipos de dados
• Um tipo de dados caracteriza o conjunto de valores a que
uma constante pertence, ou que podem ser assumidos por
uma variável ou expressão, ou que podem ser gerados por
uma função.
• Tipos simples de dados são grupos de valores indivisíveis.
– Uma variável do tipo lógico, por exemplo, pode assumir
apenas o valor verdadeiro ou o valor falso.

• Tipos estruturados de dados definem uma coleção de


valores simples ou um agregado de valores de tipos
diferentes.
– Uma variável do tipo arranjo, por exemplo, pode guardar
uma coleção de valores inteiros.
5
Tipos abstratos de dados
• Um TAD corresponde a um modelo matemático: uma
estrutura de dados acompanhada de operações definidas
sobre as mesmas.
– Por exemplo, uma lista acompanhada de suas operações de
manipulação (inicialização, inserção, remoção, etc).

• Alguns autores utilizam TADs como base para o projeto de


algoritmos.
– A implementação do algoritmo em uma linguagem de
programação exige que se represente um TAD em termos
dos tipos de dados e operadores suportados pela linguagem.

• TADs são considerados generalizações de tipos primitivos,


podendo ser usados para encapsular tipos de dados.
6
Tipos abstratos de dados
• Considere uma lista de inteiros. Pode-se definir um TAD
"Lista", com as seguintes operações:
– criar uma lista vazia;
– obter o 1º item de uma lista; se estiver vazia, retornar nulo;
– inserir um item na lista.
• Há opções de estruturas de dados que permitem uma
implementação eficiente para listas.
• Cada operação do TAD "Lista" é implementada como um
procedimento na linguagem de programação escolhida.
• Qualquer alteração na implementação do TAD fica restrita
à parte encapsulada, sem causar impactos em outras partes
do código.
• Cada conjunto diferente de operações define um TAD
diferente, mesmo que a estrutura de dados seja a mesma.
7
Tempo de execução de um programa
• O projeto de algoritmos é fortemente influenciado pelo
estudo de seus comportamentos.
• Depois que um problema é analisado e decisões de projeto
são finalizadas, é necessário estudar as várias opções de
algoritmos a serem utilizados, considerando os aspectos de
tempo de execução e espaço ocupado.
• Muitos algoritmos são encontrados nas áreas de pesquisa
operacional, otimização, teoria dos grafos, estatística e
probabilidades, recuperação de informação, entre outras.

8
Tempo de execução de um programa
• Segundo Knuth, existem dois tipos de problemas na área
de análise de algoritmos:
– Análise de um algoritmo particular
• Qual é o custo de usar um determinado algoritmo para resolver um
problema específico?
• Características que devem ser investigadas:
– Análise do nº de vezes que cada parte do algoritmo deve ser executada.
– Estudo da quantidade de memória necessária.

– Análise de uma classe de algoritmos


• Qual é o algoritmo de menor custo possível para resolver um
problema específico?
• Toda a classe de algoritmos é investigada.
– Procura-se identificar um que seja o melhor possível.
– Colocam-se limites para a complexidade computacional dos algoritmos
pertencentes à classe.
9
Tempo de execução de um programa
• Quando se consegue determinar o menor custo possível
para resolver problemas de uma classe, tem-se a medida da
dificuldade inerente para resolver tais problemas.
• Quando o custo de um algoritmo é igual ao menor custo
possível, o algoritmo é considerado ótimo para a medida
de custo considerada.
• Podem existir vários algoritmos para resolver o mesmo
problema.
– Se a mesma medida de custo é aplicada a diferentes
algoritmos, então é possível compará-los e escolher o mais
adequado para resolver o problema em questão.

10
Tempo de execução de um programa
• O custo de utilização de um algoritmo pode ser medido por meio da
execução do programa referente em um computador real.

• As medidas de tempo obtidas são inadequadas e os resultados não


devem ser generalizados, já que:
– os resultados são dependentes do compilador que pode favorecer
algumas construções em detrimento de outras;
– os resultados dependem do hardware;
– quando grandes quantidades de memória são utilizadas, as medidas de
tempo podem depender desse aspecto.

• Apesar disso, medidas reais de tempo são válidas, quando há vários


algoritmos para resolver um mesmo tipo de problema, todos com um
custo de execução dentro da mesma ordem de grandeza.
– No caso, são considerados tanto os custos reais das operações como os
custos não aparentes, tais como alocação de memória, indexação e carga.
11
Tempo de execução de um programa
• O custo de utilização de um algoritmo pode ser medido,
mais adequadamente, por meio do uso de um modelo
matemático baseado em um computador idealizado.
– Devem ser especificados o conjunto de operações a serem
executadas e os seus custos de execução.

– É mais usual ignorar o custo de algumas operações e


considerar apenas as operações mais significativas.
• Por exemplo, para algoritmos de ordenação, pode-se considerar
apenas o número de comparações entre os itens e, no caso, ignorar
operações aritméticas, de atribuição e manipulações de índices.

12
Tempo de execução de um programa
• Para medir o custo de execução de um algoritmo, é comum
definir uma função de custo ou função de complexidade f.
– f(n) é a medida do tempo necessário para executar um
algoritmo para um problema de tamanho n. Neste caso, é
chamada função de complexidade de tempo.
– f(n) é a medida da memória necessária para executar um
algoritmo para um problema de tamanho n. Neste caso, é
chamada função de complexidade de espaço.

• A não ser que haja uma referência, geralmente f(n) denota


uma função de complexidade de tempo.
• A complexidade de tempo, de fato, não representa tempo
diretamente, mas o número de vezes que determinada
operação considerada relevante é executada.
13
Tempo de execução de um programa
• Considere o algoritmo para encontrar o maior elemento de um vetor
de inteiros A[1..n], sendo n ≥ 1.
int Max ( Vetor A )
{ int i, temp;
temp = A[0];
for (i=1; i<n; i++)
if (Temp < A[i] )
Temp = A[i];
return Temp;
}

• Seja f uma função de complexidade tal que f(n) é o número de


comparações entre os n elementos de A.
– Assim, f(n) = n - 1, para n > 0.
– O algoritmo apresentado é ótimo.
14
Tempo de execução de um programa
• Teorema: Qualquer algoritmo para encontrar o maior
elemento de um conjunto com n elementos, n ≥ 1, faz pelo
menos n - 1 comparações.
• Prova: Cada um dos n - 1 elementos tem de ser mostrado,
por meio de comparações, que é menor do que algum outro
elemento. Logo, n - 1 comparações são necessárias.
• O teorema diz que, se o número de comparações for
utilizado como medida de custo, então a função Max é
ótima.

15
Tempo de execução de um programa
• A medida do custo de execução de um algoritmo depende,
principalmente, do tamanho da entrada dos dados.
– É comum considerar o tempo de execução de um programa
como uma função do tamanho da entrada.

• Para alguns algoritmos, o custo de execução é uma função


da entrada particular dos dados, não apenas do tamanho da
entrada.
– No caso da função Max, o custo é uniforme sobre todos os
problemas de tamanho n.
– Para um algoritmo de ordenação, isso não ocorre: se os
dados de entrada já estiverem quase ordenados, então o
algoritmo pode trabalhar menos.

16
Tempo de execução de um programa
• Em uma análise, existem três cenários a serem tratados:
– Melhor caso: menor tempo de execução sobre todas as
possíveis entradas de tamanho n.
– Pior caso: maior tempo de execução sobre todas as possíveis
entradas de tamanho n.
• Se f é uma função de complexidade baseada na análise de pior caso,
o custo de aplicar o algoritmo nunca é maior do que f(n).
– Caso médio (caso esperado): média dos tempos de execução
de todas as entradas de tamanho n.
• Uma distribuição de probabilidades sobre o conjunto de entradas de
tamanho n é suposta para obtenção do custo médio.
• Quando possível, é comum supor uma distribuição de probabilidades
em que todas as entradas possíveis são igualmente prováveis.
• A análise do caso médio é geralmente muito mais difícil de obter do
que as análises do melhor e do pior caso.
17
Tempo de execução de um programa
• Para ilustrar os conceitos, considere o problema de localizar
o item de um vetor, cuja chave seja a chave de pesquisa do
item desejado. Neste caso, o algoritmo mais simples é o que
faz a pesquisa sequencial.
• Seja f uma função de complexidade tal que f(n) é o número
de itens consultados no vetor (número de vezes que a chave
de pesquisa é comparada com a chave de cada item).
– Melhor caso: f(n) = 1 (item desejado é o primeiro consultado).
– Pior caso: f(n) = n (item desejado é o último consultado ou
não está presente no vetor).
– caso médio: f(n) = (n + 1)/2.

18
Tempo de execução de um programa
• No estudo do caso médio, considere:
– toda pesquisa recupera um item;
– pi é a probabilidade de que o i-ésimo item seja procurado;
– para recuperar o i-ésimo item, são necessárias i comparações.
Logo: f(n) = 1 × p1 + 2 × p2 + 3 × p3 + · · · + n × pn.
• Para calcular f(n), basta conhecer a distribuição de
probabilidades pi.
– Se os itens tiverem a mesma probabilidade de serem
procurados, então pi = 1/n, para 1 ≤ i ≤ n.
Nesse caso,

• A análise do caso esperado revela que uma pesquisa com


sucesso examina aproximadamente metade dos itens.
19
Tempo de execução de um programa
• Considere o problema de encontrar o maior e o menor elementos de
um vetor de inteiros A[1..n], sendo n ≥ 1. Um algoritmo simples pode
ser derivado do algoritmo para achar o maior elemento.
void MaxMin1(Vetor A, int *Max, int *Min)
{ int i;
*Max = A[0]; *Min = A[0];
for (i = 1; i < n; i++)
{ if (A[i] > *Max) *Max = A[i];
if (A[i] < *Min) *Min = A[i];
}
}
• Seja f uma função de complexidade tal que f(n) é o número de
comparações entre os n elementos de A.
– Assim, f(n) = 2(n – 1), para n > 0, em todos os casos.
20
Tempo de execução de um programa
• O procedimento MaxMin1 pode ser facilmente melhorado: a
comparação A[i] < *Min só é necessária quando a comparação
A[i] > *Max dá falso.
void MaxMin2(Vetor A, int *Max, int *Min)
{ int i;
*Max = A[0]; *Min = A[0];
for (i = 1; i < n; i++)
{ if (A[i] > *Max) *Max = A[i];
else if (A[i] < *Min) *Min = A[i];
}
}
• Para a nova implementação, tem-se:
– Melhor caso: f(n) = n - 1 (elementos estão em ordem crescente).
– Pior caso: f(n) = 2(n - 1) (elementos estão em ordem decrescente);
– Caso médio: f(n) = 3n/2 - 3/2 (A[i] é maior do que Max a metade das vezes). 21
Tempo de execução de um programa
• Considerando o número de comparações realizadas, ainda
existe a possibilidade de se obter um algoritmo mais
eficiente, a saber:
1. Compare os elementos de A aos pares, separando-os em
dois subconjuntos (maiores em um e menores em outro), a
um custo de comparações.
2. O máximo é obtido do subconjunto que contém os maiores
elementos, a um custo de comparações.
3. O mínimo é obtido do subconjunto que contém os menores
elementos, a um custo de comparações.

22
Tempo de execução de um programa
void MaxMin3(Vetor A, int while (i <= FimDoAnel)
*Max, int *Min) { if (A[i - 1] > A[i])
{ int i, FimDoAnel; { if (A[i - 1] > *Max) *Max =
if ((n & 1) > 0) A[i - 1];
{ A[n] = A[n - 1]; if (A[i] < *Min) *Min =
FimDoAnel = n; A[i];
} }
else FimDoAnel = n - 1; else { if (A[i - 1] < *Min)
*Min = A[i - 1];
if (A[0] > A[1])
if (A[i] > *Max) *Max =
{ *Max = A[0]; A[i];
*Min = A[1]; }
} i += 2;
else }
{ *Max = A[1]; }
*Min = A[0];
}
i = 3;
23
Tempo de execução de um programa
• De acordo com o procedimento MaxMin3:
– os elementos de A são comparados dois a dois; os maiores
elementos são comparados com Max e os menores
elementos com Min;
– quando n é ímpar, o elemento que está na posição A[n] é
duplicado na posição A[n+1], para se evitar um tratamento
de exceção;
– para o melhor caso, pior caso e caso médio,

24
Tempo de execução de um programa
• A seguinte tabela apresenta o número de comparações dos
programas MaxMin1, MaxMin2 e MaxMin3.

• Os algoritmos MaxMin2 e MaxMin3 são superiores ao


algoritmo MaxMin1 de forma geral.
• O algoritmo MaxMin3 é superior ao algoritmo MaxMin2
com relação ao pior caso e bastante próximo quanto ao
caso médio. 25
Limite Inferior - Uso de um Oráculo
• Existe possibilidade de obter um algoritmo MaxMin mais
eficiente?
• Para responder temos de conhecer o limite inferior para
essa classe de algoritmos.
• Técnica muito utilizada: uso de um oráculo.
• Dado um modelo de computação que expresse o
comportamento do algoritmo, o oráculo informa o
resultado de cada passo possível (no caso, o resultado de
cada comparação).
• Para derivar o limite inferior, o oráculo procura sempre
fazer com que o algoritmo trabalhe o máximo, escolhendo
como resultado da próxima comparação, aquele que cause
o maior trabalho possível necessário para determinar a
resposta final.
26
Exemplo de Uso de um Oráculo

27
Exemplo de Uso de um Oráculo

28
Exemplo de Uso de um Oráculo

29
Exercício
• Qual é a complexidade de tempo de:
a) Considerando o número de operações aritméticas.
int f(int n, int m){
int i, j;
for (i = 0; i < n; i++)
for (j=0; j< m; j++)
temp = temp + 1;
return temp;
}
b) Considerando o número de comparações.
int f (int n){
if (n > 0){
return f(n-1);
}
else return 1;
}
30
Exercício
b) Considerando o número de comparações.
int f (int n){
if (n > 0){
return f(n-1) + f(n-1);
}
else return 1;
}
Resolva as relações de recorrências:
a) T(n) = T(n-1) + c, n>1
T(1) = 0;
b) T(n) = T(n-1) + 2n, n>0
T(0) = 1
31
Comportamento Assintótico de Funções

32
Dominação assintótica

33
Notação O

34
Exemplo de Notação O

35
Comportamento assintótico de funções
• Algumas operações realizadas com a notação O são:

• A regra da soma O(f(n)) + O(g(n)) pode ser usada para calcular


o tempo de execução de uma sequência de trechos de programa.
– Suponha trechos cujo tempo de execução são O(n), O(n2) e
O(nlogn). Assim, o tempo de execução do programa é O(n2).
• O produto de por é:

36
Comportamento assintótico de funções
• Dizer que g(n) é O(f(n)) significa que f(n) é um limite
superior para g(n). A notação Ω especifica o limite inferior.

• Definição notação Ω: Uma


função g(n) é Ω(f(n)) se existem
duas constantes positivas c e m
tais que, para todo n ≥ m, tem-se
|g(n)| ≥ c × |f(n)|.

• Exemplo 1: Seja g(n) = 3n3 + 2n2


– Logo g(n) é Ω(n3), já que |3n3+2n2| ≥ 1 × |n3| para n ≥ 0.

• Exemplo 2: Seja g(n) = n (n ímpar) e g(n) = n2/10 (n par)


– Logo g(n) é Ω(n2), já que |n2/10| ≥ 1/10 × |n2| para n par ≥ 0.
37
Comportamento assintótico de funções
• Definição notação Θ: Uma função g(n) é Θ(f(n)) se
existirem constantes positivas c1, c2 e m tais que, para todo
n ≥ m, tem-se 0 ≤ c1 × f(n) ≤ g(n) ≤ c2 × f(n).

• Para todo n ≥ m, g(n) é igual a f(n) a menos de uma


constante. Nesse caso, f(n) é considerado um limite
assintótico firme.
38
Comportamento assintótico de funções
• Exemplo: Seja g(n) = n2/3 - 2n.
– Para mostrar que g(n) = Θ(n2), deve-se determinar valores
para as constantes c1, c2 e m tais que:

– Dividindo por n2, tem-se:


– O lado direito da desigualdade será sempre válido para
qualquer valor de n ≥ 1, ao se escolher c2 ≥ 1/3.
– O lado esquerdo da desigualdade será válido para qualquer
valor de n ≥ 7, ao se escolher c1 ≤ 1/21.
– Logo, escolhendo c1 = 1/21, c2 = 1/3 e m = 7, é possível
verificar que g(n) = Θ(n2).
– Outras constantes podem existir, mas o importante é que
existe alguma escolha para as três constantes.
39
Comportamento assintótico de funções
• A notação o é usada para definir um limite superior que não é
assintoticamente firme.

• Definição notação o: Uma função g(n) é o(f(n)) se, para qualquer


constante c > 0, então 0 ≤ g(n) < c × f(n) para todo n ≥ m.

• Exemplo: 2n = o(n2), mas 2n2 ≠ o(n2).

• As notações O e o são similares.


– A diferença é que em g(n) = O(f(n)), a expressão “0 ≤ g(n) < c × f(n)” é
válida para alguma constante c > 0, mas em g(n) = o(f(n)), tal expressão
é válida para todas as constantes c > 0.

• Na notação o, a função g(n) tem um crescimento muito menor que


f(n) quando n tende para infinito.
– Alguns autores usam o seguinte limite para definição da notação o:

40
Comportamento assintótico de funções
• A notação ω é usada para definir um limite inferior que não é
assintoticamente firme.

• Definição notação ω: Uma função g(n) é ω(f(n)) se, para qualquer


constante c > 0, então 0 ≤ c × f(n) < g(n) para todo n ≥ m.

• Exemplo: n2/2 = ω(n), mas n2/2 ≠ ω(n2).

• As notações Ω e ω são similares.


– A diferença é que em g(n) = Ω(f(n)), a expressão “0 ≤ c × f(n) < g(n)” é
válida para alguma constante c > 0, mas em g(n) = ω(f(n)), tal expressão
é válida para todas as constantes c > 0.

• Na notação ω, a função f(n) tem um crescimento muito menor que


g(n) quando n tende para infinito.
– Alguns autores usam o seguinte limite para definição da notação ω:

41

You might also like