49
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 1. Recursión

Tema 4: Procedimientos y estructuras recursivas€¦ · 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

  • Upload
    others

  • View
    5

  • Download
    0

Embed Size (px)

Citation preview

1. Recursión

1.1 Pensando recursivamente1.2 El coste de la recursión1.3 Soluciones al coste de la recursión: procesos iterativos1.4 Soluciones al coste de la recursión: memoization

2. Listas estructuradas

2.1. Definición y ejemplos2.2. Pseudo árboles con niveles2.3. Funciones recursivas sobre listas estructuradas2.4. Ejercicios

3. Datos funcionales y barreras de abstracción

3.1. Datos funcionales3.2. Barrera de abstracción3.3. Números racionales

4. Árboles binarios

4.1. Barrera de abstracción4.2. Funciones recursivas

5. Árboles genéricos

5.1. Barrera de abstracción5.2. Funciones recursivas5.3. Ejercicios

Ya hemos visto algunos ejemplos de funciones recursivas. Una función es recursiva cuando sellama 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

1. Recursión

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

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 enun 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 elFortran, 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 costeespacial y temporal de la recursión. Más adelante comprobaremos que no siempre una funciónrecursiva tiene un comportamiento recursivo, sino que hay casos en los que genera unproceso iterativo.

En las siguientes clases del tema veremos que la recursión no sólo se utiliza para definirfunciones 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 quediseñar la solución recursiva desde el principio. Debemos fijarnos en lo que devuelve la funcióny debemos preguntarnos cómo sería posible descomponer el problema de forma que podamoslanzar la recursión sobre una versión más sencilla del mismo. Supondremos que la llamadarecursiva funciona correctamente y devuelve el resultado correcto. Y después debemostransformar este resultado correcto de la versión más pequeña en el resultado de la solucióncompleta.

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

Debes confiar en que la llamada recursiva va a hacer su trabajo y devolver el resultadocorrecto, sin preocuparte de cómo lo va a hacer. Después tendrás que utilizar lo que lallamada 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

1.1. Pensando recursivamente

reflexionar sobre la solución recursiva con algún ejemplo. El objetivo es obtener unaformulación abstracta del caso general de la recursión antes de programarlo. Una vez queencontramos 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 puederecibir como parámetro la recursión. Debe devolver un valor compatible con la definición de lafunción. Por ejemplo, si la función debe construir una lista, el caso base debe devolver tambiénuna lista. Si la función construye una pareja, el caso base también devolverá una pareja. Nodebemos 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 lalongitud de una lista en términos recursivos? Tenemos que pensar: “Si puedo calcular lalongitud 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 listasin el primer elemento con una llamada a la recursión, y le sumo 1 al número que devuelve esallamada”.

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 casobase 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 formarecursiva?. Por ejemplo, las siguientes listas son palíndromas:

1.1.1. Longitud de una lista

1.1.2. Lista palíndroma

'(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 listaresultante 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ónanterior. En este caso, una lista de un elemento y una lista vacía también las consideraremospalíndromas.

palindroma(lista) <=> (primer-elemento(lista) == ultimo-elemento(lista)) ypalindroma(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)))

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 esidéntica a la figura total, pero reducida de escala. Esto nos da una pista de que es posibledibujar la figura con un algoritmo recursivo.

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

