Você está na página 1de 11

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

3
Recursividade e Iteratividade
Definição Iteriva e Recursiva de Funções
Definição Recursiva de Funções

Em programação é extremamente comum a necessidade de realizar uma determinada


tarefa um número elevado de vezes. O Lisp, tal como a generalidade das linguagens de
programação, fornece dois paradigmas diferentes para o controlo de repetições:
recursividade e iteratividade.

A recursividade é talvez o paradigma que melhor se adequa ao espírito do Lisp, já que


constitui a forma mais natural de processar listas. A característica fundamental de uma
função recursiva é o facto de uma chamada a essa função poder resultar em novas
chamadas à mesma função, i.e. uma função pode-se chamar a si própria. Eis um
exemplo típico de definição recursiva de uma função que calcula o factorial de um
número inteiro:

(defun factorial (x)


(if (= x 0)
1
(* x (factorial (- x 1)))))

> (factorial 3)
6

Observando o exemplo de interacção acima é óbvio que a função funciona. Mas como é
que funciona? Como é que uma função se pode chamar a si própria? Utilizando uma
ferramenta do Lisp vamos fazer um trace das chamadas recursivas da função:

> (trace factorial)


(FACTORIAL)

> (factorial 3)
; 1> FACTORIAL called with arg:
; 3
; | 2> FACTORIAL called with arg:
; | 2
; | | 3> FACTORIAL called with arg:
; | | 1
; | | | 4> FACTORIAL called with arg:
; | | | 0
; | | | 4< FACTORIAL returns value:
; | | | 1
; | | 3< FACTORIAL returns value:
; | | 1
; | 2< FACTORIAL returns value:
; | 2
; 1< FACTORIAL returns value:
; 6

Podemos agora tentar explicar o que acontece em resultado da chamada (factorial


3):

§ Quando a primeira chamada à função é realizada (chamada 1 no trace acima) o


parâmetro x, que é uma variável local à função, vai ter valor 3. Como 3 não é
igual a 0, a comparação do if vai falhar e a expressão correspondente ao else
vai ser avaliada. Essa expressão corresponde simplesmente ao produto da
variável x e do resultado de uma nova chamada a factorial, agora com
parâmetro (- x 1), ou seja 2. Para que este produto seja obtido, a chamada a
factorial tem de ser executada e o seu resultado devolvido e só depois a
execução desta chamada pode ser concluída.

§ A nova chamada (chamada 2 no trace acima) recebe como parâmetro o inteiro 2.


A variável x, que é uma nova variável local a esta chamada da função e nada
tem a ver com a variável x da chamada anterior, vai ser comparada com 0 e
como a comparação falha é de novo o ramo else do if que é avaliado. Tal como
na chamada anterior, a avaliação do else só pode ser efectuada se uma nova
chamada a factorial acontecer, agora com parâmetro 1.

§ Este processo vai-se repetir até que (factorial 0) é chamado (chamada 4


no trace acima). Nesta chamada a variável local x vai ter valor 0 e por isso o
teste do if vai ser verdadeiro e é o ramo then e não o else a ser avaliado. Como
o then avalia simplesmente para 1, este valor é devolvido e não há mais
nenhuma chamada recursiva à função. É por isto que a condição do if que
permite parar de chamar recursivamente a função, se chama condição de
paragem. Sem ela a função continuaria a chamar-se recursivamente até ocorrer
um overflow da pilha.

§ Como não há chamada recursiva, a execução da chamada 4 à função termina e


o valor 1 é devolvido à chamada 3. Veja como no trace retornamos da chamada
4 à função, para a chamada 3.

§ Na chamada 3 tínhamos parado quando íamos multiplicar a variável local x,


com o valor 1 pelo factorial de 0. O resultado deste produto é agora 1 e podemos
terminar a chamada 3 à função retornando esse valor para a chamada 2.

§ Na chamada 2 o valor retornado é multiplicado pelo valor de x (aqui é 2), sendo


que a chamada pode agora também terminar, devolvendo o valor 2 à chamada
1.

§ Na chamada 1 tínhamos parado na tentativa de multiplicar x (com valor 3) pelo


factorial de (- x 1), ou seja 2. A multiplicação pode agora ser feita, sendo
obtido o valor 6, que é devolvido como o resultado da chamada inicial à função.

