análise de algoritmos · • matematicamente, g(n) é o(f(n)) se: ... variáveis locais da...

32
Análise de Algoritmos Parte 4 Túlio Toffolo [email protected] www.toffolo.com.br BCC202 – Aula 07 Algoritmos e Estruturas de Dados I

Upload: others

Post on 21-Feb-2020

2 views

Category:

Documents


0 download

TRANSCRIPT

Análise de Algoritmos Parte 4 Túlio Toffolo – [email protected] www.toffolo.com.br

BCC202 – Aula 07

Algoritmos e Estruturas de Dados I

Como escolher o algoritmo mais adequado para uma situação?

(continuação)

Mais sobre notação assintótica de funções

•  Existem duas outras notações na análise assintótica de funções:

•  Notação o (“O” pequeno)

•  Notação ω

•  Estas duas notações não são usadas normalmente, mas é importante saber seus conceitos e diferenças em relação às notações O e Ω, respectivamente

3

Notação o

•  O limite assintótico superior definido pela notação O pode ser assintoticamente firme ou não:

•  Por exemplo, o limite 2n2 = O(n2) é assintoticamente firme, mas o limite 2n = O(n2) não é.

•  A notação o é utilizada para definir um limite superior que não é assintoticamente firme

•  Matematicamente, g(n) é o(f(n)) se:

•  Existem duas constantes positivas c e m tais que, para n ≥ m, temos |g(n)| < c|f(n)|.

4

Notação ω

•  De forma análoga, a notação ω está relacionada com a notação Ω, da mesma maneira, ou seja:

•  A notação ω é utilizada para definir um limite inferior que não é assintoticamente firme

•  Matematicamente, g(n) é ω (f(n)) se:

•  Existem duas constantes positivas c e m tais que, para n ≥ m, temos |g(n)| > c|f(n)|.

5

Hierarquia de Funções

•  A seguinte hierarquia pode ser definida do ponto de vista assintótico:

onde ε e c são constantes com 0 < ε < 1 < c.

6

Hierarquias de funções

A seguinte hierarquia de funções pode ser definida do ponto de vista assintótico:

1 � log logn � logn � n✏ � nc � nlogn � cn � nn � ccn

onde ✏ e c são constantes arbitrárias com 0 < ✏ < 1 < c.

UFMG/ICEx/DCC Algoritmos e Estruturas de Dados II 75

Problema do Caixeiro Viajante

•  Seja um conjunto de cidades e uma matriz de distâncias entre elas

•  PCV consiste em encontrar uma rota para um Caixeiro Viajante tal que este:

•  parta de uma cidade origem

•  passe por todas as demais cidades uma única vez •  retorne à cidade origem ao final do percurso

•  percorra a menor distância possível

•  Esta rota é conhecida como ciclo hamiltoniano

Problema do Caixeiro Viajante

Problema do Caixeiro Viajante

Recursividade Parte 1 Túlio Toffolo – [email protected] www.toffolo.com.br

BCC202 – Aula 07

Algoritmos e Estruturas de Dados I

Recursividade

•  A recursividade é uma estratégia que pode ser utilizada sempre que uma função f pode ser escrita em função dela própria.

Exemplo – Função fatorial:

n! = n * (n-1) * (n-2) * (n-3) *....* 1 (n-1)! = (n-1) * (n-2) * (n-3) *....* 1

logo:

n! = n * (n-1)!

Recursividade

•  Definição: dentro do corpo de uma função, chamar novamente a própria função

•  Recursão direta: a função A chama a própria função A

•  Recursão indireta: a função A chama uma função B que, por sua vez, chama A

Condição de parada

•  Nenhum programa nem função pode ser exclusivamente definido por si •  Um programa seria um loop infinito

•  Uma função teria definição circular

•  Condição de parada •  Permite que o procedimento pare de se executar

•  Exemplo: f(x) > 0 onde x é decrescente

•  Objetivo •  Estudar recursividade como ferramenta prática!

Recursividade

•  Para cada chamada de uma função, recursiva ou não, os parâmetros e as variáveis locais são empilhados na pilha de execução.

•  No que isto implica?

•  Maior consumo de memória!

Execução

•  Internamente, quando qualquer chamada de função é feita dentro de um programa, é criado um Registro de Ativação na Pilha de Execução do programa

•  O registro de ativação armazena os parâmetros e variáveis locais da função bem como o “ponto de retorno” no programa ou subprograma que chamou essa função.

•  Ao final da execução dessa função, o registro é desempilhado e a execução volta ao subprograma que chamou a função

