Você está na página 1de 34

Estruturas de Dados e Algoritmos II

Vasco Pedro
(edited by Salvador Abreu)

Departamento de Informática
Universidade de Évora

2023/24
Docentes

▶ Teóricas
Salvador Abreu
spa@uevora.pt / CLV-240

▶ Práticas
João Pereira
joao.pedro.pereira@uevora.pt / CLV-256

▶ Atendimento por marcação


(enviar mail, evitar mensagens do Moodle)
Programa
Tópicos

▶ Análise da complexidade
▶ Complexidade amortizada
▶ Construção de algoritmos
▶ Divisão e conquista
▶ Algoritmos greedy
▶ Programação dinâmica
▶ Estruturas de dados
▶ Partição (Union-find)
▶ Algoritmos sobre grafos
▶ Percursos, ordenação topológica, árvore de cobertura mı́nima,
caminhos, fluxos
▶ Teoria da complexidade
▶ P e NP
Pré-requisitos
Estruturas de dados e algoritmos
▶ Tipo abstracto de dados (TAD)
▶ Listas
▶ Pilha
▶ Fila (com e sem prioridade)
▶ Árvores
▶ Árvore binária de pesquisa
▶ Tabelas de dispersão (hash)
▶ Ordenação
▶ Bases de análise de complexidade

Programação
▶ Bases (sólidas)
▶ Java
Bibliografia

Referência principal
Introduction to Algorithms, Cormen, Leiserson, Rivest, e Stein
(qualquer edição, a partir da 2ª), MIT Press

Outras
Algorithms Unlocked, Cormen, MIT Press, 2013
...
Avaliação

Componente escrita (70%)


▶ 2 frequências (é necessário assistir a 75% das aulas práticas)
ou
▶ 1 exame
Consulta: uma folha A4
Nota mı́nima: 7 valores

Componente prática (30%)


▶ 2 de 3 trabalhos
Entrega no mooshak
Grupos de 1 ou 2 elementos, podem variar
Nota mı́nima: 7 valores
Avaliação
Datas

Componente escrita (datas provisórias, em 2024)


▶ 1a frequência: 2a -feira, 8 de Abril (11h) na teórica
▶ 2a frequência + exame: 3a -feira, 11 de Junho (14h)
▶ “Discussões”: 3a -feira, 11 de Junho (17h00)
▶ Recurso: 3a -feira, 25 de Junho (10h)
▶ Época especial: 3a -feira, 23 de Julho (10h)

Componente prática (datas provisórias e aproximadas)


▶ 1o trabalho: 4a ∼ 6a semanas
▶ 2o trabalho: 10a ∼ 12a semanas
▶ 3o trabalho: 13a ∼ 15a semanas
Complexidade
(ver capı́tulo 3 da referência principal)
Pseudo-código
Exemplo
PESQUISA-LINEAR(V, k)
1 n <- |V| // inicializaç~
ao
2 i <- 1
3 while i <= n and V[i] != k do // pesquisa
4 i <- i + 1
5 if i <= n then // resultado:
6 return i // - sucesso
7 return INEXISTENTE // - insucesso

|V| no de elementos de um vector — O(1)


V[1..|V|] elementos do vector
and e or só é avaliado o segundo operando se necessário
variável.campo acesso a um campo de um “objecto”

(INEXISTENTE é uma constante, com valor −1, por exemplo)


Análise da complexidade (1)
Exemplo

Análise da complexidade temporal, no pior caso, da função


PESQUISA-LINEAR, por linha de código
1. Obtenção da dimensão de um vector, afectação: operações
com complexidade (temporal) constante

O(1) + O(1) = O(1)

2. Afectação: O(1)
3. Acessos a i, n, V[i] e k, comparações e saltos condicionais
com complexidade constante

4 O(1) + 2 O(1) + 2 O(1) = O(1)

Executada, no pior caso, |V|+1 vezes

(|V|+ 1) × O(1) = O(|V|)


Análise da complexidade (2)
Exemplo

4. Acesso a i, soma e afectação: O(1) + O(1) + O(1) = O(1)


Executada, no pior caso, |V| vezes

|V|× O(1) = O(|V|)

5. Acesso a i e n, comparação e salto condicional com


complexidade constante

2 O(1) + O(1) + O(1) = O(1)

6. Saı́da de função com complexidade constante: O(1)