É interessante observar como o trace foi inicialmente incrementando o nível de


recursividade até a condição de paragem ter sido atingida, ponto a partir do qual esse

2
nível foi diminuindo até voltarmos ao nível inicial. Esta ferramenta é extremamente útil
para observar o comportamento das nossas funções recursivas, sobretudo no que diz
respeito à satisfação da condição de paragem e aos valores retornados por cada
chamada. O trace pode ser desligado chamando (untrace <nome-da-função>).

A descrição acima salienta dois dos pontos essenciais na implementação de uma função
recursiva. Em primeiro lugar precisamos de uma definição recursiva da função a
implementar. Neste caso:

O factorial de um número inteiro x é igual ao produto desse número pelo


factorial desse número decrementado.

Veja como a definição é circular, factorial é definido em termos de factorial. Em seguida


necessitamos de uma condição que permita decidir se a recursividade deve parar. Neste
caso:

O factorial de 0 é 1.

Quando pretender implementar uma função recursiva, comece sempre por definir e
escrever em qualquer lado a definição recursiva e a condição de paragem. Depois disto
feito, a implementação deve estar bastante simplificada: comece a função pelo teste à
condição de paragem. Se o teste for verdadeiro devolva o valor definido na condição. Se
o teste for falso execute o bloco em que a chamada recursiva é realizada.

1. Modifique a definição de factorial feita anteriormente, acrescentando uma segunda condição


de paragem para factorial de 1, de maneira a poupar uma chamada recursiva em cada
chamada da função.

2. Escreva uma função recursiva que calcule o resultado de elevar um número x à potência y. Não se
esqueça de seguir os passos recomendados anteriormente, escrevendo primeiro uma definição
recursiva e depois a condição de paragem da função.

Exemplo de Recursividade com Listas

Eis um exemplo de uma função que processa uma lista de maneira recursiva:

(defun apanha-listas (x)


(if (null x)
nil
(if (listp (first x))
(append (first x) (apanha-listas (rest x)))
(apanha-listas (rest x)))))

