Aula 19: Funções Recursivas

Tópicos

Aulas a distância - Google Meet:

Para verem os vídeos, vocês devem estar logados no e-mail da usp.

Uma função é dita recursiva quando dentro dela é feita uma ou mais chamadas a ela mesma.

A ideia é dividir um problema original um subproblemas menores de mesma natureza (divisão) e depois combinar as soluções obtidas para gerar a solução do problema original de tamanho maior (conquista). Os subproblemas são resolvidos recursivamente do mesmo modo em função de instâncias menores, até se tornarem problemas triviais que são resolvidos de forma direta, interrompendo a recursão.

Exemplo 1:

Calcular o fatorial de um número.

Solução 1:

Solução não recursiva.

def fatorial(n):
 fat = 1
 while n > 1:
    fat *= n
    n -= 1
 return fat
  

Solução 2:

Solução recursiva: n! = n.(n-1)!

fatorial

def fatorial(n):
  if n == 0: #Caso trivial
    return 1 #Solução direta
  else:
    return n*fatorial(n-1) #Chamada recursiva
  

Problema 1: Cálculo da série de Fibonacci

A sequência de Fibonacci consiste em uma série de números, tais que, definindo seus dois primeiros números como sendo 0 e 1, os números seguintes são obtidos através da soma dos seus dois antecessores.

fibonacci

Exemplo da sequência:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, ...
A ocorrência da sucessão de Fibonacci na natureza é tão frequente que é difícil acreditar que seja acidental (ex: flores, conchas, mão humana).

fibonacci_exemplos

Solução 1:

Versão recursiva:

def fibo(n):
  if n <= 1:
    return n
  else:
    return fibo(n-1) + fibo(n-2)
  

Solução 2:

Versão não recursiva:

def fibo(n):
  f0 = 0
  f1 = 1
  for k in range(1,n+1):
    f2 = f0 + f1
    f0 = f1
    f1 = f2
  return f0
  
Análise de eficiência das funções Fibonacci calculando o número de operações de soma S(n) realizadas:

fibonacci

Neste caso não faz sentido utilizar a versão recursiva.

Árvore de Recorrência:

A solução recursiva é ineficiente, pois recalcula várias vezes a solução para valores intermediários:

Por exemplo, para F(6)=fibo(6) = 8, temos 2xF(4), 3xF(3), 5xF(2).

arvore_de_recorrencia

Problema 2:

Calcular x elevado a n positivo.

Solução 1:

Solução não recursiva.

def potencia(x, n):
  pot = 1.0
  while n > 0:
    pot *= x
    n -= 1
  return pot
  

Solução 2:

Solução recursiva: x^n = x . x^(n-1)

def potencia(x, n):
  if n == 0:    #Caso trivial
    return 1.0  #Solução direta
  else:
    return x*potencia(x, n-1) #Chamada recursiva
  

Solução 3:

Solução recursiva: x^n = x^(n/2). x^(n/2) = (x^(n/2))^2

def potencia(x, n):
  if n == 0: #Caso trivial
    return 1.0 
  elif n%2 == 0: #Se n é par...
    pot = potencia(x, n//2)
    return pot*pot
  else: #Se n é ímpar...
    pot = potencia(x, n//2)
    return pot*pot*x
  

Problema 3:

Encontrar o maior valor de uma lista de modo recursivo.

Solução 1:

Realiza a chamada recursiva em uma cópia de um subtrecho da lista com um elemento a menos, após fatiamento que não copia o último elemento.

def maiorValor(L):
  if len(L) == 1:
    return L[0]
  else:
    m = maiorValor(L[:len(L)-1])
    if m > L[len(L)-1]:
      return m
    else:
      return L[len(L)-1]
  

Solução 2:

Realiza a chamada recursiva em uma cópia de um subtrecho da lista com um elemento a menos, após fatiamento que não copia o primeiro elemento.

def maiorValor(L):
  if len(L) == 1:
    return L[0]
  else:
    m = maiorValor(L[1:])
    if m > L[0]:
      return m
    else:
      return L[0]
  

Solução 3:

Realiza a chamada recursiva no subtrecho da lista com um elemento a menos, após a remoção do seu último elemento via o método pop(). Este último elemento é então posteriormente reposto na lista a fim de preservar o seu conteúdo original, através do método append().

def maiorValor(L):
  if len(L) == 1:
    return L[0]
  else:
    ult = L.pop()
    m = maiorValor(L)
    L.append(ult)
    if m > ult:
      return m
    else:
      return ult
  

Solução 4:

Evita o uso de fatiamento de listas e de remoções, usando uma segunda função auxiliar maiorValorAux(L,n) com um parâmetro extra para realizar a recursão. A função maiorValor(L) passa a ser apenas uma função de embrulho (wrapper function).

#Encontra o maior valor de uma lista com n elementos.
def maiorValorAux(L, n):
  if n==1: #Caso trivial
    return L[0]
  else:
    m = maiorValorAux(L, n-1)
    if m > L[n-1]:
      return m
    else:
      return L[n-1]


def maiorValor(L):
  n = len(L)
  return maiorValorAux(L, n)
  

Problema 4:

Faça uma função recursiva que recebe uma lista e que devolve uma segunda lista de mesmo comprimento, mas com os elementos em ordem reversa.

Solução 1:

def inverte(L):
    n = len(L)
    if n > 1:
        T = inverte(L[:n-1])
        u = L[n-1]
        return [u]+T        
    else:
        return L[:]

def main():
    L = [1,2,3,4,5]
    I = inverte(L)
    print(I)

main()
  

Solução 2:

def inverte(L):
    n = len(L)
    if n <= 1:
        return L[:] #return [ L[0] ] #return list(L)
    else:
        A = L[1:n]
        Ainv = inverte(A)
        Ainv.append(L[0])
        return Ainv
  

Problema 5: Torre de Hanoi

São dados um conjunto de n discos com diferentes tamanhos e três bases A, B e C.

O problema consiste em imprimir os passos necessários para transferir os discos da base A para a base B, usando a base C como auxiliar, nunca colocando discos maiores sobre menores.

Exemplo:

hanoi_1

1º passo: Mover de A para B.
hanoi_2

2º passo: Mover de A para C.
hanoi_3

3º passo: Mover de B para C.
hanoi_4

4º passo: Mover de A para B.
hanoi_5

5º passo: Mover de C para A.
hanoi_6

6º passo: Mover de C para B.
hanoi_7

7º passo: Mover de A para B.
hanoi_8

Animação da sequência completa de movimentos:
hanoi

Solução:

def hanoi(n, orig, dest, aux):
  if n == 1:
    print("Mover de",orig,"para",dest)
  else:
    hanoi(n-1, orig, aux, dest)
    print("Mover de",orig,"para",dest)
    hanoi(n-1, aux, dest, orig)


def main():
  n = int(input("Digite a quantidade de discos: "))
  hanoi(n, "A", "B", "C")

main()
  
O número de movimentos necessários para mover n discos é 2n - 1.