7. Saı́da de função com complexidade constante: O(1)
Análise da complexidade (3)
Exemplo

Juntando tudo
O(1) + O(1) + O(|V|) + O(|V|) + O(1) + max{O(1), O(1)} =
= 4 O(1) + 2 O(|V|) =
= O(|V|)

No pior caso, a função PESQUISA-LINEAR tem complexidade


temporal linear na dimensão do vector V

Se n representar a dimensão do vector V, o tempo T (n) que a


função demora a executar tem complexidade linear em n

T (n) = O(n)

Isto significa que o tempo que a função demora a executar varia


linearmente com a dimensão do input
A notação O (1)

O(g (n)) = {f (n) : ∃c,n0 >0 tais que ∀n≥n0 0 ≤ f (n) ≤ c g (n)}

cg.n/

f .n/

n
n0
f .n/ D O.g.n//
A notação O (2)

O(g (n)) = {f (n) : ∃c,n0 >0 tais que ∀n≥n0 0 ≤ f (n) ≤ c g (n)}

▶ O(n) = {f (n) : ∃c,n0 >0 tais que ∀n≥n0 0 ≤ f (n) ≤ c n}

n = O(n) 2n + 5 = O(n) log n = O(n) n2 ̸= O(n)

▶ O(n2 ) = {f (n) : ∃c,n0 >0 tais que ∀n≥n0 0 ≤ f (n) ≤ c n2 }

n2 = O(n2 ) 4n2 + n = O(n2 ) n = O(n2 ) n3 ̸= O(n2 )

▶ O(log n) = {f (n) : ∃c,n0 >0 tais que ∀n≥n0 0 ≤ f (n) ≤ c log n}

1 + log n = O(log n) log n2 = O(log n) n ̸= O(log n)

f (n) = O(g (n)) significa f (n) ∈ O(g (n))

Lê-se f (n) é O de g (n)


A notação O (3)

Ω(g (n)) = {f (n) : ∃c,n0 >0 tais que ∀n≥n0 0 ≤ c g (n) ≤ f (n)}

f .n/

cg.n/

n
n0
f .n/ D .g.n//

n = Ω(n) n2 = Ω(n) log n ̸= Ω(n2 )


A notação O (4)

Θ(g (n)) = {f (n) : ∃c1 ,c2 ,n0 >0 t.q. ∀n≥n0 0 ≤ c1 g (n) ≤ f (n) ≤ c2 g (n)}

c2 g.n/

f .n/

c1 g.n/

n
n0
f .n/ D ‚.g.n//

3n2 + n = Θ(n2 ) n ̸= Θ(n2 ) n2 ̸= Θ(n)


A notação O (5)

o(g (n)) = {f (n) : ∀c>0 ∃n0 >0 tal que ∀n≥n0 0 ≤ f (n) < c g (n)}

n = o(n2 ) n2 ̸= o(n2 ) log n = o(n)

∀k>0 nk = o(2n )

∀k>0 ∀b>1 nk = o(b n )

ω(g (n)) = {f (n) : ∀c>0 ∃n0 >0 tal que ∀n≥n0 0 ≤ c g (n) < f (n)}

n = ω(log n) n2 = ω(log n) log n ̸= ω(log n)

∀k>0 2n = ω(nk )

∀b>1 ∀k>0 b n = ω(nk )


A notação O (6)

Traduzindo. . .

f (n) = O(g (n)) f (n) não cresce mais depressa que g (n)

f (n) = o(g (n)) f (n) cresce mais devagar que g (n)

f (n) = Ω(g (n)) f (n) não cresce mais devagar que g (n)

f (n) = ω(g (n)) f (n) cresce mais depressa que g (n)

f (n) = Θ(g (n)) f (n) e g (n) crescem ao mesmo ritmo


A complexidade na prática
Pesquisa dicotómica ou binária
De um valor num vector ordenado

PESQUISA-DICOTÓMICA(V, k)
1 n <- |V|
2 return PESQUISA-DICOTÓMICA-REC(V, k, 1, n)

PESQUISA-DICOTÓMICA-REC(V, k, i, f)
1 if i > f then
2 return INEXISTENTE // intervalo vazio
3 m <- (i + f) / 2
4 if k < V[m] then
5 return PESQUISA-DICOTÓMICA-REC(V, k, i, m - 1)
6 if k > V[m] then
7 return PESQUISA-DICOTÓMICA-REC(V, k, m + 1, f)
8 return m // V[m] = k
Complexidade temporal das pesquisas linear e dicotómica