Podríamos construir un triángulo de Sierpinski más grande dibujando 3 veces el mismotriá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 untriángulo de altura h situado en la posición x, y basándonos en 3 llamadas recursivas atriángulos más pequeños. En el caso base, cuando h sea menor que un umbral, dibujaremosun 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

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 untriá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íagraphics/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 ejeX (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 algoritmoen Scheme del triángulo de Sierpinski.

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

1.1.4. Gráficos de tortuga en Racket

(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 condos 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 deejecució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 usarprogramación funcional para generar una lista de comandos y usar código imperativo sóloen 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 lasiguiente 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 llamadarecursiva que ejecuta el resto:

hipot(x) = = x+x2 x2‾ ‾‾‾‾‾‾√ 2‾‾√

(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 longitud100:

(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. Noes funcional porque se realizan pasos de ejecución, usando la forma especial begin omúltiples instrucciones en una misma función (por ejemplo la función triangle ).

1.1.5. Sierpinski en Racket

(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 asierpinski pero que en lugar de dibujar, devuelve una lista de comandos. La llamamossierpinski-list :

1.1.6. Sierpinski funcional

(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)) '(turn -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))) ;; volvemos 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.142135623730951) (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.142135623730951) (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

En la recursión mutua definimos una función en base a una segunda, que a su vez se defineen 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 imparx es impar si x–1 es par0 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 elespacio

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 formuladibujando la curva i-ésima a partir de la curva i–1.

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

1.1.8. Ejemplo avanzado: curvas de Hilbert

La función (h-der i w) dibuja una curva de Hilbert de orden i con una longitud de trazo wa 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 wa 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:

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

El resultado de esta última llamada es:

Hilbert en Scheme

Vamos a estudiar el comportamiento del proceso generado por una llamada a unprocedimiento 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

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

Cuando la recursión devuelve un valor, los valores se recuperan de la pila, se realiza lallamada y se devuelve el valor a la anterior llamada en espera. Si la recursión está mal hecha ynunca termina se genera un stack overflow.

Es posible hacer que Racket haga una traza de la secuencia de llamadas a la recursiónutilizando la librería trace.ss . Una vez cargada la librería puedes activar y desactivar lastrazas 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 elusuario, no se pueden tracear funciones primitivas de Scheme como + o car . Siqueremos 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.

Por ejemplo, vamos a tracear las llamadas recursivas a mi-length y a la suma:

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

El coste espacial de un programa es una función que relaciona la memoria consumida por unallamada para resolver un problema con alguna variable que determina el tamaño del problemaa 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

1.2.3. El coste depende del número de llamadas a la recursión

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 llamadasfinales 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 (fibonaci100)?

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

Es posible definir procedimientos recursivos que generen procesos iterativos (como los buclesen programación imperativa) en los que no se dejen llamadas recursivas en espera ni seincremente la pila de la recursión. Para ello construimos la recursión de forma que en cadallamada se haga un cálculo parcial y en el caso base se pueda devolver directamente elresultado 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 pilade 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 procesoiterativo

Tiene un parámetro adicional ( product ) que es el parámetro en el que se iránguardando 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

1.3.1. Factorial iterativo

(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 eleganteSe necesita una parámetro adicional en el que se van acumulando los resultadosparcialesLa última llamada a la recursión devuelve el valor acumuladoEl proceso resultante de la recursión es iterativo en el sentido de que no deja llamadas enespera 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

1.3.4 Fibonacci iterativo

(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) = 1Pascal (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:

1.3.5. Triángulo de Pascal

(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 lositerativos es la memoization. Si miramos la traza de (fibonacci 4) podemos ver que el costeestá 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 deasociació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

1.4.1. Fibonacci con memoization

(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 procesorecursivo visto anteriormente y la técnica de memoización, en la que se consulta el valor deFibonacci 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)))))

Podemos comprobar la diferencia de tiempos de ejecución entre esta versión y la anterior. Elcoste de la función memoizada es O(n). Frente al coste O(2n) de la versión inicial que la hacíaimposible 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 dedatos recursiva, formadas a partir de parejas y de una lista vacía. Una vez conocida suimplementació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 lafunció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 formadaspor 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 quecontienen 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 listaestructurada 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 hojaEl elemento 'b , otra hojaLa 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 ningunasublista. Por ejemplo, la lista

2. Listas estructuradas

2.1. Definición y ejemplos

’((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:

2.1.1. Definiciones en Scheme

(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 lanegació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 dondequeremos 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, endonde se agrupan los distintos elementos de la oración:

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

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

2.1.2. Ejemplos de listas estructuradas

'((<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 elprimer nivel, y cada sublista representa un nivel inferior. Los datos de las listas representan lashojas.

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

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 tieneuna altura de 1, la lista anterior tiene una altura de 2.

Veamos una definición más precisa.

altura(lista vacía) = 0altura(hoja) = 0altura(lista) = max (1 + altura (primer-elemento (lista)), altura (resto (lista))

En Scheme:

2.2. Pseudo árboles con niveles

2.2.1 Altura de una lista estructurada

(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 hojascomo listas.

Es posible definir la función utilizando las función de orden superior map para aplicar lapropia 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 laslistas 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

2.3.1. num-hojas

Definición matemática:

num-hojas(lista vacía) = 0num-hojas(hoja) = 1num-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

2.3.2. pertenece?

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 desus 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 recursivassobre las listas estructuradas.

Escribe la función (tres-elementos? lista) que tome una lista estructurada comoargumento. Devolverá #t si cada subsista que aparece y cada uno de sus elementos tienen 3elementos. 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

tres-elementos?

(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 listasestructuradas con la misma estructura, pero con diferentes elementos, y devuelva una lista deparejas 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 doslistas como argumento: la lista plantilla será una lista estructurada y estará compuesta pornúmeros enteros positivos con una estructura jerárquica, como (2 (3 1) 0 (4)) . Lasegunda lista será una lista plana con tantos elementos como indica el mayor número deplantilla más uno (en el caso anterior serían 5 elementos). El procedimiento deberá devolveruna lista estructurada donde los elementos de la segunda lista se sitúen (en situación y enestructura) 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 ydevuelva el nivel en que se encuentra. Suponemos que el dato está en la lista y no estárepetido.

diff-listas

transformar

nivel-hoja

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 unalista 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 pornúmeros y un número de nivel. Devolverá la suma de todos los nodos hoja que tengan unaprofundidad 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 (quepodemos 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ísticafundamental 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

3.1. Datos funcionales

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

Los datos funcionales son muy útiles y se recomienda su utilización en la mayoría delenguajes. En los lenguajes orientados a objetos como Java o Scala este tipo de datos recibenel 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 ointerfaces para definir nuevos datos. Debemos crearlos de forma ad-hoc definiendo unconjunto de funciones que construyen y trabajan con los datos y teniendo la disciplina de sóloutilizar esas para operar con los datos. Es lo que llamamos barrera de abstracción del dato. Aldefinir nuevas operaciones que trabajen con un dato no debemos “saltar” esa barrera deabstracció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 theprocedure would be used from the details of how the procedure is implemented in termsof more primitive procedures. The analogous notion for compound data is called dataabstraction. Data abstraction is a methodology that enables us to isolate how acompound data object is used from the details of how it is constructed from moreprimitive data objects.

The basic idea of data abstraction is to structure the programs that are to use compounddata objects so that they operate on “abstract data”. That is, our programs should usedata in such a way as to make no assumptions about the data that are not strictlynecessary for performing the task at hand. At the same time, a “concrete” datarepresentation is defined independent of the programs that use the data. The interfacebetween these two parts of our system will be a set of procedures,called selectors and constructors, that implement the abstract data in terms of theconcrete representation.

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

3.1.1. Abstracción de datos

3.2. Barrera de abstracción

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

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

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

Veamos un ejemplo de dato funcional. Supongamos que queremos construir el tipo de datonúmero racional, un número formado por un numerador y un denominador, con lasoperaciones 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 deabstracción. Utilizaremos el sufijo ‘rat’ (del inglés ‘rational’) para todas las funciones quetrabajan 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 elementosSelectores: Funciones que devuelven algún elemento a partir del datoOperadores: Funciones que definen operaciones sobre los datos. Algunas devuelvennuevos datos resultantes de la operación o devuelven otros tipos

En programación funcional no existen mutadores. Una vez construido un dato no se puedemodificar (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úmerosracionales

3.3.1. Diseño de la barrera de abstracción números racionales

div-rac

Constructores

(make-rac num denom) : Toma dos números enteros num y denom y devuelveun 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.

3.3.2. Implementación de los datos racionales

(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íael vocabulario y las capacidades de nuestros programas. Ahora podemos usar númerosracionales de la misma forma que usamos números enteros o reales:

(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 laimplementación de las funciones sin afectar a las funciones que las usan. Si esas funcioneshan respetado la definición de la barrera y no han accedido directamente a la implementaciónvan a seguir funcionando sin problemas.

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

Tenemos que modificar el constructor make-racional y los selectores num-rac ydenom-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 hacenuso de estas funciones de la barrera de abstracción.

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

(define (racional? x) (and (pair? x) (equal? (car x) 'racional)))

Ejemplos:

3.3. Modificación de la implementación

(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 hijoderecho (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 laestructura

4. Árboles binarios

4.1. Barrera de abstracción de árbol binario

4.1.1. Definición del árbol binario y de su barrera de abstracción

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 árbolbinario 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

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

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

4.2. Funciones recursivas

(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?

4.2.3. insert-bt

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)))) ⇒ 13btree2⇒ (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 binarioinicial

(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

(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 hijoses 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

5.1.1. Definición del árbol genérico y de su barrera de abstracción

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

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-tree 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 labarrera de abstracción adecuada:

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

El car de una lista de árboles (devuelta por hijos-tree ) es un árbol y debemos deusar 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 siguienteexpresión:

5.1.2. Implementación

(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 unalista 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ízy 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 árbolgenérico con dos hijos. Un árbol genérico con un único hijo no determina si el hijo está en laderecha 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 árbolbinario?

Árbol genérico:

5.1.3. Los árboles genéricos son listas estructuradas

(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 losdatos 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 listade 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

5.2.1. (to-list-tree tree)

los nodos de su primer elemento (el primer árbol) a la lista de nodos del resto de árboles (quedevuelve 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 loselementos 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 estamosdefiniendo (¡confía en la recursión!).

Veamos ahora la función (cuadrado-tree tree) que toma un árbol de números ydevuelve 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 :

5.2.2. (cuadrado-tree tree)

(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

5.2.4. niveles-tree

(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 concisatodaví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 (listade á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 elnúmero de nodos en cada nivel del árbol.

Abelson y Sussman: Cap 1.2 (Procedures and the processes they generace), Introduccióncapítulo 2 (Building Abstractionwith 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

Bibliografía

Domingo Gallardo, Cristina Pomares