Universidade Federal de Juiz de Fora Departamento de Ciência da Computação

Texto

(1)

Estrutura de Dados

(2)

◮ Recursividade

◮ Definição

◮ Algoritmos recursivos

◮ Exemplos

◮ Introdução à Análise de Complexidade

◮ Algoritmos recursivos

◮ Exemplos

(3)
(4)

◮ Recursão é uma abordagem de solução de problemas que

pode ser utilizada para gerar soluções simples a certos tipos de problemas que seriam difíceis de resolver de outra maneira.

◮ Em um algoritmo recursivo, o problema original é

dividido e um ou mais versões simples de si mesmo.

◮ P. ex., se a solução do problema original envolvenitens, o

pensamento recursivo deve dividi-lo em 2 problemas: um envolvendon−1 itens e um outro envolvendo um único

item.

◮ O problema comn1 itens poderia ser dividido

novamente em um envolvendon−2 itens e um outro

envolvendo apenas um único item, e assim por diante.

◮ Se a solução de todos os problemas com um item é trivial,

podemos construir a solução do problema original a partir das soluções dos problemas mais simples.

(5)
(6)

◮ Em imagens...

(7)

Algoritmo recursivo genérico

◮ Considere quenseja um inteiro que representa, de alguma

forma, o tamanho de um determinado problema que deseja-se resolver utilizando um algoritmo recursivo.

◮ Descrição geral de um algoritmo recursivo:

1. Se o problema puder ser resolvido diretamente para o valor atual den

Resolva-o 2. Senão

◮ Apliquerecursivamenteo algoritmo a um ou mais problemas (iguais ao original), porém envolvendo valores menores den.

3. Combineas soluções dos problemas menores para obter a

(8)

Algoritmo recursivo genérico

◮ Nosetem-se um teste para o que é chamado decaso base:

o valor denpara o problema é suficientemente pequeno e,

portanto, o problema pode ser resolvido facilmente.

◮ Nosenão, tem-se opasso recursivo, porque aqui aplica-se

o algoritmo recursivamente, isto é, para resolver o mesmo problema, mas para uma instância menor (valor den

menor).

◮ Sempre que ocorrer uma divisão, revisita-se o caso base

para cada novo problema a fim de verificar se esse é um caso base ou um caso recursivo.

(9)

◮ Uma solução recursiva tem as seguintes características: ◮ Deve-se conhecer a solução direta para o caso base (para

um valor pequeno de n)

◮ Um problema de um dado tamanho (digamos, n) pode ser dividido em uma ou mais versão menores do mesmo problema (caso recursivo).

◮ Será visto, daqui em diante, exemplos de soluções

recursivas em C++ para alguns problemas comuns de programação.

◮ Fatorial

◮ Sequência de Fibonacci

◮ Máximo divisor comum

◮ etc

◮ Note que, em implementações recursivas, as funções que

(10)

Fatorial

Definição:o fatorial de um inteiro não-negativon,

representado porn!, é o produto de todos os inteiros

positivos menores ou iguais an.

◮ Adota-se por definição que 0! =1.

◮ Exemplos: ◮ 0! =1 ◮ 1! =1 ◮ 2! =2×1 ◮ 3! =3×2×1 ◮ 4! =4×3×2×1 ◮ 5! =5×4×3×2×1

◮ Note que ◮ 5! =5×4!

(11)

Fatorial

◮ Implementaçãoiterativa(não-recursiva) int fatorial(int n)

{

int fat = 1;

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

return fat; }