Pior caso e caso esperado para a complexidade temporal das


pesquisas num vector de dimensão n

Pesquisa linear
T (n) = O(n)

Pesquisa dicotómica

T (n) = O(log n)
Pesquisas linear e dicotómica
Dos n elementos de um vector

70

60

50

40
tempo (s)

30

20

10

0
10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200

n elementos (milhares)

linear dicotómica
Pesquisas linear e dicotómica (com escalas diferentes)
Dos n elementos de um vector

70 0.05

60
0.04

50

0.03
40
tempo (s)

tempo (s)
30
0.02

20

0.01
10

0 0.00
10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200

n elementos (milhares)

linear dicotómica
Pesquisas linear e dicotómica (com escalas diferentes)
Dos n elementos de um vector
70 0.05

60
0.04

50

0.03
40
tempo (s)

tempo (s)
30
0.02

20

0.01
10

0 0
10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200

n elementos (milhares)

linear c n² dicotómica c n log n


Números de Fibonacci
Versão recursiva

public static int fibonacci(int n)


{
if (n == 0)
return 0;

if (n == 1)
return 1;

return fibonacci(n - 1) + fibonacci(n - 2);


}
Cálculo recursivo dos números de Fibonacci
Análise da complexidade temporal

Como a execução das instruções do corpo da função é feita em


tempo O(1), a complexidade temporal da função fibonacci pode
ser calculada a partir do número de chamadas recursivas feitas

Número de chamadas recursivas para o cálculo de fibonacci(n)



0 n =0∨n =1
CR(n) =
CR(n − 1) + CR(n − 2) + 2 n>1


Tem-se que CR(n) = Θ(ϕn ), onde ϕ = 1+2 5 ≈ 1.618 é o número
de ouro. De igual modo, temos CR(n) = O(2n ).
Logo, a complexidade temporal de fibonacci(n) é exponencial
em n
Números de Fibonacci
Versão iterativa com tabelação

public static int fibonacci(int n)


{
int[] tabela = new int[n + 1];

// casos base
tabela[0] = 0;
tabela[1] = 1;

// caso recursivo
for (int i = 2; i <= n; ++i)
tabela[i] = tabela[i - 1] + tabela[i - 2];

return tabela[n];
}
Números de Fibonacci
Versão iterativa :-(

public static int fibonacci(int n) {


int i = 0;
int c = 0;
int a = 1;

while (i<n) {
int p = c + a;
a = c;
c = p;
i++;
}
return c; }
Números de Fibonacci
Versão iterativa :-)

public static int fibonacci(int n)


{
int i = 0;
int corrente = 0; // fibonacci(i)
int anterior = 1; // fibonacci(i - 1)
while (i < n) {
int proximo = corrente + anterior; // fibonacci(i + 1)
anterior = corrente;
corrente = proximo;
i++;
}
return corrente;
}
Números de Fibonacci
Versão recursiva com memória

private static int NMAX = ...;


private static int[] memoria;

static {
memoria = new int[NMAX + 1];
memoria[1] = 1;
}

public static int fibonacci(int n)


{
if (n > 1 && memoria[n] == 0)
memoria[n] = fibonacci(n - 1) + fibonacci(n - 2);

return memoria[n];
}
Complexidade temporal do cálculo dos números de
Fibonacci

Cálculo recursivo (puro)

T (n) = Θ(ϕn )

Cálculo iterativo
Cálculo iterativo com tabelação
Cálculo recursivo com memória

T (n) = Θ(n)
Números de Fibonacci
7000

6000

5000

4000
tempo (ms)

recursivo
memória
3000
iterativo

2000

1000

0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

n
Números de Fibonacci
Escalas diferentes
7000 0.002

6000

5000

4000
tempo (ms)

0.001
recursivo
3000 memória
iterativo

2000

1000

0 0.000
1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40

n
Números de Fibonacci
Escalas diferentes
7000 0.002

6000

5000

4000
tempo (ms)

recursivo
0.001
c phi^n
memória
3000
cn
iterativo
c' n

2000

1000

0 0.000
1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40

Você também pode gostar