• No se han encontrado resultados

Tema 4: Procedimientos y estructuras recursivas

N/A
N/A
Protected

Academic year: 2021

Share "Tema 4: Procedimientos y estructuras recursivas"

Copied!
49
0
0

Texto completo

(1)

1. Recursión

1.1 Pensando recursivamente 1.2 El coste de la recursión

1.3 Soluciones al coste de la recursión: procesos iterativos 1.4 Soluciones al coste de la recursión: memoization 2. Listas estructuradas

2.1. Definición y ejemplos 2.2. Pseudo árboles con niveles

2.3. Funciones recursivas sobre listas estructuradas 2.4. Ejercicios

3. Datos funcionales y barreras de abstracción 3.1. Datos funcionales 3.2. Barrera de abstracción 3.3. Números racionales 4. Árboles binarios 4.1. Barrera de abstracción 4.2. Funciones recursivas 5. Árboles genéricos 5.1. Barrera de abstracción 5.2. Funciones recursivas 5.3. Ejercicios

Ya hemos visto algunos ejemplos de funciones recursivas. Una función es recursiva cuando se llama a si misma. Una vez que uno se acostumbra a su uso, se comprueba que la recursión es

Tema 4: Procedimientos y estructuras recursivas

Contenidos

(2)

una forma mucho más natural que la iteración de expresar un gran número de funciones y procedimientos.

Recordemos un ejemplo típico, el de longitud de una lista

(define (longitud lista) (if (null? lista) 0

(+ 1 (longitud (cdr lista)))))

La formulación matemática de la recursión es sencilla de entender, pero su implementación en un lenguaje de programación no lo es tanto. El primer lenguaje de programación que permitió el uso de expresiones recursivas fue el Lisp. En el momento de su creación existía ya el Fortran, que no permitía que una función se llamase a si misma.

En la clase de hoy veremos cómo diseñar procedimientos recursivos y cuál es el coste

espacial y temporal de la recursión. Más adelante comprobaremos que no siempre una función recursiva tiene un comportamiento recursivo, sino que hay casos en los que genera un

proceso iterativo.

En las siguientes clases del tema veremos que la recursión no sólo se utiliza para definir funciones y procedimientos sino que existen estructuras de datos cuya definición es recursiva, como las listas o los árboles.

Para diseñar procedimientos recursivos no vale intentarlo resolver por prueba y error. Hay que diseñar la solución recursiva desde el principio. Debemos fijarnos en lo que devuelve la función y debemos preguntarnos cómo sería posible descomponer el problema de forma que podamos lanzar la recursión sobre una versión más sencilla del mismo. Supondremos que la llamada recursiva funciona correctamente y devuelve el resultado correcto. Y después debemos transformar este resultado correcto de la versión más pequeña en el resultado de la solución completa.

Es muy importante escribir y pensar en las funciones de forma declarativa, teniendo en cuenta lo que hacen y no cómo lo hacen.

Debes confiar en que la llamada recursiva va a hacer su trabajo y devolver el resultado correcto, sin preocuparte de cómo lo va a hacer. Después tendrás que utilizar lo que la llamada recursiva ha devuelto para componer la solución definitiva al problema.

Para diseñar un algoritmo recursivo es útil no ponerse a programar directamente, sino

(3)

reflexionar sobre la solución recursiva con algún ejemplo. El objetivo es obtener una formulación abstracta del caso general de la recursión antes de programarlo. Una vez que encontramos esta formulación, pasarlo a un lenguaje de programación es muy sencillo. Por último, deberemos reflexionar en el caso base. Debe ser el caso más sencillo que puede recibir como parámetro la recursión. Debe devolver un valor compatible con la definición de la función. Por ejemplo, si la función debe construir una lista, el caso base debe devolver también una lista. Si la función construye una pareja, el caso base también devolverá una pareja. No debemos olvidar que el caso base es también un ejemplo de invocación de la función.

Empecemos con un sencillo ejemplo, la definición de la longitud de una lista. ¿Cómo definir la longitud de una lista en términos recursivos? Tenemos que pensar: “Si puedo calcular la longitud de una cadena más pequeña, ¿cómo puedo calcular la longitud de la cadena total?”. Una posible definición del caso general de la recursión es esta: “Calculo la longitud de la lista sin el primer elemento con una llamada a la recursión, y le sumo 1 al número que devuelve esa llamada”.

Lo podríamos representar de la siguiente forma:

Longitud (lista) = 1 + Longitud (resto (lista))

Y, por último, necesitamos que la función devuelva un valor concreto cuando llegue al caso base de la recursión. El caso base puede ser la lista vacía, que tiene una longitud de 0:

Longitud (lista-vacía) = 0

La implementación en Scheme es:

(define (mi-length items) (if (null? items) 0

(+ 1 (length (cdr items)))))

Veamos un ejemplo algo más complicado ¿cómo definimos una lista palíndroma de forma recursiva?. Por ejemplo, las siguientes listas son palíndromas:

1.1.1. Longitud de una lista

(4)

'(1 2 3 3 2 1) '(1 2 1)

'(1) '()

Comenzamos con una definición no recursiva:

Una lista es palíndroma cuando es igual a su inversa.

Esta definición no es recursiva porque no llamamos a la recursión con un caso más sencillo. La definición recursiva del caso general es la siguiente:

Una lista es palíndroma cuando su primer elemento es igual que el último y la lista resultante de quitar el primer y el último elemento también es palíndroma

En el caso base debemos buscar el caso más pequeño no contemplado por la definición anterior. En este caso, una lista de un elemento y una lista vacía también las consideraremos palíndromas.

palindroma(lista) <=> (primer-elemento(lista) == ultimo-elemento(lista)) y palindroma(quitar-primero-ultimo(lista))

palindroma(lista) <=> un-elemento(lista) o vacía(lista)

Vamos a escribirlo en Scheme:

(define (palindromo? lista) (or (null? lista)

(null? (cdr lista))

(and (equal? (car lista) (ultimo lista))

(palindromo? (quitar-primero-ultimo lista)))))

La función auxiliar quitar-primero-ultimo la podemos definir así:

(define (quitar-ultimo lista) (if (null? (cdr lista)) '()

(cons (car lista)

(quitar-ultimo (cdr lista)))))

(define (quitar-primero-ultimo lista) (cdr (quitar-ultimo lista)))

(5)

Triángulo de Sierpinski

¿Ves alguna recursión en la figura?

¿Cuál podría ser el parámetro de la función que la dibujara? ¿Se te ocurre un algoritmo recursivo que la dibuje?

La figura es autosimilar (una característica de las figuras fractales). Una parte de la figura es idéntica a la figura total, pero reducida de escala. Esto nos da una pista de que es posible dibujar la figura con un algoritmo recursivo.

Para intentar encontrar una forma de enfocar el problema, vamos a pensarlo de la siguiente forma: supongamos que tenemos un triángulo de Sierpinski de anchura h y altura h/2 con su esquina inferior izquierda en la posición 0,0. ¿Cómo podríamos construir el siguiente triángulo de Sierpinski?.

Podríamos construir un triángulo de Sierpinski más grande dibujando 3 veces el mismo triángulo, pero en distintas posiciones:

1. Triángulo 1 en la posición (0,0) 2. Triángulo 2 en la posición (h/2,h/2) 3. Triángulo 3 en la posición (h,0)

El algoritmo recursivo se basa en la misma idea, pero hacia atrás. Debemos intentar dibujar un triángulo de altura h situado en la posición x, y basándonos en 3 llamadas recursivas a