◮ Implementaçãorecurvisa int fatorial(int n) {

if(n==0 || n==1) return 1; else

(12)

Fatorial

(13)

Potência

◮ Desenvolver uma função recursiva para calcularxn. ◮ Para simplificar, inicialmente, considere quen0. ◮ Definição recursiva do problema:

xn=

(

1, sen=0

(14)

Potência

◮ Implementação iterativa float pot(float x, int n) {

float r=1.0;

for(int i=1; i<=n; i++) r = r * x;

return r; }

◮ Implementaçãorecursiva float pot(float x, int n) {

if(n==0) return 1.0; else

return x * pot(x,n-1); }

(15)

Potência

◮ Senpode ser negativo, então

xn=

  

 

1, sen=0

1

xn, sen<0 x×xn−1, sen>0

◮ E assim

float pot(float x, int n) {

if(n==0) return 1.0; else if(n < 0)

return 1.0/pot(x,-n); else

(16)

Potência

◮ Pode-se ainda pensar na seguinte melhoria dessa função:

xn=

          

1, sen=0

1

xn, sen<0

(x2)⌊n/2⌋, sené par

x(x2)⌊n/2⌋

, sené ímpar

float pot(float x, int n) {

if(n==0) return 1.0; else if(n < 0)

return 1.0/pot(x,-n); else if(n%2==0) return pot(x*x,n/2); else return x*pot(x*x,n/2); }

(17)

MDC

◮ Calcular o máximo divisor comum (m.d.c.) entremen. ◮ Definição recursiva:

mdc(m,n) =

  

 

mdc(n,m), seM<n

n, sené divisor dem

mdc(n,mmodn), caso contrário

int mdc(int m, int n) {

if(m < n)

return mdc(n, m);

else if(m%n == 0) // caso base - conhecido return n;

else // passo recursivo

(18)

Fibonacci

◮ A sequência de Fibonacci é uma sequência de números

inteiros, que começa com os números 0 e 1, na qual cada termo subsequente corresponde à soma dos dois

anteriores.

0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987, ...

◮ SejaF1=1 o primeiro número. SeFné on-ésimo número

da sequência de Fibonacci, então, este pode ser definido como:

Fn=Fn−1+Fn−2

(19)

Fibonacci

◮ Implementaçãorecursiva int fib(int n)

{

if(n == 0 || n == 1) return n;

else

return fib(n-1) + fib(n-2); }

◮ Note que noelsedessa implementação recursiva, a

(20)

Fibonacci

◮ Essa solução é muito ineficiente.

◮ Exemplo paran=5 (foi usado f(n) no lugar de fib(n)):

f(5) f(4) f(3) f(2) f(1) f(0) f(1) f(2) f(1) f(0) f(3) f(2) f(1) f(0) f(1)

◮ O problema com essa solução é que a função é chamada

várias vezes para o mesmo valor (parâmetro). Ex:F(2)é

calculado 3 vezes.

◮ Para valoresnmaiores, a situação piora.

(21)

Fibonacci

◮ Uma outra implementaçãorecursiva:

int fib(int atual, int anterior, int n) {

if(n == 1) return atual; else

return fib(atual + anterior, atual, n-1); }

◮ Para começar a execução deve-se usar a seguinte função int fibonacci(int n)

{

(22)

Fibonacci

◮ Essa versão é mais eficiente. Veja o exemplo paran=5.

(23)

Intervalo

◮ Desenvolver uma função recursiva para imprimir todos os

números inteiros no intervalo fechado deaatéb. void seq(int a, int b)

{

if(a <= b) {

cout << a << " "; seq(a+1, b); }

}

◮ Exemplo de como a função deve ser chamada seq(0, 10)

◮ Saída

(24)

Intervalo

◮ Qual o resultado de inverter as seguintes instruções? void seq(int a, int b)

{

if(a <= b) {

seq(a+1, b); cout << a << " "; }

}

(25)

Número primo

◮ Um número primo só é divisível por 1 e por ele mesmo. bool ehPrimo(int p)

{

return auxPrimo(p, 2); }

bool auxPrimo(int p, int i) {

if (i==p) return true;

if (p%i == 0) return false;

(26)

Maior

◮ Desenvolver uma função recursiva para calcular e retornar

o maior valor de um vetorvcomnnúmeros reais. ◮ Caso base: vetor com apenas 1 elemento, que é o maior. ◮ Passo recursivo: o maior elemento do vetor é ou o último

elemento ou o maior elemento dentre osn−1 primeiros

elementos do vetor.

◮ Isto é:

max(v,n) =

  

 

v[0], sen=1

v[n−1], sev[n−1]>max(v,n−1)

max(v,n−1), caso contrário

(27)

Maior

◮ Implementaçãorecursiva

float max(float vet[], int n) {

if (n == 1) // caso base return vet[0];

float x = max(vet, n-1); // passo recursivo

if(x > vet[n-1]) return x; else

(28)

Maior

◮ Outra implementação ◮ Função empacotadora

float max(float vet[], int n) {

return maxR(vet, 0, n); }

◮ Função recursiva

float maxR(float vet[], int i, int n) {

if(i == n-1) return vet[i];

float x = maxR(vet, i+1, n);

return (x > vet[i]) ? x : vet[i]; }

(29)

Pares

◮ Desenvolver uma função recursiva para calcular e retornar

a quantidade de valores pares de um vetorvcomn

números inteiros.

int pares(int vet[], int n) {

if(n == 1) // caso base if (vet[0] % 2 == 0)

return 1;

int x = pares(vet, n-1) ; // passo recursivo

if (vet[n-1] % 2 == 0) return x+1;

else

(30)

1. Desenvolver uma função recursiva que recebe um número inteiro n e retorna o valor do somatório:

n+ (n−1) + (n−2) +. . .+2+1.

2. Desenvolver uma função recursiva que recebe um vetor de reais e seu tamanhon, calcular e retornar o menor valor do

vetor.

3. Desenvolver uma função recursiva que recebe um vetor de reais e seu tamanhon, calcular e retornar a soma de todos

os seus valores.

4. Desenvolver uma função recursiva para calcular e retornar a quantidade de valores pares de um vetorvcomn

números inteiros.

5. Desenvolver uma função recursiva para calcular e retornar umastringde caracteres contendo ‘0’ e ‘1’ correspondente

à versão binária de um número inteiro positivo dado.

(31)
(32)

◮ Algoritmos demandam tempo de execução e recursos

(memória, espaço em disco, dispositivos externos, etc).

◮ Analisar a alocação de recursos que um certo algoritmo

demanda é importante na escolha de soluções mais rápidas ou que ocupem menos espaço de memória, por exemplo.

◮ Bons programadores se preocupam em implementar

algoritmos que demandam o mínimo de recursos e executem no menor tempo possível.

◮ Embora um programa possa ser analisado sob vários

aspectos, destaca-se a seguir a análise relativa ao seu desempenho, especialmente, em relação a medida do seu tempo de computação.

(33)

◮ A análise de complexidade de algoritmos é uma

ferramenta que permite estudar como um algoritmo se comporta quando os dados de entrada aumentam.

◮ Se o algoritmo é alimentado com uma outra entrada, como

o algoritmo se comporta?

◮ Se o algoritmo leva 1 segundo para executar para uma

(34)

◮ Seja um programa comninstruções. Então, o tempo total

de execução do programaTé dado por

T=

n X

i=1 (tini)

◮ onde: ◮ t

ié o tempo de execução da instruçãoi ◮ n

ié o número de vezes que a instruçãoié executada

◮ Entretanto, como o tempo de execução da instruçãoié

sempre de difícil obtenção, avalia-se o tempo total

considerando somente o número de vezes que a instrução é executada.

◮ Esse número é chamado de contagem de frequência ou

simplesmentefrequência da instruçãoi.

(35)

◮ Exemplos de determinação da frequênciaf de uma

instrução.

◮ Comando que pertence a uma sequência simples: tem

frequênciaf =1.

x = x + 1;

◮ Se essa instrução pertencer a uma estrutura de repetição for(i=1; i<=n; i++)

{

// ... x = x + 1; // ... }

◮ Nesse caso a instrução tem frequência:

f =

n X

(36)

◮ Se a estrutura do exemplo anterior pertencer a outra

estrutura de repetição:

for(j=1; j<=n; j++) {

// ...

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

// ... x = x + 1; // ... }

// ... }

◮ Nesse caso a instrução tem frequência:

f =

n X

j=1

n X

i=1

1

!

=

n X

j=1

n=n2

(37)

◮ A maior frequência encontrada em um programa é

chamada deordem de grandezade crescimento de tempo

do programa.

◮ A ordem de grandeza de um algoritmo é o principal

parâmetro para análise do desempenho de sua execução.

◮ SejaNum parâmetro que caracteriza o tamanho de um

problema.

◮ As ordens de grandeza mais comuns nos algoritmos são: ◮ O(1)→constante

◮ O(log

2N)→logaritmo

◮ O(N)linear ◮ O(Nlog

2N)

(38)

Figura:Ordens de grandeza.

(39)

◮ Exemplo: Analisar a solução iterativa de um algoritmo que

leia um valor inteiroN, calcule e imprima o seu fatorial. Se Nfor negativo, imprimir uma mensagem de erro.

int n, c, fat = 1;

cout << "Digite n" << endl; cin >> n;

if(n >= 0) {

for(c = 1; c<=n; c++) fat = fat * c;

cout << "Fatorial = " << fat << endl; }

else

cout << "Valor negativo" << endl;

◮ O algoritmo será estudado para os seguintes casos:

(40)

◮ Cason<0

int n, c, fat = 1;

cout << "Digite n" << endl; // 1 cin >> n; // 1

if(n >= 0) // 1

{

for(c = 1; c<=n; c++) fat = fat * c;

cout << "Fatorial = " << fat << endl; }

else

cout << "Valor negativo" << endl; // 1

◮ Soma das frequências=4

(41)

◮ Cason=0

int n, c, fat = 1;

cout << "Digite n" << endl; // 1 cin >> n; // 1

if(n >= 0) // 1

{

for(c = 1; c<=n; c++) // 1

fat = fat * c;

cout << "Fatorial = " << fat << endl; // 1 }

else

cout << "Valor negativo" << endl;

(42)

◮ Cason=1

int n, c, fat = 1;

cout << "Digite n" << endl; // 1 cin >> n; // 1

if(n >= 0) // 1

{

for(c = 1; c<=n; c++) // 2

fat = fat * c; // 1 cout << "Fatorial = " << fat << endl; // 1 }

else

cout << "Valor negativo" << endl;

◮ Soma das frequências=7

(43)

◮ Cason=1

int n, c, fat = 1;

cout << "Digite n" << endl; // 1 cin >> n; // 1

if(n >= 0) // 1

{

for(c = 1; c<=n; c++) // n+1

fat = fat * c; // n cout << "Fatorial = " << fat << endl; // 1 }

else

cout << "Valor negativo" << endl;

(44)

Comportamento assintótico

◮ Para valores suficientemente pequenos den, qualquer

algoritmo custa pouco para ser executado, mesmo os ineficientes.

◮ Nesse caso, a escolha de um algoritmo não é um problema

crítico.

◮ É importante analisar algoritmos para grandes valores de n.

◮ Portanto, estuda-se ocomportamento assintóticodas

funções de complexidade de um programa, isto é, o comportamento para grandes valores den.

(45)

◮ De volta ao exemplo anterior do fatorial.

◮ No caso mais geraln>1, a soma das frequências é de

2n+5.

◮ Como deseja-se estudar o comportamento apenas paran

grande, pode-se desprezar as constantes e os termos de menor ordem.

◮ Assim, conclui-se que o programa possui complexidade

(46)

Exemplo - Algoritmo 1

◮ Analisar o tempo de processamento de um programa para

calcular o seguinte somatório (série geométrica):

S=

n X

i=0

xi

float soma(int x, int n) {

int soma = 0; // 1 for(int i=0; i<=n; i++) // n+2 {

int prod = 1; // n+1 for(int j=0; j<i; j++)

prod = prod*x; // Pn

i=0i

soma = soma + prod; // n+1 }

return soma; // 1

}

(47)

Exemplo - Algoritmo 1

◮ O tempo de processamentoT(n)desse programa é obtido

somando-se a execução de todas instruções listadas anteriormente.

T(n) =1+ (n+2) + (n+1) + (

n X

i=0

i) + (n+1) +1

T(n) =6+3n+

n X

i=0

i=6+3n+n(n+1)

2

T(n) = n

2

2 + 7n

2 +6

(48)

Exemplo - Algoritmo 2

◮ Pode-se utilizar um algoritmo conhecido comoalgoritmo

de Hornerpara realizar esse cálculo.

S=

n X

i=0

xi=1+x+x2+x3+. . .+xn

=1+x(1+x+x2+. . .+xn−1)

=1+x(1+x(1+x+. . .+xn−2)) =. . .

=1+x(1+x(1+x(1+. . .(1+x(1+x))). . .))

(49)

Exemplo - Algoritmo 2

◮ Algoritmo de Horner

float somaHorner(int x, int n) {

int i, soma = 0; // 1

for(i=0; i<=n; i++) // n+2

{

soma = soma*x + 1; // n+1 }

return soma; // 1

}

◮ O tempo de processamento éT(n) =2n+5

(50)

Exemplo - Algoritmo 3

◮ Fórmula fechada

S=

n X

i=0

xi= x

n+1

−1 x−1

◮ Para calcular a potência, usa-sepot(int x, int n)que

possui complexidadeT(n) = log2n+2

float soma(int x, int n) {

return (pot(x,n+1)-1)/(x-1); }

◮ Portanto, esse algoritmo éO(log 2n).

(51)

Exemplo

◮ Comparação dos três algoritmos ◮ Algoritmo 1:T(n) = n

2

2 +72n+6 ⇒ O(n2)

◮ Algoritmo 2:T(n) =2n+5 ⇒ O(n) ◮ Algoritmo 3:T(n) = log

(52)

◮ Tabela com tempos de execução.

◮ Algumas ordens de grandeza de complexidade tornam

proibitivo a aplicabilidade do algoritmo, devendo ser usado apenas quando não se conheça solução de menor complexidade.

(53)

Exercícios

1. Qual a complexidade do algoritmo abaixo?

int maior(int n, int v[]) {

int m = v[0];

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

if( v[i] >= m ) { m = v[i];

} }

(54)

Exercícios

2. Qual a complexidade das funçõesf,geh? int f(int n){

int i, soma=0; for(i=1; i<=n; ++i)

soma += 1; return soma; }

int g(int n){ int i, soma=0; for(i=1; i<=n; ++i)

soma += i + f(i); return soma;

}

int h(int n){

return f(n) + g(n); }

(55)

Exercícios

3. Apresente ao menos dois algoritmos para calcularxne

(56)

◮ Lembre-se da solução recursiva discutida na aula anterior.

float exp_rec(float x, int n) {

if(n < 0)

return exp_rec(1.0/x, -n); else if(n == 0)

return 1.0; else if(n == 1)

return x;

else if(n % 2 == 0)

return exp_rec(x*x, n/2); else

return x * exp_rec(x*x, (n-1)/2); }

(57)

float exp_sq(float x, int n) {

if(n < 0) { x = 1.0/x; n = -n; }

if(n == 0) return 1.0; float y = 1.0;

while(n > 1) { if(n % 2 == 0){

x = x*x; n = n/2; } else {

y = x*y; x = x*x; n = (n-1)/2; }

Imagem

Referências