Aulas Práticas de
Fundamentos de Inteligência Artificial
Arlindo Silva
Ana Paula Neves
Aula
4
Técnicas de Programação Recursiva
Recursividade em Listas Simples e Imbricadas
Recursividade em Listas Simples
As operações básicas sobre listas simples, por exemplo encontrar, remover ou substituir
um elemento podem de forma imediata ser definidas recursivamente. Estas definições
possuem uma estrutura comum que podemos replicar em outras funções que
pretendamos implementar:
Vamos testar esta estrutura utilizando como exemplo a função occurs que conta o
número de ocorrências de um elemento numa lista:
(defun occurs (e l)
(if (null l)
0
(if (equal e (first l))
(+ 1 (occurs e (rest l)))
(occurs e (rest l)))))
1. A primeira coisa que a função faz é verificar se a lista que recebe é a lista vazia. Se
tal for o caso termina devolvendo 0. Este é portanto o caso de paragem.
Vejamos agora um segundo exemplo, desta vez uma função replace que substitui
todas as ocorrências de determinado elemento por outro numa lista argumento:
1. Mais uma vez a primeira coisa a ser feita é a verificação se a lista recebida é vazia ou
não. Neste caso a função também termina mas agora devolve nil (a lista vazia). O
caso de paragem fica assim resolvido.
3. Caso o primeiro elemento seja o que pretendemos substituir, a função devolve uma
lista com o novo elemento à cabeça. Se o primeiro elemento não for aquele que
queremos substituir, a nova lista tem o mesmo elemento à cabeça do que a lista
inicial. O resto da lista resulta da substituição do elemento em causa no resto da lista,
ou seja da chamada recursiva à função com o resto da lista como argumento.
1. my-remove que recebe uma lista e um elemento e devolve uma nova lista onde foi removido o
elemento.
2
Quando e Como Parar
O objectivo das regras acima é levar a que um parâmetro na chamada recursiva atinja o
valor que é testado na condição de paragem. Nos exemplos anteriores vimos que
passando consecutivamente o resto de uma lista, recebida como parâmetro, mais cedo
ou mais tarde vamos fazer uma chamada recursiva com argumento nil. Se a condição
de paragem for a comparação da lista com nil então é garantida a paragem da
chamada recursiva. Se o parâmetro utilizado para controlar a recursividade não for uma
lista mas antes um número, no caso mais simples o número deve ser decrementado na
chamada recursiva e comparado a 0 na condição de paragem. No exemplo seguinte é
calculada a potência positiva de um número:
> (pot 2 3)
8
(defun pot (a b)
(if (= b 0)
1
(* a (pot a (- b 1)))))
Quando a condição de paragem é atingida temos ainda que pensar em qual o valor que a
função deve retornar. Mais uma vez podemos utilizar algumas linhas de orientação para
decidir qual o valor a retornar.
As regras que apresentámos neste ponto podem ajudar em muitos casos de definição
recursiva de funções. Lembre-se no entanto que devem ser extrapoladas para cada caso
e não seguidas cegamente, já que apenas podem ser vistas como linhas gerais de
orientação.
1. Redefina a função pot de maneira a que a função consiga manipular expoentes negativos (deve
continuar a ser uma função recursiva).
3
2. Defina recursivamente uma função my-remove que remova todas as ocorrências de
um elemento numa lista. Por exemplo:
> (my-remove 'a '(a b a c a))
(B C)
Além das listas simples discutidas no ponto anterior é necessário por vezes executar
tarefas sobre listas cujos elementos podem por sua vez ser listas. Estas listas são
comummente chamadas lista imbricadas. Em Lisp, é por exemplo, comum representar
árvores através de listas imbricadas. As próprias expressões do Lisp podem ser vistas
como árvores em que o nome da função corresponde à raiz e as expressões argumento
a sub-árvores ligadas a essa raiz. O processamento recursivo de listas imbricadas
diverge ligeiramente do das listas simples:
Usando esta estrutura implementámos uma função que elimina todas as ocorrências de
um elemento numa lista imbricada:
(defun elimina (e l)
(if (null l)
nil
(cond ((atom (first l)) (if (equal e (first l))
(elimina e (rest l))
(cons (first l)
(elimina e (rest l)))))
((listp (first l)) (cons (elimina e (first l))
(elimina e (rest l)))))))
1. Verificamos se a lista recebida é vazia ou não. Este teste permite terminar todos os
ramos do processo recursivo. Vejam que a nova lista é construída utilizando o cons,
logo é uma boa ideia devolver uma lista vazia quando a condição de paragem é
satisfeita.
2. Se o primeiro elemento da lista for um átomo ele é processado. Neste caso vamos
verificar se se trata do elemento a apagar. Caso seja o elemento a apagar, a nova
lista é o resultado da remoção recursiva das outras ocorrências no resto da lista.
4
Caso não seja o elemento a apagar, a nova lista vai ter esse elemento à cabeça,
enquanto que o resto também vai ser obtido removendo recursivamente as restantes
ocorrências do resto da lista. Note que em ambos os casos, após o tratamento do
primeiro elemento, o resto da lista continua a ser processado recursivamente.
3. Se o primeiro elemento da lista for também uma lista é feita uma chamada recursiva
com o elemento como argumento, após o que o resto da lista é também processado
recursivamente. A lista devolvida é o cons da lista elemento com as ocorrências
apagadas, com o resto da lista inicial onde as ocorrências também vão ser
eliminadas.
1. Implemente uma função recursiva encontra que devolva t se encontrar um átomo que recebe
como parâmetro, numa lista imbricada. Por exemplo:
Exercícios
1. Uma palavra palíndrome é uma palavra cuja leitura da esquerda para a direita é igual
à leitura da direita para a esquerda. A função a baixo verifica se a lista que recebe
como argumento é um palíndrome.
(+ (* 1 2 pi) 3 (- 4 5))
Para a maioria das pessoas a leitura da expressão seria facilitada caso fosse
utilizada uma notação infix:
5
Escreva uma função infix que receba expressões matemáticas no formato do Lisp
e que retorne a versão correspondente no formato infix.
4. Escreva uma função my-replace a qual deve receber uma lista e dois elementos
como argumentos e que retorne uma lista a qual deve resultar da substituição de
todas as ocorrências do primeiro elemento pelo segundo na lista argumento.
5. Escreva uma função conta_atomos que conte todos os elementos átomos de uma
lista imbricada que recebe como argumento.
6. Escreva uma função insere que deve receber como argumento uma lista e dois
átomos e retorne uma lista que deve resultar da inserção do segundo átomo à direita
de todas as ocorrências do primeiro átomo na lista argumento.
7. Escreva uma função my-merge que deve receber duas listas de números de igual
tamanho. A função deverá somar os números correspondentes de cada uma das
listas e retornar o produto dos resultados de todas as somas. Por exemplo, (merge
‘(1 2 3) ‘(2 2 2)) deverá retornar 60, dado que (1 + 2) *(2 + 2) * (3 + 2) = 60.
Implemente uma função recursiva que permita calcular o nth número de Fibonacci.
9. Será que a função seguinte termina sempre? (Deverá considerar todos os casos).