triángulos más pequeños. En el caso base, cuando h sea menor que un umbral, dibujaremos un triángulo de lado h y altura h/2:

O sea, que para dibujar un triángulo de Sierpinski de base h y altura h/2 debemos:

Dibujar tres triángulos de Sierpinsky de la mitad del tamaño del original (h/2) situadas en 1.1.3. Triángulo de Sierpinski

(6)

las posiciones (x,y), (x+h/4, y+h/4) y (x+h/2,y)

En el caso base de la recursión, en el que h es menor que una constante, se dibuja un triángulo de base h y altura h/2.

Una versión del algoritmo en pseudocódigo:

Sierpinsky (x, y, h): if (h > MIN) { Sierpinsky (x, y, h/2) Sierpinsky (x+h/4, y+h/4, h/2) Sierpinsky (x+h/2, y, h/2) } else dibujaTriangulo (x, y, h)

Se pueden utilizar los gráficos de tortuga en Racket cargando la librería

graphics/turtles : (require graphics/turtles) estando en el lenguaje Muy Grande.

(require graphics/turtles) (turtles #t)

Los comandos más importantes:

(turtles #t) : abre una ventana y coloca la tortuga en el centro, mirando hacia el eje X (derecha)

(clear) : borra la ventana y coloca la tortuga en el centro (draw d) : avanza la tortuga dibujando d píxeles

(move d) : mueve la tortuga d píxeles hacia adelante (sin dibujar)

(turn g) : gira la tortuga g grados (positivos: en el sentido contrario a las agujas del reloj)

Prueba a realizar algunas figuras con los comandos de tortuga, antes de escribir el algoritmo en Scheme del triángulo de Sierpinski.

Por ejemplo, podemos definir una función que dibuja un triángulo rectángulo con catetos de longitud x :

(7)

(define (hipot x) (* x (sqrt 2))) (define (triangulo x) (draw x) (turn 90) (draw x) (turn 135) (draw (hipot x)) (turn 135)) (triangulo 100)

La función (hipot x) devuelve la longitud de la hipotenusa de un triángulo rectángulo con dos lados de longitud x . O sea, la expresión:

Como puedes comprobar, el código es imperativo. Se basa en realizar una serie de pasos de ejecución que modifican el estado (posición y orientación) de la tortuga.

¿Es posible usar gráficos de tortuga de forma recursiva? Una forma de hacerlo sería usar programación funcional para generar una lista de comandos y usar código imperativo sólo en la función que realiza el dibujo de la lista de comandos.

Por ejemplo, la lista de comandos que dibuja un triángulo rectángulo con lados 100 sería:

'((draw l00) (turn 90) (draw l00) (turn 135) (draw (hipot 100)) (turn 135))

Podemos ejecutar esta lista de comandos con un enfoque imperativo. Para ello, definimos la siguiente función run que recibe una lista de comandos como la anterior y los ejecuta, mediante una llamada a eval para que evalúe el primer comando de la lista y una llamada recursiva que ejecuta el resto:

(8)

(define (run comandos)

(if (not (null? comandos)) (begin

(eval (car comandos)) (run (cdr comandos)))))

Por ejemplo, el siguiente código dibuja un triángulo rectángulo con los dos catetos de longitud 100: (draw 100) (turn 90) (draw 100) (turn 135) (draw (hipot 100))

Podemos definir la función triangulo-list x que devuelve una lista de comandos:

(define (triangulo-list x) (list (list 'draw x) '(turn 90) (list 'draw x) '(turn 135)

(cons 'draw (list (list 'hipot x))) '(turn 135)))

Y llamar a run con lo que devuelve la llamada anterior:

(run (triangulo-list 100))

La siguiente es una versión imperativa del algoritmo que dibuja el triángulo de Sierpinski. No es funcional porque se realizan pasos de ejecución, usando la forma especial begin o múltiples instrucciones en una misma función (por ejemplo la función triangle ). 1.1.5. Sierpinski en Racket

(9)

(require graphics/turtles) (turtles #t) (define (hipot x) (* x (sqrt 2))) (Define (triangle w) (draw w) (turn 135) (draw (hipot (/ w 2))) (turn 90) (draw (hipot (/ w 2))) (turn 135)) (define (sierpinski w) (if (> w 20) (begin (sierpinski (/ w 2))

(move (/ w 4)) (turn 90) (move (/ w 4)) (turn -90) (sierpinski (/ w 2))

(turn -90) (move (/ w 4)) (turn 90) (move (/ w 4)) (sierpinski (/ w 2))

(turn 180) (move (/ w 2)) (turn -180)) ;; volvemos a la posició n original

(triangle w)))

Usando la misma idea que vimos anteriormente, podemos construir la función equivalente a sierpinski pero que en lugar de dibujar, devuelve una lista de comandos. La llamamos sierpinski-list :

(10)

(define (triangle-list w) (list

(list 'draw w) '(turn 135)

(list 'draw (hipot (/ w 2))) '(turn 90)

(list 'draw (hipot (/ w 2))) '(turn 135)))

(define (sierpinski-list w) (if (> w 20)

(append

(sierpinski-list (/ w 2))

(list (list 'move (/ w 4)) '(turn 90) (list 'move (/ w 4)) '(tu rn -90))

(sierpinski-list (/ w 2))

(list '(turn -90) (list 'move (/ w 4)) '(turn 90) (list 'move ( / w 4)))

(sierpinski-list (/ w 2))

(list '(turn 180) (list 'move (/ w 2)) '(turn -180))) ;; volvem os a la posición original

(triangle-list w)))

Por ejemplo, (sierpinski-list 40) devuelve la siguiente lista de comandos:

((draw 20) (turn 135) (draw 14.142135623730951) (turn 90) (draw 14.142135 623730951)

(turn 135) (move 10) (turn 90) (move 10) (turn -90) (draw 20) (turn 135)

(draw 14.142135623730951) (turn 90) (draw 14.142135623730951) (turn 135) (turn -90)

(move 10) (turn 90) (move 10) (draw 20) (turn 135) (draw 14.1421356237309 51)

(turn 90) (draw 14.142135623730951) (turn 135) (turn 180) (move 20) (turn -180))

Que se dibujan con una llamada a la función run produciendo la siguiente figura:

Sierpinski 40 1.1.7. Recursión mutua

(11)

En la recursión mutua definimos una función en base a una segunda, que a su vez se define en base a la primera.

También debe haber un caso base que termine la recursión Por ejemplo: x es par si x–1 es impar x es impar si x–1 es par 0 es par Programas en Scheme: (define (par? x) (if (= 0 x) #t (impar? (- x 1)))) (define (impar? x) (if (= 0 x) #f (par? (- x 1))))

La curva de Hilbert es una curva fractal que tiene la propiedad de rellenar completamente el espacio

Su dibujo tiene una formulación recursiva

Curva de Hilbert

La curva H3 se puede construir a partir de la curva H2. El algoritmo recursivo se formula dibujando la curva i-ésima a partir de la curva i–1.

Como en la curva de Sierpinsky, utilizamos la librería graphics/turtles , que permite usar la tortuga de Logo con los comandos de Logo draw y turn

(12)

La función (h-der i w) dibuja una curva de Hilbert de orden i con una longitud de trazo w a la derecha de la tortuga

La función (h-izq i w) dibuja una curva de Hilbert de orden i con una longitud de trazo w a la izquierda de la tortuga

Para dibujar una curva de Hilbert de orden i a la derecha de la tortuga:

1. Gira la tortuga -90

2. Dibuja una curva de orden i-1 a la izquierda 3. Avanza w dibujando

4. Gira 90

5. Dibuja una curva de orden i-1 a la derecha 6. Avanza w dibujando

7. Dibuja una curva de orden i-1 a la derecha 8. Gira 90

9. Avanza w dibujando

10. Dibuja una curva de orden i-1 a la izquierda 11. Gira -90

El algoritmo para dibujar a la izquierda es simétrico El algoritmo en Scheme:

(13)

(require graphics/turtles) (turtles #t) (define (h-der i w) (if (> i 0) (begin (turn -90) (h-izq (- i 1) w) (draw w) (turn 90) (h-der (- i 1) w) (draw w) (h-der (- i 1) w) (turn 90) (draw w) (h-izq (- i 1) w) (turn -90)))) (define (h-izq i w) (if (> i 0) (begin (turn 90) (h-der (- i 1) w) (draw w) (turn -90) (h-izq (- i 1) w) (draw w) (h-izq (- i 1) w) (turn -90) (draw w) (h-der (- i 1) w) (turn 90))))

Podemos probarlo con distintos parámetros de grado de curva y longitud de trazo:

(clear) (h-izq 3 20) (h-izq 6 5)

(14)

Hilbert en Scheme

Vamos a estudiar el comportamiento del proceso generado por una llamada a un procedimiento recursivo. Supongamos la función mi-length :

(define (mi-length items) (if (null? items)

0

(+ 1 (mi-length (cdr items)))))

Examinamos cómo se evalúan las llamadas recursivas:

(mi-length '(a b c d)) (+ 1 (mi-length '(b c d))) (+ 1 (+ 1 (mi-length '(c d)))) (+ 1 (+ 1 (+ 1 (mi-length '(d))))) (+ 1 (+ 1 (+ 1 (+ 1 (mi-length '()))))) (+ 1 (+ 1 (+ 1 (+ 1 0)))) (+ 1 (+ 1 (+ 1 1))) (+ 1 (+ 1 2)) (+ 1 3) 4

1.2. El coste de la recursión

1.2.1. La pila de la recursión

(15)

Cada llamada a la recursión deja una función en espera de ser evaluada cuando la recursión devuelva un valor (en el caso anterior el +). Esta función, junto con sus argumentos, se almacenan en la pila de la recursión.

Cuando la recursión devuelve un valor, los valores se recuperan de la pila, se realiza la

llamada y se devuelve el valor a la anterior llamada en espera. Si la recursión está mal hecha y nunca termina se genera un stack overflow.

Es posible hacer que Racket haga una traza de la secuencia de llamadas a la recursión utilizando la librería trace.ss . Una vez cargada la librería puedes activar y desactivar las trazas de funciones específicas con (trace <función>) y (untrace <función>) . Debes tener activo el lenguaje Muy Grande.

Un pequeño problema de las trazas es que sólo se pueden tracear funciones definidas por el usuario, no se pueden tracear funciones primitivas de Scheme como + o car . Si

queremos comprobar un ejemplo de traza en donde haya una llamada a una función primitiva, podemos definir una función propia que llame a la primitiva y tracear nuestra función.

(16)

(require (lib "trace.sss)) (define (suma x y) (+ x y)) (define (mi-length lista) (if (null? lista) 0

(suma 1 (mi-length (cdr lista)))))

(trace suma) (trace mi-length) (mi-length '(a b c d e f)) >(mi-length '(a b c d e f)) > (mi-length '(b c d e f)) > >(mi-length '(c d e f)) > > (mi-length '(d e f)) > > >(mi-length '(e f)) > > > (mi-length '(f)) > > > >(mi-length '()) < < < <0 > > > (suma 1 0) < < < 1 > > >(suma 1 1) < < <2 > > (suma 1 2) < < 3 > >(suma 1 3) < <4 > (suma 1 4) < 5 >(suma 1 5) <6 6

El coste espacial de un programa es una función que relaciona la memoria consumida por una llamada para resolver un problema con alguna variable que determina el tamaño del problema a resolver.

En el caso de la función mi-length el tamaño del problema viene dado por la longitud de la lista. El coste espacial de mi-lenght es O(n), siendo n la longitud de la lista.

1.2.2. Coste espacial de la recursión

(17)

Veamos con un ejemplo que el coste de las llamadas recursivas puede dispararse. Supongamos la famosa secuencia de Fibonacci: 0,1,1,2,3,5,8,13,…

Formulación matemática de la secuencia de Fibonacci:

>Fibonacci(n) = Fibonacci(n-1) + Fibonacci(n-2) >Fibonacci(0) = 0

>Fibonacci(1) = 1

Formulación recursiva en Scheme:

(define (fib n) (cond ((= n 0) 0) ((= n 1) 1)

(else (+ (fib (- n 1))

(fib (- n 2))))))

Evaluación de una llamada a Fibonacci:

Llamada recursiva a Fibonacci

Cada llamada a la recursión produce otras dos llamadas, por lo que el número de llamadas finales es 2n siendo n el número que se pasa a la función.

El coste espacial y temporal es exponencial, O(2n). ¿Qué pasa si intentamos evaluar (fibonaci 100)?

(18)

Diferenciamos entre procedimientos y procesos: un procedimiento es un algoritmo y un proceso es la ejecución de ese algoritmo.

Es posible definir procedimientos recursivos que generen procesos iterativos (como los bucles en programación imperativa) en los que no se dejen llamadas recursivas en espera ni se incremente la pila de la recursión. Para ello construimos la recursión de forma que en cada llamada se haga un cálculo parcial y en el caso base se pueda devolver directamente el resultado obtenido.

Este estilo de recursión se denomina recursión por la cola (tail recursion, en inglés).

Se puede realizar una implementación eficiente de la ejecución del proceso, eliminando la pila de la recursión.

Es posible modificar la formulación de la recursión para se eviten las llamadas en espera: Definimos la función (fact-iter-aux product n) que es la que define el proceso iterativo

Tiene un parámetro adicional ( product ) que es el parámetro en el que se irán guardando los cálculos intermedios

Al final de la recursión el factorial debe estar calculado en product y se devuelve

(define (factorial-iter n) (fact-iter-aux n n))

(define (fact-iter-aux product n) (if (= n 1)

product

(fact-iter-aux (* product (- n 1)) (- n 1))))

Secuencia de llamadas:

1.3. Soluciones al coste de la recursión: procesos iterativos

(19)

(factorial-iter 4) (factorial-iter-aux 4 4) (factorial-iter-aux 12 3) (factorial-iter-aux 24 2) (factorial-iter-aux 24 1) 24

¿Cómo sería la versión iterativa de mi-length? Solución:

(define (mi-length-iter lista) (mi-length-iter-aux lista 0))

(define (mi-length-iter-aux lista result) (if (null? lista)

result

(mi-length-iter-aux (cdr lista) (+ result 1))))

La recursión resultante es menos elegante

Se necesita una parámetro adicional en el que se van acumulando los resultados parciales

La última llamada a la recursión devuelve el valor acumulado

El proceso resultante de la recursión es iterativo en el sentido de que no deja llamadas en espera ni incurre en coste espacial

Cualquier programa recursivo se puede transformar en otro que genera un proceso iterativo. En general, las versiones iterativas son menos intuitivas y más difíciles de entender y depurar. Ejemplo: Fibonacci iterativo

1.3.2. Versión iterativa de mi-length

1.3.3. Procesos iterativos

(20)

(define (fib-iter n) (fib-iter-aux 1 0 n))

(define (fib-iter-aux a b count) (if (= count 0) b (fib-iter-aux (+ a b) a (- count 1)))) 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 1 1 6 15 20 15 6 1 1 7 21 35 35 21 7 1 ... Formulación matemática:

Pascal (0,0) = Pascal (1,0) = Pascal (1,1) = 1

Pascal (fila, columna) = Pascal (fila–1,columna–1) + Pascal (fila–1, columna)

La versión recursiva pura:

(define (pascal row col) (cond ((= col 0) 1) ((= col row) 1)

(else (+ (pascal (- row 1) (- col 1)) (pascal (- row 1) col) ))))

La versión iterativa:

(21)

(define (pascal-iter fila col)

(list-ref (pascal-iter-aux '(1 1) fila) col))

(define (pascal-iter-aux fila n) (if (= n (length fila)) fila

(pascal-iter-aux (pascal-sig-fila fila) n)))

(define (pascal-sig-fila fila) (append '(1)

(pascal-sig-fila-central fila) '(1)))

(define (pascal-sig-fila-central fila) (if (= 1 (length fila))

'()

(append (list (+ (car fila) (car (cdr fila))))

(pascal-sig-fila-central (cdr fila)))))

Una alternativa que mantiene la elegancia de los procesos recursivos y la eficiencia de los iterativos es la memoization. Si miramos la traza de (fibonacci 4) podemos ver que el coste está producido por la repetición de llamadas; por ejemplo (fibonacci 3) se evalúa 2 veces. En programación funcional la llamada a (fibonacci 3) siempre va a devolver el mismo valor. Podemos guardar el valor devuelto por la primera llamada en alguna estructura (una lista de asociación, por ejemplo) y no volver a realizar la llamada a la recursión las siguientes veces.

Usamos los métodos procedurales put y get que implementan un diccionario clave-valor (para probarlos hay que utilizar el lenguaje R5RS):

1.4. Soluciones al coste de la recursión: memoization

(22)

(define lista (list '*table*))

(define (get key lista)

(let ((record (assq key (cdr lista)))) (if (not record)

'()

(cdr record))))

(define (put key value lista)

(let ((record (assq key (cdr lista)))) (if (not record)

(set-cdr! lista

(cons (cons key value) (cdr lista)))

(set-cdr! record value))) 'ok)

La función (put key value lista) asocia un valor a una clave y la guarda en la lista (con mutación).

La función (get key lista) devuelve el valor de la lista asociado a una clave. Ejemplos:

(define mi-lista (list '*table*)) (put 1 10 mi-lista)

(get 1 mi-lista) -> 10 (get 2 mi-lista) -> '()

La función fib-memo realiza el cálculo de la serie de Fibonacci utilizando el proceso recursivo visto anteriormente y la técnica de memoización, en la que se consulta el valor de Fibonacci de la lista antes de realizar la llamada recursiva:

(define (fib-memo n lista) (cond ((= n 0) 0)

((= n 1) 1)

((not (null? (get n lista))) (get n lista))

(else (let ((result

(+ (fib-memo (- n 1) lista) (fib-memo (- n 2) lista)))) (begin

(put n result lista) result)))))

(23)

Podemos comprobar la diferencia de tiempos de ejecución entre esta versión y la anterior. El coste de la función memoizada es O(n). Frente al coste O(2n) de la versión inicial que la hacía imposible de utilizar.

(define lista (list '*table*))

(fib-memo 200 lista) -> 280571172992510140037611932413038677189525

Hemos visto temas anteriores que las listas en Scheme se implementan como un estructura de datos recursiva, formadas a partir de parejas y de una lista vacía. Una vez conocida su

implementación, vamos a volver a estudiar las listas desde un nivel de abstracción alto,

usando las funciones car y cdr para obtener el primer elemento y el resto de la lista y la función cons para añadir un nuevo elemento a su cabeza.

En la mayoría de funciones y ejemplos que hemos visto hasta ahora las listas están formadas por datos y el recorrido por la lista es un recorrido lineal, una iteración por sus elementos. En este apartado vamos a ampliar este concepto y estudiar cómo trabajar con listas que contienen otras listas.

Las listas en Scheme pueden tener cualquier tipo de elementos, incluido otras listas. Llamaremos lista estructurada a una lista que contiene otras sublistas. Lo contrario de lista estructurada es una lista plana, una lista formada por elementos que no son listas.

Llamaremos hojas a los elementos de una lista que no son sublistas. Por ejemplo, la lista estructurada:

'(a b (c d e) (f (g h)))

es una lista estructurada con 4 elementos: El elemento 'a , una hoja

El elemento 'b , otra hoja La lista plana '(c d e)

La lista estructurada '(f (g h))

Una lista formada por parejas la consideraremos una lista plana, ya que no contiene ninguna sublista. Por ejemplo, la lista

2. Listas estructuradas

(24)

’((a . 3) (b . 5) (c . 12))

es una lista plana de tres elementos (hojas) que son parejas.

Vamos a escribir las definiciones anteriores en código de Scheme. Un dato es una hoja si no es una lista:

(define (hoja? dato) (not (list? dato)))

Una definición recursiva de lista plana:

Una lista es plana si y solo si el primer elemento es una hoja y el resto es plana.

Y el caso base:

Una lista vacía es plana.

En Scheme:

(define (plana? lista) (or (null? lista)

(and (hoja? (car lista)) (plana? (cdr lista)))))

Una lista es estructurada cuando alguno de sus elementos es otra lista:

(define (estructurada? lista) (if (null? lista)

#f

(or (list? (car lista))

(estructurada? (cdr lista)))))

Ejemplos:

(25)

(plana? '(a b c d e f)) #t (plana? '((a . 1) (b . 2) (c . 3))) #t (plana? '(a (b c) d)) #f (plana? '(a () b)) #f (estructurada? '(1 2 3 4)) #f (estructurada? '((a . 1) (b . 2) (c . 3))) #f (estructurada? '(a () b)) #t (estructurada? '(a (b c) d)) #t

Realmente bastaría con haber hecho una de las dos definiciones y escribir la otra como la negación de la primera:

(define (estructurada? lista) (not (plana? lista)))

O bien:

(define (plana? lista)

(not (estructurada? lista)))

Las listas estructuradas son muy útiles para representar información jerárquica en donde queremos representar elementos que contienen otros elementos.

Por ejemplo, las expresiones de Scheme son listas estructuradas:

'(= 4 (+ 2 2))

'(if (= x y) (* x y) (+ (/ x y) 45))

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

El análisis sintáctico de una oración puede generar una lista estructurada de símbolos, en donde se agrupan los distintos elementos de la oración:

'((Juan) (compró) (la entrada (de los Miserables)) (el viernes por la tar de))

Una página HTML, con sus distintos elementos, unos dentro de otros, también se puede representar con una lista estructurada:

(26)

'((<h1> Mi lista de la compra </h1>) (<ul> (<li> naranjas </li>)

(<li> tomates </li>)

(<li> huevos </li>) </ul>))

Las listas estructuradas definen una estructura de niveles, donde la lista inicial representa el primer nivel, y cada sublista representa un nivel inferior. Los datos de las listas representan las hojas.

Por ejemplo, la representación en forma de niveles de la lista '((1 2 3) 4 5) es la siguiente:

La estructura no es un árbol propiamente dicho, porque todos los datos están en las hojas. Otro ejemplo. ¿Cuál sería la representación en niveles de la siguiente lista estructurada?:

'(let ((x 12) (y 5)) (+ x y)))

La altura de una lista estructurada viene dada por su número de niveles: una lista plana tiene una altura de 1, la lista anterior tiene una altura de 2.

Veamos una definición más precisa. altura(lista vacía) = 0

altura(hoja) = 0

altura(lista) = max (1 + altura (primer-elemento (lista)), altura (resto (lista)) En Scheme:

2.2. Pseudo árboles con niveles

(27)

(define (altura lista) (cond

((null? lista) 0) ((hoja? lista) 0)

(else (max (+ 1 (altura (car lista))) (altura (cdr lista))))))

Por ejemplo:

(altura '(1 (2 3) 4)) 2 (altura '(1 (2 (3)) 3)) 3

Hay que hacer notar que en la función anterior que el parámetro lista va a aceptar tanto hojas como listas.

Es posible definir la función utilizando las función de orden superior map para aplicar la propia función que estamos definiendo a los elementos de la lista:

(define (altura lista) (cond

((null? lista) 0) ((hoja? lista) 0)

(else (+ 1 (apply max (map altura lista))))))

Vamos a diseñar distintas funciones recursivas que trabajan con la estructura jerárquica de las listas estructuradas.

(num-hojas lista) : cuenta las hojas de una lista estructurada (pertenece? lista) : busca una hoja en una lista estructurada

(cuadrado-lista lista) : eleva todas las hojas al cuadrado (suponemos que la lista estructurada contiene números)

(map-lista f lista) : similar a map, aplica una función a todas las hojas de la lista estructurada y devuelve el resultado (otra lista estructurada)

Cuenta el número de hojas de una lista estructurada. 2.2.2. Implementación de altura con map

2.3. Funciones recursivas sobre listas estructuradas

(28)

Definición matemática: num-hojas(lista vacía) = 0 num-hojas(hoja) = 1

num-hojas(lista) = num-hojas (primer-elemento (lista)) + num-hojas (resto (lista)) Implementación en Scheme:

(define (num-hojas lista) (cond

((null? lista) 0) ((hoja? lista) 1)

(else (+ (num-hojas (car lista)) (num-hojas (cdr lista))))))

Por ejemplo:

(num-hojas '(1 2 (3 4 (5) 6) (7))) 7

Con map y apply :

(define (num-hojas lista) (cond

((null? lista) 0) ((hoja? lista) 1)

(else (apply + (map num-hojas lista)))))

Comprueba si el dato x aparece en la lista estructurada.

(define (pertenece? x lista) (cond

((null? lista) #f)

((hoja? lista) (equal? x lista)) (else (or (pertenece? x (car lista)) (pertenece? x (cdr lista))))))

Ejemplos:

(pertenece? 'a '(b c (d (a)))) #t (pertenece? 'a '(b c (d e (f)) g)) #f

(29)

Devuelve una lista estructurada con la misma estructura y sus números elevados al cuadrado.

(define (cuadrado-lista lista) (cond ((null? lista) '())

((hoja? lista) (* lista lista))

(else (cons (cuadrado-lista (car lista)) (cuadrado-lista (cdr lista))))))

Por ejemplo:

(cuadrado-lista '(2 3 (4 (5)))) (4 9 (16 (25))

Devuelve una lista estructurada igual que la original con el resultado de aplicar a cada uno de sus hojas la función f

(define (map-lista f lista) (cond ((null? lista) '())

((hoja? lista) (f lista))

(else (cons (map-lista f (car lista)) (map-lista f (cdr lista))))))

Por ejemplo:

(map-lista (lambda (x) (* x x)) '(2 3 (4 (5)))) (4 9 (16 (25))

Planteamos a continuación algunos ejercicios para que practiquéis las funciones recursivas sobre las listas estructuradas.

Escribe la función (tres-elementos? lista) que tome una lista estructurada como argumento. Devolverá #t si cada subsista que aparece y cada uno de sus elementos tienen 3 elementos. Suponemos que nunca se llamará a la función con una lista vacía.

Ejemplos:

2.3.3. cuadrado-lista

2.3.4. map-lista

2.4. Ejercicios

(30)

(tres-elementos? '(1 2 3)) #t

(tres-elementos? '((1 2 3) 2 3)) #t (tres-elementos? '((1 2 3) (4 5 6))) #f (tres-elementos? '(1 2 (3 3))) #f

Escribe la función (diff-listas l1 l2) que tome como argumentos dos listas

estructuradas con la misma estructura, pero con diferentes elementos, y devuelva una lista de parejas que contenga los elementos que son diferentes.

Ejemplos:

(dif-listas '(a (b ((c)) d e) f) '(1 (b ((2)) 3 4) f)) ((a . 1) (c . 2) (d . 3) (e . 4))

(diff-listas '() '()) ()

(diff-listas '((a b) c) '((a b) c)) ()

Define un procedimiento llamado (transformar plantilla lista) que reciba dos listas como argumento: la lista plantilla será una lista estructurada y estará compuesta por números enteros positivos con una estructura jerárquica, como (2 (3 1) 0 (4)) . La segunda lista será una lista plana con tantos elementos como indica el mayor número de plantilla más uno (en el caso anterior serían 5 elementos). El procedimiento deberá devolver una lista estructurada donde los elementos de la segunda lista se sitúen (en situación y en estructura) según indique la plantilla.

Ejemplos:

(transformar '((0 1) 4 (2 3)) '(hola que tal estas hoy)) ((hola que) hoy (tal estas))

(transformar '(1 4 3 2 5 (0)) '(vamos todos a aprobar este examen)) (todos este aprobar a examen (vamos))

Escribe la función (nivel-hoja dato lista) que recorra la lista buscando el dato y devuelva el nivel en que se encuentra. Suponemos que el dato está en la lista y no está repetido.

diff-listas

transformar

(31)

Ejemplos:

(nivel-hoja 2 '(1 2 (3))) 1 (nivel-hoja 2 '(1 (2) 3)) 2 (nivel-hoja 2 '(1 3 4 ((2))) 4

Define la función (hojas-nivel lista) que reciba una lista estructurada y devuelva una lista de parejas con las hojas y la profundidad en que se encuentra cada hoja.

Ejemplos:

(hojas-nivel '(2 3 4 (5) ((6)))) ((2.1) (3.1) (4.1) (5.2) (6.3)) (hojas-nivel '(2 (5 (8 9) 10 1)))

((2.1) (5.2) (8.3) (9.3) (10.2) (1.2))

Define la función (acumula lista nivel) que reciba una lista estructurada compuesta por números y un número de nivel. Devolverá la suma de todos los nodos hoja que tengan una profundidad mayor o igual que el nivel indicado.

Ejemplos:

(acumula '(2 3 4 (5) ((6))) 2) 11 (acumula '(2 3 4 (5) ((6))) 3) 6 (acumula '(2 (5 (8 9) 10 1)) 3) 17

Hablamos de datos funcionales para referirnos a valores inmutables de primera clase (que podemos asignar a variables). Ejemplos en Scheme: int, boolean, char, string, pareja, … Un dato funcional se inicializa con un estado, pero no se puede modificar una vez creado. Sí que podemos construir nuevos datos a partir de datos ya existentes. Pero la característica fundamental es su inmutabilidad.

En las operaciones en las que intervienen datos funcionales siempre se devuelve un dato hojas-nivel

acumula

3. Datos funcionales y barreras de abstracción

(32)

nuevo recién creado; los objetos “matemáticos” son así. Por ejemplo, si sumamos dos números racionales el resultado es un número nuevo; los números que intervienen en la operación no se modifican.

Los datos funcionales son muy útiles y se recomienda su utilización en la mayoría de

lenguajes. En los lenguajes orientados a objetos como Java o Scala este tipo de datos reciben el nombre de objetos valor - ver p. 73 Effective Java Programming, Joshua Bloch).

Los lenguajes de programación funcional no proporcionan mecanismos como los de clases o interfaces para definir nuevos datos. Debemos crearlos de forma ad-hoc definiendo un

conjunto de funciones que construyen y trabajan con los datos y teniendo la disciplina de sólo utilizar esas para operar con los datos. Es lo que llamamos barrera de abstracción del dato. Al definir nuevas operaciones que trabajen con un dato no debemos “saltar” esa barrera de abstracción.

Cita de Abelson y Sussman en la que explica el concepto de abstracción de datos:

When defining procedural abstraction we make an abstraction that separates the way the procedure would be used from the details of how the procedure is implemented in terms of more primitive procedures. The analogous notion for compound data is called data abstraction. Data abstraction is a methodology that enables us to isolate how a

compound data object is used from the details of how it is constructed from more primitive data objects.

The basic idea of data abstraction is to structure the programs that are to use compound data objects so that they operate on “abstract data”. That is, our programs should use data in such a way as to make no assumptions about the data that are not strictly necessary for performing the task at hand. At the same time, a “concrete” data

representation is defined independent of the programs that use the data. The interface

between these two parts of our system will be a set of procedures,

called selectors and constructors, that implement the abstract data in terms of the concrete representation.

Definimos como barrera de abstracción (o interfaz) de un dato funcional al conjunto de funciones que nos permiten trabajar con él y que separan su uso de su implementación. 3.1.1. Abstracción de datos

(33)

;;---;; ;; ;; ;; USO DE LA ABSTRACCION ;; ;; ;; ;; ---INTERFAZ: funciones ---;; ;; ;; ;; IMPLEMENTACION ;; ;; ;; ;; ---;;

La funciones que están por encima de la barrera de abstracción deben usar las funciones propuestas por la barrera.

De esta forma se esconde la implementación y se independiza las funciones que usan la abstracción de su implementación.

Veamos un ejemplo de dato funcional. Supongamos que queremos construir el tipo de dato número racional, un número formado por un numerador y un denominador, con las

operaciones típicas de suma, resta, multiplicación y división.

Vamos a definir una nomenclatura estándar para nombrar a las funciones de la barrera de abstracción. Utilizaremos el sufijo ‘rat’ (del inglés ‘rational’) para todas las funciones que trabajan con el nuevo tipo de datos.

La barrera de abstracción está formada por constructores, selectores y operadores:

Constructores: Funciones que devuelven un nuevo dato creado a partir de sus elementos Selectores: Funciones que devuelven algún elemento a partir del dato

Operadores: Funciones que definen operaciones sobre los datos. Algunas devuelven nuevos datos resultantes de la operación o devuelven otros tipos

En programación funcional no existen mutadores. Una vez construido un dato no se puede modificar (eso sí, se puede construir un nuevo dato a partir de los ya existentes).

En el caso de los números racionales, vamos a definir las siguientes funciones: Constructor (o constructores): make-rac

Selectores y operadores: num-rac , denom-rac , suma-rac , sub-rac ,

3.3. Ejemplo de barrera de abstracción para construir datos: números

racionales

(34)

div-rac Constructores

(make-rac num denom) : Toma dos números enteros num y denom y devuelve un nuevo número racional cuyo numerador es num y denominador es denom . Selectores

(num-rac rac) : Toma un número racional y devuelve su numerador. (denom-rac rac) : Toma un número racional y devuelve su denominador. Operadores

(suma-rac r1 r2) : Toma dos números racionales r1 y r2 y devuelve su suma. (resta-rac r1 r2) : Toma dos números racionales r1 y r2 y devuelve su resta.

(mult-rac r1 r2) : Toma dos números racionales r1 y r2 y devuelve su multiplicación.

(div-rac r1 r2) : Toma dos números racionales r1 y r2 y devuelve su división.

(35)

(define (make-rac numer denom) (cons numer denom))

(define (num-rac rac) (car rac))

(define (denom-rac rac) (cdr rac))

(define (add-rac x y)

(make-rac (+ (* (num-rac x) (denom-rac y)) (* (num-rac y) (denom-rac x))) (* (denom-rac x) (denom-rac y))))

(define (sub-rac x y)

(make-rac (- (* (num-rac x) (denom-rac y)) (* (num-rac y) (denom-rac x))) (* (denom-rac x) (denom-rac y))))

(define (mul-rac x y)

(make-rac (* (num-rac x) (num-rac y))

(* (denom-rac x) (denom-rac y))))

(define (div-rac x y)

(make-rac (* (num-rac x) (denom-rac y)) (* (denom-rac x) (num-rac y))))

(define (equal-rac? x y)

(= (* (num-rac x) (denom-rac y)) (* (denom-rac x) (num-rac y))))

(define (rac->string rac) (string-append

(number->string (num-rac rac)) "/"

(number->string (denom-rac rac))))

Una vez definida la barrera de abstracción, hemos creado una nueva abstracción que amplía el vocabulario y las capacidades de nuestros programas. Ahora podemos usar números racionales de la misma forma que usamos números enteros o reales:

(36)

(define r1 (make-rac 1 3)) ; r1 = 1/3 (num-rac r1) 1 (denom-rac r1) 3 (define r2 (make-rac 2 3)) (define r3 (add-rac r1 r2)) (rac->string r2) "2/3" (rac->string r3) "9/9"

Una de las ventajas de definir una barrera de abstracción es que es posible cambiar la implementación de las funciones sin afectar a las funciones que las usan. Si esas funciones han respetado la definición de la barrera y no han accedido directamente a la implementación van a seguir funcionando sin problemas.

Veamos un ejemplo. Vamos a modificar la implementación del número racional para añadir a la estructura el símbolo racional . Este símbolo nos va a permitir añadir un predicado que compruebe si un dato es del tipo racional.

Tenemos que modificar el constructor make-racional y los selectores num-rac y denom-rac :

(define (make-rac numer denom) (cons 'racional

(cons numer denom)))

(define (num-rac rac) (cadr rac))

(define (denom-rac rac) (cddr rac))

El resto de funciones no hay que tocarlas, siguen funcionando correctamente, porque hacen uso de estas funciones de la barrera de abstracción.

Ahora ya podemos definir el predicado racional? que comprueba si un número es racional:

(define (racional? x) (and (pair? x)

(equal? (car x) 'racional)))

Ejemplos:

(37)

(define r1 (make-rac 1 3)) ; r1 = 1/3 (racional? r1) #t (racional? 12) #f (num-rac r1) 1 (denom-rac r1) 3 (define r2 (make-rac 2 3)) (define r3 (suma-rac r1 r2)) (rac->string r2) "2/3" (rac->string r3) "9/9" Definición recursiva:

Un árbol binario es una estructura que contiene un dato, un hijo izquierdo y un hijo derecho (ambos árboles binarios)

Un árbol binario también puede ser un árbol vacío

Un árbol binario

En la asignatura no nos preocupa cuáles son los datos del árbol, ni su ordenación; sólo la estructura

4. Árboles binarios

4.1. Barrera de abstracción de árbol binario

(38)

Barrera de abstracción b-tree Funciones de la barrera:

(make-bt dato izq-bt der-bt) : crea un árbol binario con un dato, un árbol binario izquierdo y un árbol binario derecho

(make-hoja-bt dato) : crea una hoja, con su hijo izquierdo e hijo derecho vacíos 'vacio-bt : símbolo que define el árbol binario vacío

(dato-bt bt) : devuelve el dato de un árbol binario (izq-bt bt) : devuelve el árbol binario izquierdo (der-bt bt) : devuelve el árbol binario derecho

(hoja-bt? bt) : comprueba si el árbol es una hoja (no tiene hijos) (vacio-bt? bt) : comprueba si el árbol es vacío

Implementación árbol binario Implementación de la estructura de datos:

Un árbol binario es una lista con 3 elementos: el dato de la raíz, el árbol que forma el hijo 4.1.2. Implementación

(39)

izquierdo y el árbol que forma el hijo derecho Implementación de las funciones:

(define (make-bt dato izq der) (list dato izq der))

(define (make-hoja-bt dato)

(make-bt dato 'vacio-bt 'vacio-bt))

(define (vacio-bt? btree) (equal? btree 'vacio-bt)) (define (dato-bt btree) (car btree))

(define (izq-bt btree) (car (cdr btree)))

(define (der-bt btree) (car (cdr (cdr btree))))

(define (hoja-bt? btree)

(and (vacio-bt? (izq-bt btree)) (vacio-bt? (der-bt btree))))

Por ejemplo, para construir el árbol binario de la figura anterior y operar con él:

(define btree (make-bt 10

(make-bt 5 (make-hoja-bt 3) 'vacio-bt) (make-bt 23 (make-hoja-bt 12) (make-hoja-bt 28))))

(hoja-bt? (izq-bt btree)) #f

(hoja-bt? (izq-bt (izq-bt btree))) #t (dato-bt (izq-bt (izq-bt btree))) 3 (dato-bt (izq-bt (der-bt btree))) 12

Con esta implementación, los árboles binarios son listas estructuradas de tres elementos. Por ejemplo, la lista estructurada del árbol anterior es:

(10 (5 (3 vacio-bt vacio-bt) vacio-bt) (23 (12 vacio-bt vacio-bt) (28 vac io-bt vacio-bt)))

(to-list-bt bt) : devuelve una lista formada por los elementos del bt

(40)

(member-bt? x bt) : busca el elemento x en un árbol binario ordenado

(insert-bt x bt) : “inserta” (realmente, no modifica el árbol que se pasa como parámetro, sino que crea otro) un dato en un árbol binario ordenado

(insert-list-bt lista bt) : “inserta” una lista en un árbol binario ordenado (list-to-bt list) : construye un árbol binario ordenado a partir de una lista

Devuelve una lista plana con todos los datos del árbol binario

(define (to-list-bt btree) (if (vacio-bt? btree) '()

(append (to-list-bt (izq-bt btree)) (list (dato-bt btree))

(to-list-bt (der-bt btree)))))

Ejemplo:

(to-list-bt btree) (3 5 10 12 23 28)

Comprueba si un número pertenece a un árbol binario ordenado

(define (member-bt? x bt) (cond ((vacio-bt? bt) #f) ((= x (dato-bt bt)) #t) ((< x (dato-bt bt)) (member-bt? x (izq-bt bt))) (else (member-bt? x (der-bt bt))))) Ejemplos: (member-bt? 12 btree) #t (member-bt? 13 btree) #f

Devuelve un nuevo árbol binario en el que se ha añadido un número (se aplica a árboles 4.2.1. to-list-bt

4.2.2. member-bt?

(41)

binarios ordenados) (define (insert-bt x bt) (cond ((vacio-bt? bt) (make-hoja-bt x)) ((< x (dato-bt bt)) (make-bt (dato-bt bt) (insert-bt x (izq-bt bt)) (der-bt bt))) ((> x (dato-bt bt)) (make-bt (dato-bt bt) (izq-bt bt) (insert-bt x (der-bt bt)))) (else bt))) Ejemplos:

(insert-bt 12 'vacio-bt) (12 vacio-bt vacio-bt) (define btree2 (insert-bt 13 btree))

(member-bt? 13 btree2) #t

(hoja-bt? (izq-bt (der-bt btree2))) #f

(dato-bt (der-bt (izq-bt (der-bt btree2)))) 13 btree2 (10 (5 (3 vacio-bt vacio-bt) vacio-bt) (23 (12 vacio-bt (13 vacio-bt vacio-bt)) (28 vacio-bt vacio-bt)))

Devuelve un árbol binario ordenado resultado de añadir una lista de números a un árbol binario inicial

(define (insert-list-bt list bt) (if (null? list)

bt

(insert-list-bt (cdr list) (insert-bt (car list) bt))))

Construye un árbol binario ordenado a partir de una lista de números 4.2.4. list-to-bt

(42)

(define (list-to-bt lista)

(insert-list-bt lista 'vacio-bt))

Ejemplo:

(list-to-bt '(12 23 10 1))

(12 (10 (1 vacio-bt vacio-bt) vacio-bt)

(23 vacio-bt vacio-bt))

Una definición recursiva:

Un árbol genérico está formado por un dato y una lista de árboles hijos (también árboles) La lista de árboles hijos puede ser vacía.

No usamos el concepto de árbol-vacio, un árbol o nodo hoja es un árbol cuya lista de hijos es una lista vacía.

Un ejemplo de árbol genérico Funciones:

(make-tree dato lista-hijos) : construye un árbol a partir de un dato y una lista

5. Árboles genéricos

5.1. Barrera de abstracción

(43)

de hijos formada por árboles. La lista puede ser vacía. (dato-tree tree) : devuelve el dato de la raíz del árbol (hijos-tree tree) : devuelve una lista de árboles hijos

(hoja-tree? tree) : predicado que comprueba si el árbol es un nodo hoja (no tiene hijos)

Bosque: lista de árboles (devueltos por la función hijos-tree)

(define (make-tree dato lista-hijos) (cons dato lista-hijos)) (define (make-hoja-tree dato) (make-tree dato '()))

(define (dato-tree tree) (car tree)) (define (hijos-tree tree) (cdr tree))

(define (hoja-tree? tree) (null? (hijos-tree tree)))

El árbol anterior se puede construir con las siguientes instrucciones:

(define tree (make-tree '*

(list (make-tree '+ (list (make-hoja-tree 5)

(make-tree '* (list (make-hoja-tre e 2) (make-hoja-tree 3)))

(make-hoja-tree 10))) (make-tree '- (list (make-hoja-tree 12))))))

Cuando trabajamos con árboles genéricos y hacemos funciones recursivas que los recorren, es muy importante considerar en cada caso con qué tipo de dato estamos trabajando y usar la barrera de abstracción adecuada:

La función hijos-tree devuelve una lista de árboles, que podemos recorrer usando car y cdr

El car de una lista de árboles (devuelta por hijos-tree ) es un árbol y debemos de usar las funciones de su barrera de abstracción: dato-tree e hijos-tree

La función dato-tree devuelve un dato de árbol, del tipo que guardemos en el árbol Por ejemplo, para obtener el número 2 en el árbol anterior habría que evaluar la siguiente expresión:

(44)

(dato-tree (car (hijos-tree (cadr (hijos-tree (car (hijos-tree tree)))))) )

2

Si expresamos el árbol en forma de lista podemos comprobar que es una lista estructurada

(* (+ (5) (* (2) (3)) (10)) (- (12)))

De hecho, podríamos construirlo también utilizando directamente su formulación como una lista estructurada (aunque estaríamos rompiendo la barrera de abstracción).

(define tree '(* (+ (5) (* (2) (3)) (10)) (- (12))))

Un árbol genérico de n hijos se implementa con una lista de n+1 elementos (el nodo de la raíz y sus n hijos). Un nodo hoja (árbol sin hijos) se implementa, por tanto, con una lista de un único elemento (el dato).

En adelante vamos a utilizar esta forma de definir los árboles genéricos.

Hay que hacer notar que un árbol binario no tiene la misma representación que un árbol genérico con dos hijos. Un árbol genérico con un único hijo no determina si el hijo está en la derecha o en la izquierda (que es fundamental en el árbol binario).

¿Cómo se implementa el árbol de la siguiente figura como un árbol genérico y como un árbol binario?

Árbol genérico:

(45)

(40 (18 (3) (23 (29))) (52 (47)))

Árbol binario:

(40 (18 (3 vacio-bt vacio-bt)

(23 vacio-bt (29 vacio-bt vacio-bt))) (52 (47 vacio-bt vacio-bt)

vacio-bt))

Vamos a diseñar las siguientes funciones recursivas:

(to-list-tree tree) : devuelve una lista con los datos del árbol

(cuadrado-tree tree) : eleva al cuadrado todos los datos de un árbol manteniendo la estructura del árbol original

(map-tree f tree) : devuelve un árbol con la estructura del árbol original aplicando la función f a subdatos.

(niveles-tree tree) : devuelve el número de niveles de un árbol Todas comparten un patrón similar de recursión mutua.

Queremos diseñar una función (to-list-tree tree) que devuelva una lista con los datos del árbol en un recorrido inorden.

(define (to-list-tree tree) (cons (dato-tree tree)

(to-list-bosque (hijos-tree tree))))

(define (to-list-bosque bosque) (if (null? bosque)

'()

(append (to-list-tree (car bosque)) (to-list-bosque (cdr bosque)))))

La función utiliza una recursión mutua: para listar todos los nodos, añadimos el dato a la lista de nodos que nos devuelve la función to-list-bosque . Esta función coge una lista de árboles (un bosque) y devuelve la lista inorden de sus nodos. Para ello, concatena la lista de

5.2. Funciones recursivas

(46)

los nodos de su primer elemento (el primer árbol) a la lista de nodos del resto de árboles (que devuelve la llamada recursiva).

Ejemplo:

(to-list-tree '(* (+ (5) (* (2) (3)) (10)) (- (12)))) (* + 5 * 2 3 10 - 12)

Una definición alternativa usando funciones de orden superior:

(define (to-list-tree tree) (if (null? (hijos-tree tree)) (list (dato-tree tree)) (cons (dato-tree tree)

(apply append (map to-list-tree (hijos-tree tree))))))

Esta versión es muy elegante y concisa. Usa la función map que aplica una función a los elementos de una lista y devuelve la lista resultante. Como lo que devuelve

(hijos-tree tree) es precisamente una lista de árboles podemos aplicar a sus elementos cualquier función definida sobre árboles. Incluso la propia función que estamos definiendo (¡confía en la recursión!).

Veamos ahora la función (cuadrado-tree tree) que toma un árbol de números y devuelve un árbol con la misma estructura y sus datos elevados al cuadrado:

(define (cuadrado-tree tree)

(make-tree (cuadrado (dato-tree tree))

(cuadrado-bosque (hijos-tree tree))))

(define (cuadrado-bosque bosque) (if (null? bosque)

'()

(cons (cuadrado-tree (car bosque))

(cuadrado-bosque (cdr bosque)))))

Ejemplo:

(cuadrado-tree '(2 (3 (4) (5)) (6))) (4 (9 (16) (25)) (36))

Versión 2, con map :

(47)

(define (cuadrado-tree tree)

(make-tree (cuadrado (dato-tree tree))

(map cuadrado-tree (hijos-tree tree))))

La función map-tree es una función de orden superior que generaliza la función anterior. Definimos un parámetro adicional en el que se pasa la función a aplicar a los elementos del árbol.

(define (map-tree f tree)

(make-tree (f (dato-tree tree))

(map-bosque f (hijos-tree tree))))

(define (map-bosque f bosque) (if (null? bosque)

'()

(cons (map-tree f (car bosque)) (map-bosque f (cdr bosque))))) Ejemplos: (map-tree cuadrado '(2 (3 (4) (5)) (6))) (4 (9 (16) (25)) (36)) (map-tree (lambda (x) (+ x 1)) '(2 (3 (4) (5)) (6))) (3 (4 (5) (6)) (7)) Con map :

(define (map-tree f tree)

(make-tree (f (dato-tree tree)) (map (lambda (x)

(map-tree f x)) (hijos-tree tree))))

Vamos por último a definir una función que devuelve los niveles de un árbol. Un árbol con un único nodo tiene 1 nivel.

Solución 1:

5.2.3. map-tree

(48)

(define (niveles-tree tree) (if (hoja-tree? tree) 1

(+ 1 (max-niveles-bosque (hijos-tree tree)))))

(define (max-niveles-bosque bosque) (if (null? bosque)

0

(max (niveles-tree (car bosque))

(max-niveles-bosque (cdr bosque)))))

Ejemplos:

(niveles-tree '(2)) 1

(niveles-tree '(4 (9 (16) (25)) (36)) 3

Solución 2:

La función max-niveles-bosque puede implementarse de una forma más concisa todavía usando las funciones apply y map :

(define (max-niveles-bosque bosque)

(apply max (map niveles-tree bosque)))

La función map mapea la función niveles-tree a todos los elementos del bosque (lista de árboles) devolviendo una lista de números, de la que obtenemos el máximo aplicando ( apply ) la función max .

Implementa la función (nodos-niveles-tree tree) que devuelve una lista con el número de nodos en cada nivel del árbol.

Abelson y Sussman: Cap 1.2 (Procedures and the processes they generace), Introducción capítulo 2 (Building Abstraction

with Data) pp. 79–89 y pp. 107–113

Lenguajes y Paradigmas de Programación, curso 2013–14

© Departamento Ciencia de la Computación e Inteligencia Artificial, Universidad de Alicante

5.3. Ejercicios

(49)

Referencias

Documento similar

Volviendo a la jurisprudencia del Tribunal de Justicia, conviene recor- dar que, con el tiempo, este órgano se vio en la necesidad de determinar si los actos de los Estados

La última de las mesas, integrada por representantes del sector privado, abordará soluciones aplicadas a situaciones reales de Datos Abiertos para que sirvan de acicate a

b) El Tribunal Constitucional se encuadra dentro de una organiza- ción jurídico constitucional que asume la supremacía de los dere- chos fundamentales y que reconoce la separación

Se ha visto en cuanto precede que un gran número de los productos que los árboles nos proporcionan se emplean o como medicamentos o para fabricar éstos, pero, no obstante, se van

Y en el caso específico del CEDH, valor orientativo mediado por la jurisprudencia del TEDH (6). El derecho a la inviolabilidad del domicilio que proclama el artículo 18.2 CE

Porque antiguamente se había establecido que hubiese quienes públicamente interpretaran el derecho, a cuales se dio por el César el de derecho de responder, y se les

&#34;No pasó nada&#34;, dijo el jardinero, recogiendo la bolsa y lo que se había caído de ella y tirando todo dentro.. Lo miré y un grito estrangulado escapó de

 Durante la 2ª sesión, cada alumno dispondrá de una cartulina o papel de tamaño DIN A3 con la silueta de un gran árbol lleno de ramas (los profesores de 1er ciclo de Primaria