Problema: Agenda Queremos programar una agenda de contactos. De cada persona nos interesa guardar: Introducción a la Computación Primer Cuatrimestre de 2012 I Nombre I Teléfono I Dirección ¿Cómo representamos los datos de las personas? Tipos Abstractos de Datos I Nombres: arreglo de strings. I Teléfonos: arreglo de strings. I Direcciones: arreglo de strings. Tales que la i-ésima posición de los 3 arreglos correspondan a la misma persona. Esta representación funciona (cumple el objetivo), pero tiene serios problemas... 1 Problema: Agenda 2 Problema: Contar dı́as entre 2 fechas // Dice si el an~o a es bisiesto. bool bisiesto(int a) { return (a%4==0 && (a%100!=0 || a%400==0)); } Mucho mejor serı́a poder definir un tipo Persona, que encapsule todos los datos relevantes a una persona para nuestra agenda. // Devuelve la cantidad de dias del mes m, an~o a. int diasEnMes(int m, int a) { int r=0; if (m==1||m==3||m==5||m==7||m==8||m==10||m==12) r = 31; if (m==4||m==6||m==9||m==11) r = 30; if (m==2) { if (bisiesto(a)) r = 29; else r = 28; } return r; } Tipo Abstracto de Datos (TAD) Persona Operaciones: I CrearPersona(nom, tel, dir ) → Persona: Crea una persona nueva con el nombre, teléfono y dirección especificados. I Nombre(p) → String: Devuelve el nombre de la persona p. I Teléfono(p) → String: Devuelve el teléfono de la persona p. I Dirección(p) → String: Devuelve la dirección de la persona p. // Cuenta los dias entre los meses m1 y m2 // inclusive, en el an~o a. int diasEntreMeses(int m1, int m2, int a) { int r = 0; int m = m1; while (m <= m2) { r = r + diasEnMes(m, a); m = m + 1; } return r; } Ası́, podemos representar la agenda con un arreglo de Personas. 3 // Cuenta los dias entre el d1/m1/a1 y el d2/m2/a2. int contarDias(int d1, int m1, int a1, int d2, int m2, int a2) { int r = 0; if (a1 == a2 && m1 == m2) r = d2 - d1; if (a1 == a2 && m1 < m2) { r = diasEnMes(m1, a1) - d1; r = r + diasEntreMeses(m1+1, m2-1, a1); r = r + d2; } if (a1 < a2) { r = diasEnMes(m1, a1) - d1; r = r + diasEntreMeses(m1+1, 12, a1); int a = a1 + 1; while (a < a2) { r = r + 365; if (bisiesto(a)) { r = r + 1; } a = a + 1; } r = r + diasEntreMeses(1, m2-1, a2); r = r + d2; } return r; } 4 Problema: Contar dı́as entre 2 fechas Modularidad y encapsulamiento Imaginemos que contamos con un tipo Fecha que nos ofrece estas operaciones (entre otras): I CrearFecha(d, m, a) → Fecha I FechaSiguiente(f ) → Fecha: Devuelve la fecha siguiente a la fecha dada. (Ej: al 31/12/1999 le sigue el 1/1/2000.) I Menor(f1 , f2 ) → B: Devuelve TRUE si la fecha f1 es anterior que la fecha f2 , y FALSE en caso contrario. La clave está en encapsular los datos y sus operaciones. Al programar, definimos funciones (ej: Primo(n)) para generar código más simple y claro (código modular ). Encapsulamos un algoritmo para poder reusarlo muchas veces. Ahora generalizamos este concepto, y encapsulamos datos (ej: personas, fechas) y sus operaciones (ej: FechaSiguiente(f)) en Tipos Abstractos de Datos (TAD). Usando esto, un algoritmo para contar dı́as podrı́a ser: ContarDı́as(f1 , f2 ) → Z: RV ← 0 while (Menor(f1 , f2 )) { RV ← RV + 1 f1 ← FechaSiguiente(f1 ) } Para usar un TAD, el programador sólo necesita conocer el nombre del TAD y la especificación de sus operaciones. Un TAD puede estar implementado de muchas formas distintas. Esto debe ser transparente para el usuario. 6 5 TAD Lista(ELEM) TAD Lista(ELEM) Operaciones (cont.): I Operaciones: I CrearLista() → Lista(ELEM): Crea una lista vacı́a. I Agregar(x, L): Inserta el elemento x al final de L. I Longitud(L) → Z: Devuelve la cantidad de elementos de L. I Iésimo(i, L) → ELEM: Devuelve el i-ésimo elemento de L. Precondición: 0 ≤ i < Longitud(L). I Insertar(i, x, L): Inserta el elemento x en la posición i de L, pasando los elementos de la posición i y siguientes a la posición inmediata superior. Pre: 0 ≤ i ≤ Longitud(L). (Si L = a0 , .., an−1 , se convierte en L = a0 , .., ai−1 , x, ai , .., an−1 .) I BorrarIésimo(i, L): Borra el i-ésimo elemento de L Precondición: 0 ≤ i < Longitud(L). (Si L = a0 , .., an−1 , se convierte en L = a0 , .., ai−1 , ai+1 , .., an−1 .) Cantidad(x, L) → Z: Devuelve la cantidad de veces que aparece el elemento x en L. I donde L : Lista(ELEM), i : Z, x : ELEM (entero, char, etc.). Indice(x, L) → Z: Devuelve el ı́ndice (0 .. Longitud(L) − 1) de la primera aparición de x en L. Pre: Cantidad(x, L) > 0. donde L : Lista(ELEM), i : Z, x : ELEM (entero, char, etc.). ... y agregamos cualquier otra función que consideremos útil. 7 8 TAD Lista(ELEM) - Posible implementación TAD Lista(ELEM) - Posible implementación Vamos a representar una lista como una cadena de nodos. Definamos primero qué es un nodo. Primero elegimos una estructura de representación del TAD Lista(ELEM) (que es privada). Tipo Nodo(ELEM): hvalor : ELEM, siguiente : Ref(Nodo)i Lista(ELEM) == hcabeza : Ref(Nodo), longitud : Zi h...i define una tupla. Después describimos el invariante de representación de esta estructura. En este caso longitud siempre debe ser igual a la cantidad de nodos encadenados a partir de cabeza, y en la cadena de nodos no deben formarse ciclos. Ref(Nodo) es una referencia a una instancia de tipo Nodo. Puede tomar el valor especial Nil (“referencia a nada”). Construcción de un nuevo Nodo: I Por último, damos los algoritmos de las operaciones del TAD Lista(ELEM) para la estructura de representación elegida: n ← Nodo(x, r ) Acceso a los campos de un nodo n: I n.valor devuelve el campo valor . I n.siguiente devuelve el campo siguiente. CrearLista() → Lista(ELEM): RV .cabeza ← Nil RV .longitud ← 0 9 TAD Lista(ELEM) - Posible implementación 10 TAD Lista(ELEM) - Posible implementación Agregar(x, L): nuevo ← Nodo(x, Nil) if (L.cabeza = Nil) { L.cabeza ← nuevo } else { aux ← L.cabeza while (aux.siguiente 6= Nil) { aux ← aux.siguiente } aux.siguiente ← nuevo } L.longitud ← L.longitud + 1 Longitud(L) → Z: RV ← L.longitud Iésimo(i, L) → ELEM: aux ← L.cabeza while (i > 0) { aux ← aux.siguiente i ←i −1 } RV ← aux.valor (Pre: 0 ≤ i < Longitud(L)) Y ası́ con las otras operaciones del TAD Lista(ELEM): Cantidad, Insertar, BorrarIésimo e Indice. 11 12 Partes de un Tipo Abstracto de Datos I Parte pública: Disponible para el usuario externo. I I I Problema: Paréntesis balanceados Nombre y tipos paramétricos (ej: Lista(ELEM)). Operaciones y sus especificaciones (encabezado, precondición, poscondición). Dado un string, determinar si los caracteres { }, [ ], ( ) están balanceados correctamente. Ejemplos: Parte privada: Sólo accesible desde dentro del TAD. ¡El usuario externo nunca debe ver ni meterse en esto! I I I Estructura de representación del TAD. Invariante de representación: qué propiedades debe cumplir la estructura elegida para que tenga sentido como la representación de una instancia del TAD. Algoritmos de las operaciones para la estructura de representación elegida. I “{a(b)x[()]}” está balanceado. I “}”, “a(b))” y “[[})” no están balanceados. ¿Se les ocurre una solución? 14 13 TAD Pila(ELEM) Problema: Paréntesis balanceados Dado un string, determinar si los caracteres { }, [ ], ( ) están balanceados correctamente. Operaciones: I CrearPila() → Pila(ELEM): Crea una pila vacı́a. I Apilar(x, P): Inserta el elem. x sobre el tope de la pila P. I Vacı́a?(P) → B: Dice si la pila P está vacı́a. I Tope(P) → ELEM: Devuelve el elemento del tope de P. Precondición: ¬Vacı́a?(P). I Desapilar(P): Borra el elemento del tope de P. Precondición: ¬Vacı́a?(P). ¿Se les ocurre una solución usando el tipo Pila? p ← CrearPila() Para cada caracter c en el string de entrada: Si c es ‘{’, ‘[’ o ‘(’: Apilar(c, p) Si c es ‘}’, ‘]’ o ‘)’: d ← Tope(p) Desapilar(p) Si c no se corresponda con d: responder que no. Si Vacı́a?(p), responder que sı́. Si no, responder que no. donde P : Pila(ELEM), x : ELEM (entero, char, etc.). Las pilas tienen estrategia LIFO (last in, first out). 15 16 Repaso de la clase de hoy I Tipos Abstractos de Datos. I I I Parte pública: nombre + operaciones. Parte privada: estructura, invariante, algoritmos. Lista(ELEM), Pila(ELEM). Temas de la clase que viene I Una posible implementación del TAD Pila(ELEM). I TAD Cola(ELEM). I TAD Conjunto(ELEM). 17