Você está na página 1de 7

Escola Superior de Tecnologia

Instituto Politécnico de Castelo Branco

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

O processamento de listas é uma tarefa extremamente comum em Lisp, já que, como


vimos, as listas são utilizadas tanto como estruturas de dados como na representação
das expressões que constituem o próprio programa em Lisp. Embora tenhamos
verificado que existe uma função dolist que permite percorrer listas de forma iterativa,
frequentemente é mais natural e/ou mais elegante implementar de forma recursiva as
funções de processamento de listas.

A programação recursiva embora comum em Lisp, requer aos utilizadores de outras


linguagens de programação algum esforço de modificação da forma de pensar
predominantemente iterativa que vai sendo adquirida com a utilização de linguagens
como o Pascal, C, etc.

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:

Para processar recursivamente uma lista simples devemos:


1. Testar a condição de paragem
1. 2. Processar o primeiro elemento da lista
3. Chamar de novo a função (aqui está a recursividade) passando o resto da
lista como argumento.

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)))))

> (occurs 'a '(a b a c a d))


3
Vejamos se a definição acima respeita a estrutura geral para recursividade em listas
simples que definimos acima:

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.

2. Seguidamente a função testa se o primeiro elemento da lista é ou não o elemento


que pretendemos contar. Isto corresponde ao passo de processamento do primeiro
elemento da lista.

3. Em função do primeiro elemento ser ou não o que pretendemos contar, a função


devolve respectivamente 1 mais o número de ocorrências no resto da lista ou
simplesmente o número de ocorrências no resto da lista. Em ambos os casos a
função é de novo chamada com o resto da lista como argumento.

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:

(defun sub (e1 e2 l)


(if (null l)
nil
(if (equal e1 (first l))
(cons e2 (sub e1 e2 (rest l)))
(cons (first l) (sub e1 e2 (rest l))))))

> (sub 'a 'b '(a b a c a d))


(B B B C B D)

Vejamos de novo se a definição acima respeita a estrutura referida:

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.

2. A função testa a seguir se o primeiro elemento da lista é ou não aquele que


pretendemos substituir. Isto corresponde ao passo de processamento do primeiro
elemento da lista.

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.

Os exemplos anteriores ilustram a forma como a generalidade das funções recursivas


que processam listas obedecem a uma estrutura comum, mesmo quando varia o seu
nível de complexidade.

Seguindo a estrutura descrita acima implemente as seguintes funções de forma recursiva:

1. my-remove que recebe uma lista e um elemento e devolve uma nova lista onde foi removido o
elemento.

2. my-length que recebe uma lista e devolve o tamanho da lista.

2
Quando e Como Parar

Os exemplos e exercícios acima ilustram ainda dois pontos importantes sobre a


recursividade: com garantir que a condição de paragem é atingida e o que devolver
quando tal acontece.

Em relação a atingir a condição de paragem duas regras simples devem ser


seguidas:
1. Pelo menos um dos parâmetros deve ser modificado na chamada recursiva
à função.
2. A condição de paragem deve testar esse parâmetro.

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)))))

Neste exemplo o parâmetro numérico b é usado para controlar a recursividade. Vai


sendo decrementado na chamada e comparado com 0 na condição de paragem.

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.

1. Quando o valor devolvido pela função é obtido através de uma soma,


geralmente devolvemos 0 na condição de paragem.
2. Quando o valor devolvido pela função é obtido através de um produto,
geralmente devolvemos 1 na condição de paragem.
3. Quando a função devolve uma lista construída utilizando cons, geralmente
devolvemos nil na condição de paragem.

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)

Recursividade em Listas Imbricadas

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:

Para processar recursivamente uma lista imbricada devemos:


1. Testar a condição de paragem
2. Verificar se o primeiro elemento é um átomo ou uma lista
i. Se for um átomo é processado.
ii. Se for uma lista chamamos a função recursivamente passando o
elemento como argumento.
3. Chamar de novo recursivamente a função passando o resto da lista como
argumento.

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)))))))

> (elimina 'a '((a b c) a b c ((a b c) a b c)))


((B C) B C ((B C) B C))

Esta definição respeita a estrutura geral que definimos acima:

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:

> (encontra ‘a ‘(c d (a b) e))

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.

(defun palindromep (lst)


(equal lst (reverse lst)) )

Assim, por exemplo:

>(palindromep '(1 2 3 4 5 4 3 2 1))


T
>(palindromep '(a b b a))
T
>(palindromep '(1 2 3))
NIL

Escreva uma versão recursiva da função palindromep e chame-lhe r-


palindromep. Não deve fazer uso da função reverse.

2. Uma expressão matemática em Lisp tem uma apresentação semelhante a:

(+ (* 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:

((1 * 2 * pi) + 3 + (4 - 5))

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.

8. A série de Finonacci é definida da seguinte forma:

fib(n) = {fib(n-1) + fib(n-2) Se n>1 }


{1 Se n=0 or n=1}

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).

(defun mystery (n)


(cond ((= n 0) 0)
(t (mystery (- n 1)))))

Você também pode gostar