Clase # 12 Hasta ahora hemos construido TDAs con un

Anuncio
Clase # 12
Hasta ahora hemos construido TDAs con un número fijo de componentes, estos objetos son estáticos. En
muchas aplicaciones requerimos de que los objetos de datos tengan un número variable de componentes,
son estructuras dinámicas, aunque sus componentes en algún nivel de resolución son estáticos; este es el
caso de las listas.
Si por ejemplo le pedimos a cada estudiante que construya su lista de materias inscritas, las listas pueden
tener diferentes tamaños e incluso pueden haber estudiantes con su lista vacía. Las listas se pueden
escribir de izquierda a derecha o de arriba hacia abajo, pero siempre habrá un orden implícito en donde
podremos hablar del “primer elemento” de la lista o el “último elemento”.
Una lista es “una colección de 0 o más elementos escritos en determinado orden”. La definición de la lista
es de naturaleza recursiva en donde:
Caso base: una lista de 0 elementos es una lista vacía
Caso general: una lista consta de dos partes:
• el primer elemento llamado cabeza
• una lista del resto de los elementos llamada cola
Otros objetos de naturaleza recursiva son las expresiones artiméticas en donde una expresión se puede
definir de la siguiente manera:
Caso base: una variable (representada por un identificador)
Caso general: una expresión consta de tres partes:
• una expresión
• un operador
• una expresión
También podemos tener un árbol genealógico en donde
Caso base: un nombre
Caso general: un árbol genealógico consta de tres partes:
• nombre de la persona
• árbol genealógico de la madre
• árbol genealógico del padre
Para implementar el TDA lista en el lenguaje Scheme podemos usar pares ya que se tiene que la lista
consta de dos partes, sin embargo debemos tomar en cuenta el caso base.
Se aplicarán los siguientes procedimientos:
(cons ele lst) dado un elemento ele y una lista l, cons retorna una lista con cabeza ele y cola lst.
(car lst) Si la lista lst no está vacía, car retorna su cabeza.
(cdr lst) Si la lista lst no está vacía, cdr retorna su cola.
(null? lst) Retorna verdadero si la lista lst está vacía
También podemos escoger otros nombres como hacer-lista, cabeza, cola, lista-vacia?
para mantener el principio de abstracción de datos.
Para construir listas en Scheme podemos usar cons. Por ejemplo:
(cons 1 (cons 2 (cons 3 (cons 4)))) construye la lista (1 2 3 4)
Existe un procedimiento primitivo list tal que
(list 1 2 3 4) también construye la lista (1 2 3 4)
Técnicas básicas de procesamiento de listas
Para realizar el procesamiento de listas se toma en cuenta su naturaleza recursiva. Así tendremos que para
contar el número de elementos de una lista:
(define longitud
(lambda (lst)
(if (null? lst) 0
(+ 1 (longitud (cdr lst))))))
o para encontrar la suma de una lista de enteros se tendría:
(define sum
(lambda (lst)
(if (null? lst)0
(+ (car lst) (sum (cdr lst))))))
Estos procedimientos generan procesos recursivos y se dice que utilizan la técnica de cdr down en una
lista ya que hacen la llamada recursiva sobre el cdr o la cola de la lista. Su orden de crecimiento es lineal
con respecto a la longitud de la lista, tanto en tiempo como en espacio.
Ejercicio 1: Desarrolle la versión iterativa de estos procedimientos.
Ejercicios utilizando cdr down
1. Defina un predicado que determinará si un elemento particular se encuentra en una lista.
2. Generalice el ejercicio anterior a un predicado que determinará si existe algún elemento de la lista
que satisface el predicado.
3. Defina un proceidmiento que retorne el primer elemento de la lista que satisface un predicado dado.
4. Defina un procedimiento que encuentre la posición de un elemento particular dentro de la lista
(asume que la posición del primer elemento es 0).
Para ejercitar cdr down sobre dos listas:
5.
Defina un procedimiento que tome dos listas de enteros del mismo tamaño y retorne verdadero si
cada elemento de la primera lista es menor que el elemento correspondiente en la segunda lista. ¿Qué
pasaría si las listas son de diferentes tamaños?
Frecuentemente utilizamos listas para construir otras listas, en ese caso hacemos cdr down de una lista
mientras hacemos cons up de la otra lista.
Por ejemplo para seleccionar los elementos de una lista que satisfacen cierto predicado tendremos:
(define filtro
(lambda (ok? lst)
(cond ((null? lst)
‘())
((ok? (car lst))
(cons (car lst (filtro ok? (cdr lst))))
(else
(filtro ok? (cdr lst))))))
>(filtro impar? (enteros-desde-hasta 1 15))
(1 3 5 7 9 11 13 15)
A continuación desarrollaremos un proyecto que requiere procesamiento de listas. Se quiere saber cuántos
“barajeos perfectos” se necesitan hacer en un paquete de 52 cartas para llevarlos a su orden original. (Un
“barajeo perfecto” divide al paquete en dos partes iguales que luego son combinados de una manera
alternada estricta comenzando con la primera carta de la primera mitad).
Podemos representar nuestro paquete original como una lista de números del 1 al 52 usando el
procedimiento enteros-desde-hasta :
(define enteros-desde-hasta
(lambda (desde hasta)
(if (> desde hasta)
‘()
(cons desde
(enteros-desde-hasta (+ 1 desde) hasta)))))
Para dividir al paquete en dos mitades tendremos dos procedimientos, uno para conseguir los primeros n
elementos de una lista, y otro procedimiento para conseguir el resto de los elementos.
(define primeros-elementos-de
(lambda (n lst)
(if (= n 0)
‘()
(cons (car lst)
(primeros-elementos-de (- n 1)
(cdr lst))))))
Utilizaremos la primitiva resto-de la cual dado un n devuelve todos los elementos excepto los n
primeros.
(define resto-de
(lambda (n lst)
(if (= n 0)
lst
(resto-de (- n 1) (cdr lst)))))
El procedimiento intercalar toma dos listas y las combina en una sola de manera alternada:
(define intercalar
; intercala lst1 y lst2 comenzando con
(lambda (lst1 lst2) ; el primer elemento de lst1 (si hay)
(if (null? lst1)
lst2
(cons (car lst1)
(intercalar lst2 (cdr lst1))))))
(define barajar
(lambda (paquete tamano)
(let ((mitad (quotient (+ tamano 1) 2)))
(intercalar (primeros-elementos-de mitad paquete)
(resto-de paquete mitad)))))
Para saber cuantos barajeos se requieren se escribe el siguiente procedimiento:
(define barajeo-multiple
(lambda (paquete tamano veces)
(if (= veces 0)
paquete
(barajeo-multiple (barajar paquete tamano)
tamano
(- veces 1)))))
Hacemos varias llamadas a barajeo-multiple con diferente número de veces, con 8 veces retornamos a
nuestro paquete original.
Procesamiento de listas e iteración
Para ejemplificar el procesamiento de listas de manera iterativa, determinaremos si una lista de símbolos
es palíndrome.
Un palíndrome es una palabra que permanece igual cuando la escribimos con las letras invertidas (por ej.,
arepera). Hay frases completas que son palíndromes si ignoramos signos de puntuación y espacios.
Invertiremos la lista de símbolos y veremos si el resultado es igual a la lista original. Para invertir una
lista necesitamos invertir el cdr de la lista y luego “añadir” el car de la lista al final.
Definamos el procedimiento para anadir-al-final
(define anadir-al-final
(lambda (lst elt)
(if (null? lst)
; anadir a lista vacia
(cons elt ‘()) ; hace una lista de un elemento
(cons (car lst)
(anadir-al-final (cdr lst) elt)))))
(define invertir
(lambda (lst)
(if (null? lst)
‘()
(anadir-al-final (invertir (cdr lst))
(car lst)))))
Esta manera de invertir una lista consume mucho tiempo debido a la llamada a anadir-al-final
la cual hace un recorrido completo de la lista. Veremos cuántas veces hacemos cons para determinar el
espacio.
Añadir al final de una lista de k elementos hace k+1 llamadas a cons. Llamaremos R(n) al número de cons
que requiere invertir (indirectamente a través de anadir-al-final) una lista de tamaño n. R(0)=0 ya que
invertir devuelve la lista vacia.
R(n) será igual a la cantidad de cons requeridos para invertir el cdr de la lista, o sea R(n-1), más los que se
necesiten para añadir al final de esta lista invertida.
R(n)= R(n-1) + ((n-1) + 1) = R(n-1) + n
R(n-1) = R(n-2) + ((n-2) + 1) = R(n-2) + (n-1)
R(n)=R(n-1) + n
=R(n-2) + (n-1) + n
.
.
=R(0)+1+2+3……+(n-2)+(n-1)+n = 0+1+2+3……+(n-2)+(n-1)+n = n(n+1)/2
El tiempo es O(n2) donde n es la longitud de la lista
Definiremos ahora un invertir iterativo basándonos en el principio de que si tenemos por ejemplo un
paquete de cartas boca arriba y la queremos invertir, solo tenemos que ir tomando en orden las cartas y
colocándolas una encima de la otra.
(define invertir
(lambda (lst)
(define invertir-iter ;retorna la lista de elementos de lst1
(lambda (lst1 lst2) ;en orden inverso seguidos de
; los elementos de lst2
(if (null? lst1)
lst2
(invertir-iter (cdr lst)
(cons (car lst1)
lst2)))))
(invertir-iter lst ‘())))
El procedimiento interno invertir-iter hace un cons para cada elemento de la lista, el número total de cons
es igual al número total de cdr. El número total de cons es n, o sea que el proceso se ha reducido a O(n)
en tiempo. Para saber si la palabra es palíndrome se tiene:
(define palindorme?
(lambda(lst)
(equal? lst (invertir lst))))
Recursión de árbol y listas
Para examinar recursión de árbol desarrollarremos el merge sort. El enfoque es separar la lista en dos
listas más pequeñas, hacer merge sort de cada una de ellas, y luego hacer merge de ellas (mezclarlas).
(define merge-sort
(lambda (lst)
(cond ((null? lst)
‘())
((null? (cdr lst))
lst
(else
(merge (merge-sort (una-parte lst))
(merge-sort (la-otra-parte lst)))))))
Para el procedimiento merge tenemos dos casos base que ocurren cuando alguna de las listas está vacía,
en esos casos retornamos la otra lista. Tenemos adicionalmente tres casos recursivos dependiendo de la
comparación de los primeros elementos de cada una de las listas.
(define merge
(lambda (lst1 lst2)
(cond ((null? lst1) lst2)
((null? lst2) lst1)
((< (car lst1) (car lst2))
(cons (car lst1) (merge (cdr lst1) lst2)))
((= (car lst1) (car lst2))
(cons (car lst1) (merge (cdr lst1) (cdr lst2))))
(else
(cons (car lst2) (merge lst1 (cdr lst2)))))))
Para separar la lista en dos nos basaremos en que una mitad tiene los elementos 1, 3, 5, 7….. y la otra
mitad tiene los elementos 2, 4, 6, 8….. Tendremos los siguientes dos procedimientos:
(define parte-impar
(lambda (lst)
(if (null? lst)
‘()
(cons (car lst) (parte-par (cdr lst))))))
(define parte-par
(lambda (lst)
(if (null? lst)
‘()
(cons (car lst) (parte-impar (cdr lst))))))
Este es un ejemplo de dos procedimientos mutuamente recursivos. Para hacer el merge-sort tendremos:
(define una-parte parte-impar)
(define la-otra-parte parte-par)
Descargar