Escolar Documentos
Profissional Documentos
Cultura Documentos
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
> (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:
> (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
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 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.
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.
Eis um exemplo de uma função que processa uma lista de maneira recursiva:
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 é:
(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
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:
(defun potencia (x y)
(let ((r x))
(dotimes (i (- y 1) r)
(setq r (* x r)))))
> (potencia 2 3)
8
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:
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:
Note que o let não permite inicializar uma variável em função de outra variável
inicializada anteriormente no mesmo let:
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!
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 é:
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:
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!
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):
É muito mais fácil uma implementação recursiva da função que faz a contagem:
Vamos observar o comportamento da função num trace com a lista anterior como
argumento:
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
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.
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:
10