> (apanha-listas '(1 (a) ola (b c) 9))


(A B C)

A função apanha-listas recebe uma lista e devolve uma outra lista que resulta da
concatenação de todos os elementos da lista argumento que forem por sua vez listas. A
definição recursiva é a seguinte:

Para concatenarmos todos os elementos de uma lista que forem por sua vez listas
devemos:

3
§ Se o primeiro elemento for uma lista, concatenar este elemento
com a concatenação das listas presentes na cauda (no resto) da
lista principal e devolver o resultado.
§ Se o primeiro elemento na for uma lista, devemos devolver a
concatenação de todas as listas presentes na cauda da lista
principal.

A condição de paragem é:

Se o argumento for uma lista vazia, o resultado da concatenação de todos os


elementos que forem listas (não há nenhum!) é a lista vazia.

Verifique a maneira como a definição e a condição de paragem foram implementadas


quase de forma directa na função em Lisp. Tente seguir o trace seguinte da chamada
anterior à função apanha-listas.

> (trace apanha-listas)


(APANHA-LISTAS)

> (apanha-listas '(1 (a) ola (b c) 9))


; 20> APANHA-LISTAS called with arg:
; (1 (A) OLA (B C) 9)
; | 21> APANHA-LISTAS called with arg:
; | ((A) OLA (B C) 9)
; | | 22> APANHA-LISTAS called with arg:
; | | (OLA (B C) 9)
; | | | 23> APANHA-LISTAS called with arg:
; | | | ((B C) 9)
; | | | | 24> APANHA-LISTAS called with arg:
; | | | | (9)
; | | | | | 25> APANHA-LISTAS called with arg:
; | | | | | NIL
; | | | | | 25< APANHA-LISTAS returns value:
; | | | | | NIL
; | | | | 24< APANHA-LISTAS returns value:
; | | | | NIL
; | | | 23< APANHA-LISTAS returns value:
; | | | (B C)
; | | 22< APANHA-LISTAS returns value:
; | | (B C)
; | 21< APANHA-LISTAS returns value:
; | (A B C)
; 20< APANHA-LISTAS returns value:
; (A B C)

(A B C)

1. Defina uma função recursiva que receba uma lista e conte o número de elementos que são
números. Use o trace para observar o comportamento da sua função.

2. Defina uma função recursiva que receba uma lista e devolva uma nova lista com os elementos da
primeira que forem números. Não tente usar a função append! Use o trace para observar o
comportamento da sua função.

4
Definição Iterativa de Funções

O Lisp fornece um conjunto alargado de funções que permitem realizar determinada


tarefa iterativamente, ou seja permitem repetir uma acção um determinado número de
vezes. Vamo-nos preocupar aqui apenas com as duas de uso mais frequente: dotimes
e dolist.

dotimes

Tal como o nome indica, o dotimes permite-nos avaliar uma expressão (ou conjunto de
expressões) um determinado número de vezes. Tem a seguinte estrutura:

(dotimes (<contador> <limite> <resultado>) <corpo>)

Contador é uma variável local que é inicializada a 0 e incrementada a cada execução do


corpo. Limite é uma expressão que ao ser avaliada deve resultar num inteiro e
corresponde ao valor máximo que o contador vai atingir. Resultado é opcional:
quando existe é avaliado quando o ciclo termina e o seu valor é devolvido. Se não existir
o dotimes devolve nil. Corpo é constituído pela expressão (ou expressões a avaliar).
Eis uma implementação agora iterativa da função potencia que implementou
recursivamente num exercício anterior:

(defun potencia (x y)
(let ((r x))
(dotimes (i (- y 1) r)
(setq r (* x r)))))

> (potencia 2 3)
8

A função anterior limita-se a multiplicar o valor x (- y 1) vezes para obter a potência y


de x (2^3=2*2*2). Neste caso o contador é i que vai sendo incrementado até (- y
1), que é o limite. A cada iteração vai-se multiplicando x pelo valor armazenado em r
(uma variável auxiliar local inicializada a x). No fim o resultado é r.

Criação de variáveis locais

Um facto interessante na definição anterior tem a ver com a utilização do let para criar a
variável local r, a qual era necessária para utilizar como acumulador durante o ciclo
dotimes. O let tem a seguinte estrutura:

(let ((<var1> <exp1>)


...............
(<varn> <expn>))
<corpo>)

Referimos anteriormente que era de evitar a criação indiscriminada de variáveis globais


com setq ou setf e que sempre que possível deveríamos utilizar variáveis locais.
Acrescentamos agora que essas variáveis devem apenas existir enquanto são
estritamente necessárias. O let permite-nos criar n variáveis locais que apenas existem
enquanto o parêntesis não é fechado. Para tal recebe como argumento uma lista de
pares (nome-de-variável expressão-de-inicialização). As variáveis criadas existem apenas

5
no corpo do let, o qual pode ser constituído por qualquer número de expressões (tal
como o corpo de uma função). Quando o parêntesis do let é fechado as variáveis locais
deixam de existir. O let devolve o resultado da avaliação da última expressão do seu
corpo. Eis um exemplo:

> (let ((a 2)


(b 3))
(* a b))
6

Note que o let não permite inicializar uma variável em função de outra variável
inicializada anteriormente no mesmo let:

> (let ((a 2)


(b (+ a 1)))
(* a b))
;; Error: Unbound variable A in #<function 0 #xF3CB6C>

Uma solução possível para este problema é o encadeamento de lets:

> (let ((a 2))


(let ((b (+ a 1)))
(* a b)))
6

Mais simples ainda é a utilização do let*, o qual na prática encadeia a criação de


variáveis, mas sem nós termos de utilizar vários lets:

> (let* ((a 2)


(b (+ a 1)))
(* a b))
6

Note que o let* só deve ser utilizado quando é necessário inicializar variáveis em
função de outras variáveis definidas no mesmo let!

1. Escreva uma versão iterativa da função factorial.

2. Escreva uma função que receba dois inteiros x y e que calcule a soma dos produtos de todos os
números que vão de 1 a x por todos os números que vão de 1 a y Por exemplo, se x=2 e y
=2 o resultado deverá ser 1*1+1*2+2*1+2*2.

dolist

O dolist é semelhante ao dotimes mas serve para iterar ao longo de listas. A sua
estrutura é:

(dolist (<elemento> <lista> <resultado>) <corpo>)

O dolist percorre a lista atribuindo, em cada iteração, um novo elemento em


elemento, avaliando de seguida todas as expressões que compõe o corpo. Quando o

6
fim da lista é atingido dolist devolve o resultado da avaliação de resultado ou nil
se aquele não for fornecido. Eis um exemplo de utilização do dolist para escrever uma
versão iterativa de apanha-listas:

(defun apanha-listas (x)


(let ((r nil))
(dolist (e x r)
(if (listp e)
(setq r (append r e))))))

> (apanha-listas '(1 (a) ola (b c) 9))


(A B C)

Nesta função a lista vai sendo percorrida com o dolist e cada elemento (atribuído a e)
vai ser testado com o listp. Caso seja uma lista, é concatenado com a lista resultado r.
Quando o ciclo termina r é devolvido.

1. Defina uma função iterativa que conte e devolva o número de sub-listas numa lista que recebe como
argumento.

2. Defina uma função iterativa que receba uma lista e devolva uma nova lista com os elementos da
primeira que forem números. Não tente usar a função append! Eu sei que me estou a repetir mas o
append não deve ser utilizado para construir listas mas sim para as concatenar!

Recursividade Vs. Iteratividade

Uma pergunta que surge frequentemente em Lisp é se uma determinada função deve ser
implementada de forma recursiva ou de forma iterativa. Os exemplos desta aula podem
ser implementados de forma recursiva ou de forma iterativa com a mesma facilidade. No
entanto nem sempre é assim. Imagine que pretende contar o número de vezes que o
símbolo a surge na seguinte lista (que representa uma árvore binária):

((a (a (c a))) ((b (a a)) (c (c a))))

É muito mais fácil uma implementação recursiva da função que faz a contagem:

(defun conta-as (l)


(cond
((null l) 0)
((listp (first l)) (+ (conta-as (first l)) (conta-as (rest
l))))
((equal (first l) 'a) (+ 1 (conta-as (rest l))))
(t (conta-as (rest l)))))

Vamos observar o comportamento da função num trace com a lista anterior como
argumento:

> (conta-as '((a (a (c a))) ((b (a a)) (c (c a)))))


6

7
> (trace conta-as)
(CONTA-AS)
> (conta-as '((a (a (c a))) ((b (a a)) (c (c a)))))
; 27> CONTA-AS called with arg:
; ((A (A (C A))) ((B (A A)) (C (C A))))
; | 28> CONTA-AS called with arg:
; | (A (A (C A)))
; | | 29> CONTA-AS called with arg:
; | | ((A (C A)))
; | | | 30> CONTA-AS called with arg:
; | | | (A (C A))
; | | | | 31> CONTA-AS called with arg:
; | | | | ((C A))
; | | | | | 32> CONTA-AS called with arg:
; | | | | | (C A)
; | | | | | | 33> CONTA-AS called with arg:
; | | | | | | (A)
; | | | | | | | 34> CONTA-AS called with arg:
; | | | | | | | NIL
; | | | | | | | 34< CONTA-AS returns value:
; | | | | | | | 0
; | | | | | | 33< CONTA-AS returns value:
; | | | | | | 1
; | | | | | 32< CONTA-AS returns value:
; | | | | | 1
; | | | | | 35> CONTA-AS called with arg:
; | | | | | NIL
; | | | | | 35< CONTA-AS returns value:
; | | | | | 0
; | | | | 31< CONTA-AS returns value:
; | | | | 1
; | | | 30< CONTA-AS returns value:
; | | | 2
; | | | 36> CONTA-AS called with arg:
; | | | NIL
; | | | 36< CONTA-AS returns value:
; | | | 0
; | | 29< CONTA-AS returns value:
; | | 2
; | 28< CONTA-AS returns value:
; | 3
; | 37> CONTA-AS called with arg:
; | (((B (A A)) (C (C A))))
; | | 38> CONTA-AS called with arg:
; | | ((B (A A)) (C (C A)))
; | | | 39> CONTA-AS called with arg:
; | | | (B (A A))
; | | | | 40> CONTA-AS called with arg:
; | | | | ((A A))
; | | | | | 41> CONTA-AS called with arg:
; | | | | | (A A)
; | | | | | | 42> CONTA-AS called with arg:
; | | | | | | (A)
; | | | | | | | 43> CONTA-AS called with arg:
; | | | | | | | NIL
; | | | | | | | 43< CONTA-AS returns value:
; | | | | | | | 0

8
; | | | | | | 42< CONTA-AS returns value:
; | | | | | | 1
; | | | | | 41< CONTA-AS returns value:
; | | | | | 2
; | | | | | 44> CONTA-AS called with arg:
; | | | | | NIL
; | | | | | 44< CONTA-AS returns value:
; | | | | | 0
; | | | | 40< CONTA-AS returns value:
; | | | | 2
; | | | 39< CONTA-AS returns value:
; | | | 2
; | | | 45> CONTA-AS called with arg:
; | | | ((C (C A)))
; | | | | 46> CONTA-AS called with arg:
; | | | | (C (C A))
; | | | | | 47> CONTA-AS called with arg:
; | | | | | ((C A))
; | | | | | | 48> CONTA-AS called with arg:
; | | | | | | (C A)
; | | | | | | | 49> CONTA-AS called with arg:
; | | | | | | | (A)
; | | | | | | | | 50> CONTA-AS called with arg:
; | | | | | | | | NIL
; | | | | | | | | 50< CONTA-AS returns value:
; | | | | | | | | 0
; | | | | | | | 49< CONTA-AS returns value:
; | | | | | | | 1
; | | | | | | 48< CONTA-AS returns value:
; | | | | | | 1
; | | | | | | 51> CONTA-AS called with arg:
; | | | | | | NIL
; | | | | | | 51< CONTA-AS returns value:
; | | | | | | 0
; | | | | | 47< CONTA-AS returns value:
; | | | | | 1
; | | | | 46< CONTA-AS returns value:
; | | | | 1
; | | | | 52> CONTA-AS called with arg:
; | | | | NIL
; | | | | 52< CONTA-AS returns value:
; | | | | 0
; | | | 45< CONTA-AS returns value:
; | | | 1
; | | 38< CONTA-AS returns value:
; | | 3
; | | 53> CONTA-AS called with arg:
; | | NIL
; | | 53< CONTA-AS returns value:
; | | 0
; | 37< CONTA-AS returns value:
; | 3
; 27< CONTA-AS returns value:
; 6

9
Observe como a profundidade da recursividade vai oscilando conforme a função vai
sendo chamada com as sub-listas como argumento. Esta função seria muito mais difícil
de programar iterativamente, além de ser necessário utilizar uma pilha para imitar a
recursividade. Em casos como este (e as procuras em diversas formas de árvores e listas
imbricadas são problemas comuns em IA) em que é mais fácil e/ou mais claro utilizar
uma definição recursiva a escolha é óbvia. Normalmente a escolha entre uma versão
recursiva ou iterativa fica à escolha de quem programa, a não ser que a velocidade do
programa seja um factor essencial. Aí as versões iterativas devem ser preferidas, já que
são normalmente mais rápidas.

Exercícios

1. Redefina a função potência de maneira a que a função consiga manipular expoentes


negativos (sem fazer uso da função do expt do Lisp).

2. Escreva uma função chamada my-remove que remova todas as ocorrências de um


elemento de uma lista nessa mesma lista. Deverá definir a função de forma iterativa. Aqui
estão alguns exemplos de como a função se deve comportar:

>(my-remove 'hello '(hello why dont you say hello))


(WHY DONT YOU SAY)
>(my-remove '(oops my) '(1 2 (oops my) 4 5))
(1 2 4 5)

3. Defina as seguintes funções de forma iterativa:

a. my-lenght recebe uma lista e conta o número de elementos dessa lista.

b. occurs recebe uma lista e um símbolo e conta o número de vezes que o símbolo
aparece na lista.

c. add-numbers recebe uma lista e devolve a soma dos elementos da lista que forem
números.

d. multiple-occurs recebe duas listas e devolve uma terceira com o número de


vezes que cada símbolo da primeira aparece na segunda.

e. add-three-lists recebe uma lista com três listas cada uma com três números e
devolve a soma de todos os números, por exemplo:

> (add-three-lists '((1 2 3) (4 5 6) (7 8 9)))


45

10

Você também pode gostar