MAC2166 1s2017 - Parte 3

Aula 01: Funções Recursivas

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.

Problema 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 2: 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 3:

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 4:

Encontrar o maior valor de uma lista.

Solução:

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


def maiorinteiro(L):
  n = len(L)
  return maiorinteiro_aux(L, n)
  

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.

Aula 02: Ordenação de listas

Problema 1:

Dada uma lista com n > 0 números inteiros, faça uma função para ordenar os seus elementos em ordem crescente.

Solução 1:

Utilizaremos a seguinte função auxiliar para realizar a troca entre os elementos da lista L nos índices i e j:

def troca(L, i, j):
  tmp = L[i]   #Guarda cópia de backup do valor L[i] em tmp.
  L[i] = L[j]
  L[j] = tmp
  
A primeira solução apresentada utiliza a função auxiliar abaixo que devolve o índice do maior elemento da lista compreendido entre os índices de 0 a n-1.
def indice_maior(L, n):
  imax = 0
  for i in range(1,n):
    if L[i] > L[imax]:
      imax = i
  return imax
  
No algoritmo abaixo, a cada passo o maior elemento da lista de n elementos é encontrado, e colocado na sua posição final definitiva de índice n-1 no final da lista. O processo se repete para o trecho da lista remanescente, composto pelos primeiros n-1 elementos ainda não ordenados (do índice zero até n-2), e assim por diante para subtrechos cada vez menores, até ter toda a lista ordenada. Este algoritmo é conhecido como algoritmo de ordenação por seleção simples.
def ordenacao_selecao(L):
  """Ordenação por seleção:
  A cada passo o maior elemento da lista é
  encontrado e colocado na sua posição
  final definitiva. O processo se repete para o
  trecho da lista remanescente."""
  n = len(L)
  for tam in range(n, 1, -1):
    imax = indice_maior(L, tam)
    troca(L, imax, tam-1)
  
No código acima, o laço controla em tam o tamanho da porção da lista a ser ordenada (elementos de L[0] até L[tam-1]). A chamada da função indice_maior busca e armazena em imax o índice do maior elemento presente no subtrecho do vetor a ser ordenado. O elemento L[imax] é então movido para a última posição do subtrecho (L[tam-1]), ocupando assim a sua posição definitiva. Inicialmente, o subtrecho a ser ordenado corresponde a toda a lista (elementos de L[0] até L[n-1]). A medida que os maiores valores são movidos para o final da lista, temos a correspondente redução do subtrecho remanescente a ser ordenado.

Solução 2:

O próximo algoritmo é conhecido como bubble sort. O laço mais interno percorre a lista em j comparando elementos consecutivos da lista (L[j] e L[j+1]) e trocando a sua ordem sempre que eles estiverem fora de ordem, isto é L[j] > L[j+1]. Após a primeira varredura, o maior elemento da lista vai ocupar a última posição da lista, ou seja sua posição final definitiva na lista ordenada. A medida que os maiores valores são movidos para o final da lista, temos a correspondente redução do subtrecho remanescente a ser ordenado, até que todo a lista esteja ordenada.

def ordenacao_bolha(L):
  n = len(L)
  for i in range(n-1, 0, -1):
    for j in range(0,i):
      if L[j] > L[j+1]:
        troca(L, j, j+1)
  
Uma segunda versão do algoritmo bubble sort é apresentada abaixo. Essa variante possui um segundo critério de parada do laço. Sempre que a lista é percorrida no laço mais interno, sem que haja trocas de elementos, podemos concluir que a lista já se encontra ordenada e podemos interromper o processo.
def ordenacao_bolha(L):
  n = len(L)
  for i in range(n-1,0,-1):
    houvetroca = False
    for j in range(0,i):
      if L[j] > L[j+1]:
        troca(L, j, j+1)
        houvetroca = True
    if not houvetroca:
      break
  

Solução 3:

Ordenação por inserção: Assume que uma parte da sequência já está ordenada, e insere mais um elemento no lugar apropriado.

def ordenacao_insercao(L):
  n = len(L)
  for i in range(0,n-1):
    # Insere L[i+1] em L[0],...,L[i].
    x = L[i+1]
    j = i
    while j >= 0 and L[j] > x:
      L[j+1] = L[j]
      j -= 1
    L[j+1] = x
  

Aula 03: Busca em listas e mais ordenação

Problema 1:

Faça uma função que, dada uma coleção de n > 0 valores inteiros, em uma lista L, e um inteiro x, testa se x pertence à lista.

Solução 1:

Solução por busca exaustiva percorrendo a lista.

def pertence(x, L):
  n = len(L)
  i = 0
  while i < n:
    if L[i] == x:
      return True
    i += 1
  return False
  

Solução 2:

Essa solução adiciona na posição seguinte a do último elemento o valor de x, conseguindo com isso a simplificação do laço que passa agora a realizar uma única comparação para cada iteração. Essa técnica de busca é conhecida como busca com sentinela.

# busca com sentinela
def pertence(x, L):
  n = len(L)
  i = 0
  L.append(x) #adiciona x no final da lista
  while L[i] != x:
    i += 1
  L.pop() #remove x do final da lista
  if i == n:
    return False
  else:
    return True
  

Solução 3:

Essa terceira solução se aplica somente a listas previamente ordenadas. A cada iteração, o valor procurado é comparado com o elemento na posição central da lista, eliminando metade do intervalo de busca. Essa solução é conhecida como algoritmo de busca binária.

# Busca binária:
def pertence(x, L):
  n = len(L)
  inic = 0
  fim = n-1
  while inic <= fim:
    meio = (inic+fim)//2
    if x == L[meio]:
      return True
    elif x > L[meio]:
      inic = meio+1
    else:
      fim = meio-1

  return False
  

Solução 4:

Essa solução apresenta o algoritmo de busca binária de modo recursivo.

# Busca binária recursiva:
def pertence_aux(x, L, inic, fim):
  meio = (inic+fim)//2
  if inic > fim:
    return False
  elif x == L[meio]:
    return True
  elif x > L[meio]:
    return pertence_aux(x, L, meio+1, fim)
  else:
    return pertence_aux(x, L, inic, meio-1)


def pertence(x, L):
  n = len(L)
  inic = 0
  fim = n-1
  return pertence_aux(x, L, inic, fim)
  

Problema 2:

Dada uma lista L com n > 0 números inteiros no intervalo de 0 a 255 (isto é, 0 <= L[i] <= 255 para i = 0,.., n-1), faça uma função para ordenar os seus elementos em ordem crescente.

Solução 1:

Explorando o fato de que os valores estão em um intervalo limitado de 0 a 255, podemo usar o algoritmo de ordenação do Counting Sort. Para isso vamos usar uma lista de contadores para cada valor de 0 a 255. Portanto, o algoritmo necessita de memória auxiliar.

def ordenacao_por_contagem(L):
  n = len(L)
  C = [0]*256 #Lista de contadores
  for i in range(0,n):
    C[L[i]] += 1
  #Agora C[k] contém o número de elementos iguais a k.
  i = 0
  for k in range(0,256):
    for c in range(0,C[k]):
      L[i] = k
      i += 1
Exemplo de função principal que chama as funções anteriores.

def main():
  L = [6,3,9,2,7,1,0,4,8,5]

  ordenacao_insercao(L)

  for i in range(0,n):
    print(L[i], end=" ")
  print()

  x = int(input("Digite x: "))
  if pertence(x, L):
    print("Sim")
  else:
    print("Não")

main()