Exemplo

int fat1(int n) { int r; if (n<=0) r = 1; else r = n*fat1(n-1); return r;}

int fat2(int n) { if (n<=0) return 1; else return n * fat2(n-1); }

void main() { int f; f = fatX(4); printf("%d",f);}

Complexidade

•  A complexidade de tempo do fatorial recursivo é O(n). (Em breve iremos ver a maneira de calcular isso usando equações de recorrência).

•  Mas a complexidade de espaço também é O(n), devido a pilha de execução.

•  Ja no fatorial não recursivo a complexidade de espaço é O(1).

Fat (int n) { int f = 1; while(n > 0){ f = f * n; n = n – 1; } return f;}

Recursividade

•  Portanto, a recursividade nem sempre é a melhor solução, mesmo quando a definição matemática do problema é feita em termos recursivos

•  Além disso, pode-se afirmar que:

•  Todo algoritmo recursivo tem uma versão não-recursiva

•  O que devemos nos perguntar é: vale a pena implementar esta versão não-recursiva?

Fibonacci

•  Outro exemplo: Série de Fibonacci: •  Fn = Fn-1 + Fn-2 n > 2,

•  F0 = 0 F1 = 1

•  0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89...

int Fib(int n) { if (n<=0) return 0; else if ( n == 1) return 1; else return Fib(n-1) + Fib(n-2);}

Fibonacci não recursivo

•  Complexidade: O(n)

•  Conclusão: não usar recursividade cegamente!

int FibIter(int n) { int i, k, F; i = 1; F = 0; for (k = 1; k <= n; k++) { F += i; i = F - i; } return F; }

Análise da função Fibonacci

•  Ineficiência em Fibonacci

•  Termos Fn-1 e Fn-2 são computados independentemente

•  Número de chamadas recursivas = número de Fibonacci!

•  Custo para cálculo de Fn

•  O(φn) onde φ = (1 + √5)/2 = 1,61803...

•  Golden ratio

•  Exponencial!!!

Fibonacci recursivo “melhorado”

int FibR(int n, int a, int b) { if (n == 0) return a; if (n == 1) return b; return FibR(n-1, b, a+b);}int Fib(int n) { return FibR(n, 0, 1);}

Quando vale a pena usar recursividade

•  Recursividade vale a pena para Algoritmos complexos, cuja a implementação iterativa é complexa e normalmente requer o uso explícito de uma pilha

•  Dividir para Conquistar (Ex. Quicksort)

•  Caminhamento em Árvores (pesquisa, backtracking)

Dividir para Conquistar

•  Duas chamadas recursivas •  Cada uma resolvendo a metade do problema

•  Muito usado na prática •  Solução eficiente de problemas

•  Decomposição

•  Não se reduz trivialmente como fatorial •  Duas chamadas recursivas

•  Não produz recomputação excessiva como fibonacci •  Porções diferentes do problema

Exemplo simples: régua

int regua(int l,int r,int h){ int m; if ( h > 0 ) { m = (l + r) / 2; marca(m, h); regua(l, m, h – 1); regua(m, r, h – 1); }}

Qual é a ordem de impressão das marcas?

Execução: régua

regua(0, 8, 3) marca(4, 3)

regua(0, 4, 2) marca(2, 2)

regua(0, 2, 1) marca(1, 1)

regua(0, 1, 0) regua(1, 2, 0)

regua(2, 4, 1) marca(3, 1)

regua(2, 3, 0) regua(3, 4, 0)

regua(4, 8, 2) marca(6, 2)

regua(4, 6, 1) marca(5, 1)

regua(4, 5, 0) regua(5, 6, 0)

regua(6, 8, 1) marca(7, 1)

regua(6, 7, 0) regua(7, 8, 0)

Outra implementação: régua

int regua2(int l,int r,int h){ int m; m = (l + r) / 2; marca(m,h); if ( h > 1 ) { regua(l, m, h – 1); regua(m, r, h – 1); }}

Outra implementação: régua

int regua2(int l,int r,int h){ int m; m = (l + r) / 2; marca(m,h); if ( h > 1 ) { regua(l, m, h – 1); regua(m, r, h – 1); }}

int regua(int l,int r,int h){ int m; if ( h > 0 ) { m = (l + r) / 2; marca(m, h); regua(l, m, h – 1); regua(m, r, h – 1); }}

Perguntas?

Exercício

•  Crie uma função recursiva que calcula a potência de um número: •  Como escrever a função para o termo n em função do termo

anterior?

•  Qual a condição de parada?