¡Descarga Tipos Abstractos de Datos y más Apuntes en PDF de Algoritmos y Programación solo en Docsity!
PrProoggrraammaacciióónn
Tipos Abstractos de Datos
Universitat de València
Jesus Albert, Ricardo Ferrís
CONTENIDO
2.1. PROGRAMACIÓN Y ABSTRACCIÓN ................................................................................................................... 2
2.2. TIPOS DE DATOS Y ABSTRACCIÓN ..................................................................................................................... 3
2.3. CLASES EN C++ ........................................................................................................................................... 6
2.4. TIPOS ABSTRACTOS DE DATOS........................................................................................................................ 8
2.5. EJEMPLO DE CONSTRUCCIÓN DE TIPOS CON CLASES EN C++................................................................................ 10
2.1. PROGRAMACIÓN Y ABSTRACCIÓN
La abstracción es un mecanismo fundamental para la comprensión de fenómenos o
situaciones que implican gran cantidad de detalles. La idea de abstracción es uno de los
conceptos más potentes en el proceso de resolución de problemas. Se entiende por
abstracción la capacidad de manejar un objeto (tema o idea) como un concepto general, sin
considerar la enorme cantidad de detalles que pueden estar asociados con dicho objeto. Sin
abstracción no sería posible manejar, ni siquiera entender, la gran complejidad de ciertos
problemas. Por ejemplo, es muy difícil entender la organización y funcionamiento de una gran
empresa multinacional si se piensa en ella en términos de cada trabajador individual
(posiblemente miles, distribuidos por todo el mundo) o de cada uno de los productos que
fabrica, sin embargo, es más sencilla su comprensión si se ve, simplemente, como una
agrupación de departamentos especializados en una tarea concreta.
En todo proceso de abstracción aparecen dos aspectos complementarios:
i. destacar los aspectos relevantes del objeto.
ii. ignorar aspectos irrelevantes del mismo.
La relevancia de los detalles (información) depende del nivel de abstracción considerado, ya
que si se pasa a niveles más concretos, es posible que ciertos aspectos pasen a ser relevantes.
Se puede decir que la abstracción permite estudiar los fenómenos complejos siguiendo un
método jerárquico, es decir, por sucesivos niveles de detalle.
La abstracción implica reducción de información y, por tanto, simplificación del problema
tratado.
En el aspecto concreto que nos interesa, la programación, hay que tener en cuenta que los
programas son entidades complejas que pueden estar compuestos por miles de instrucciones,
cada una de las cuales puede dar lugar a un error del programa y que, por lo tanto, necesitan
mecanismos de definición que eviten, en la medida de lo posible, que el programador cometa
errores. Así, por ejemplo, los lenguajes de programación de alto nivel permiten al
programador abstraerse de la gran cantidad de detalles que es necesario controlar al
así como las operaciones básicas sobre dicho conjunto, es decir, definen cómo se representa la
información y cómo se interpreta.
Los tipos de datos pueden variar de un lenguaje de programación a otro, tanto los tipos
simples como los mecanismos para crear tipos compuestos. Los tipos de datos constituyen un
primer nivel de abstracción, ya que no se tiene en cuenta cómo se representa realmente la
información sobre la memoria de la máquina, ni cómo se manipula. Para el usuario el proceso
de representación es invisible. El programador no manipula directamente las cadenas de bits
que constituyen los datos, sino que hace uso de las operaciones previstas para cada tipo de
datos. Por ejemplo, el tipo simple ENTERO define un conjunto de valores enteros
comprendidos en un determinado intervalo, para los que están definidas las operaciones
suma, resta, multiplicación, división entera, asignación, etc. Para el programador es imposible
manipular un dato entero si no es a través de las operaciones definidas para ese tipo de datos,
cualquier otro proceso de manipulación está prohibido por el lenguaje. De esta manera, al
escribirse los programas independientemente de la representación última de los datos en la
memoria, si cambiase, por ejemplo, la forma de representar la información en los
ordenadores, los programas escritos en lenguajes de alto nivel sólo necesitarían ser
recompilados para ejecutarse correctamente en las nuevas máquinas.
La memoria del ordenador es una estructura unidimensional (secuencia de elementos)
formada por celdas iguales que pueden almacenar números binarios con un número fijo de
cifras (bits). Cada tipo de datos tiene asociada una función de transformación que permite
pasar los datos del formato en que se manejan en un programa al formato de la memoria y
viceversa. De manera que cambiar la representación en memoria de los datos sólo implica
modificar la función de transformación, el programador no ve afectado para nada su trabajo,
ya que se encuentra en un nivel superior de abstracción.
Los tipos de datos que un programador utiliza en un lenguaje de alto nivel suelen ser de dos
tipos: predefinidos en el lenguaje y definidos por el usuario. Esta última posibilidad contribuye
a elevar el nivel del lenguaje, pues permite definir tipos de datos más próximos al problema
que se desea resolver. Para ello, el lenguaje suministra constructores genéricos de tipos
mediante los cuales el programador puede definir tipos concretos. Sin embargo, en los
lenguajes de alto nivel más tradicionales (procedimentales y no orientados a objetos), al
programador no se le permite definir cuáles son las operaciones permitidas para los nuevos
tipos de datos. En general, los lenguajes suministran unas operaciones predefinidas muy
genéricas que, en la mayoría de los casos, no resultan las más apropiadas para el nuevo tipo.
Supóngase, por ejemplo, la definición de un tipo de datos para manipular fechas del
calendario, una posible definición en C/C++ sería:
struct fecha { int dia; int mes; int año; };
Con esta declaración se intenta indicar que una fecha es un dato compuesto, formado por tres
elementos: día, mes y año, y todos ellos son números enteros. Sin embargo, esto último no es
del todo cierto, ya que el rango de variación de, al menos, dos de los tres elementos es mucho
más restringido. El concepto día sólo puede variar en el rango 1..31 y el concepto mes sólo lo
puede hacer en el rango 1..12.
Una vez definido el tipo fecha, ya se pueden declara en el programa variables de ese nuevo
tipo:
fecha f1, f2;
También se pueden definir subprogramas que operen sobre valores de este tipo y que, de
alguna manera, representarían los operadores válidos sobre esos datos. Por ejemplo, en una
aplicación se puede considerar adecuado manejar datos del tipo fecha mediante las siguientes
operaciones (procedimientos o funciones):
Asignar un valor (día, mes y año) válido a una fecha:
void asignarFecha (fecha f, int d, int m, int a);
Incrementar en un día una fecha:
void incrementarFecha (fecha f);
Decrementar en un día una fecha:
void decrementarFecha (fecha f);
Comprobar si una fecha es menor (anterior) que otra:
bool menorFecha (fecha f, fecha g);
Visualizar los datos de una fecha:
void visualizarFecha (fecha f);
Todas estas operaciones pueden incluir las comprobaciones pertinentes para verificar que
todas las fechas manejadas sean válidas (meses de 30 o 31 días, años bisiestos, etc). Sin
embargo, no se puede impedir que se generen, mediante otros operadores, valores que no
tengan sentido. Por ejemplo, hacer:
f1.dia = 30; f1.mes = 2; /* ¡¡¡día 30 de febrero!!! */
Como se puede observar, la declaración incluye tanto la especificación de los datos necesarios
para representar una fecha (día, mes y año), como las operaciones que permiten su
manipulación.
En la declaración de la clase hay que destacar el significado de las palabras reservadas public
y private. La finalidad de estos elementos del lenguaje es especificar el nivel de visibilidad de
los componentes definidos. La palabra public permite especificar aquellos componentes
(datos y/o operaciones) que está permitido usar “fuera” de la clase, es decir, por cualquier
programa o función que declare fechas y desee manipularlas de alguna manera. Por el
contrario, la palabra private especifica aquellos componentes (también datos y/o
operaciones) que se desea que estén protegidos por el lenguaje, de manera que se impide su
acceso desde “fuera” de la clase. Sólo las operaciones definidas en la clase pueden tener
acceso a estos elementos.
Por lo tanto, una declaración de clase no sólo permite definir conjuntamente datos y
operaciones de manipulación, sino también establecer el nivel de visibilidad (protección) de
estos elementos en un programa.
EJEMPLO:
El siguiente programa hace uso de la clase fecha anteriormente declarada:
int main () { fecha f; // f.dia = 3; //2, error f.mes = 5; //3, error f.anyo = 2100; //4, error f.visualizarFecha (); // f.asignarFecha(4,10,2020); // f.visualizarFecha (); // return 0; }
Lo primero que hay que destacar es que un dato de tipo fecha, f, se declara de igual forma
que una variable estándar (línea 1). No obstante, en lugar de variables, estos elementos
reciben el nombre de objetos ( objeto = dato perteneciente a una clase). Por su lado, el acceso
a los elementos contenidos en el interior de un objeto, que vienen dados por la declaración de
la clase a la que pertenece, se realiza mediante la sintaxis típica de una estructura en C++
(struct). En la forma: objeto.componente, como se puede ver en las líneas 2-7. Las
operaciones definidas en una clase siempre deben de ser invocadas a través de un objeto. Por
ejemplo, la sentencia:
visualizarFecha ();
no es válida. Puesto, que está declarada en el interior de la clase fecha, debe de ser invocada
asociada con un objeto de dicha clase (¿qué fecha se debe visualizar?):
f.visualizarFecha ();
En realidad, se le está pidiendo al objeto f que ejecute dicha operación. De ahí que los objetos
sean considerados algo más que variables. Las variables se limitan a almacenar información de
manera pasiva, sin embargo, los objetos son elementos capaces de realizar acciones y para
ello, adicionalmente almacenan información.
No todas las sentencias del programa de ejemplo son correctas. Así, las líneas 2, 3 y 4 darían
lugar a errores de compilación, puesto que se está intentando acceder a elementos que han
sido declarados como privados en la clase. Este es un mecanismo de protección frente al uso
incorrecto de la información. Por su parte, las líneas 5, 6 y 7 muestran cual debería ser la
forma correcta de acceder a las fechas, a través de los elementos públicos de la clase.
Como regla general, se establece que: (i) todos los datos (atributos) declarados en el interior
de una clase deben de ser privados a la misma, (ii) deben de ser declaradas como públicas
aquellas operaciones a través de las cuales se permita manipular (correctamente) los objetos
que se declaren de esta clase.
La descripción de las clases permite observar claramente los dos niveles que existen en la
manipulación de datos en un programa. Por un lado, el proceso de diseño y construcción de
los datos y por otro lado, la utilización de estos datos en los programas para describir
soluciones a problemas. Durante el primero de los procesos es necesario conocer y tener
acceso a todos los componentes declarados en la clase (tanto públicos como privados). Sin
embargo, para el uso de objetos de la clase sólo es necesario conocer la especificación de la
misma y la forma en que se puede manipular (elementos públicos). Lo mismo ocurre con los
objetos del mundo real. Un teléfono móvil, una vez construido sólo puede ser manipulado a
través de las operaciones que el fabricante ha previsto a tal efecto (botones, menús, etc).
- TIPOS ABSTRACTOS DE DATOS
Los tipos de datos se pueden presentar con distinto nivel de detalle (abstracción), desde la
especificación formal (tipos abstractos de datos) hasta el nivel de implementación en un
lenguaje de programación concreto. De igual manera que un algoritmo, que describe la
secuencia de operaciones a realizar para resolver un problema, se puede expresar desde un
nivel esquemático mediante diagramas de flujo antes de concretarlo en forma de función en
C++.
El nivel más alto de abstracción en la representación de los tipos de datos lo constituye la
especificación formal de los mismos, dando lugar a los denominados tipos abstractos de datos.
Éstos se pueden ver como modelos matemáticos sobre los que se definen una serie de
operaciones. El tipo abstracto de datos es independiente del lenguaje de programación, ya que
un tipo abstracto de datos (en adelante TAD) es una especificación que no incluye ningún
detalle sobre la forma de representación.
Existen múltiples formas de implantar un mismo TAD, pero todos los TAD se deben
implementar utilizando otros tipos de datos (predefinidos o no en el lenguaje de
programación).
Las estructuras de datos son tipos de datos orientados al almacenamiento de información
(contenedores) que son habitualmente utilizados como soporte para construir otros tipos de
datos y que, por tanto, son fundamentales en el proceso de programación. Son tipos de
aplicación generalizada a un amplio espectro de aplicaciones informáticas.
- EJEMPLO DE CONSTRUCCIÓN DE TIPOS CON CLASES EN C++
En esta sección se muestra un ejemplo completo de construcción de un tipo de datos
utilizando clases en el lenguaje de programación C++.
Se desea construir un tipo de datos fecha a partir de la siguiente especificación del tipo
abstracto de datos:
TAD FECHA
Dominio:
Conjunto de fechas válidas especificadas por tres valores enteros que identifican el día, mes y
año.
Operaciones:
asignarFecha (Entero, Entero, Entero ) Fecha incrementarFecha (Fecha) Fecha decrementarFecha (Fecha) Fecha menorFecha (Fecha, Fecha) Lógico visualizarFecha (Fecha)
Axiomas:
La especificación de los axiomas de un tipo se suele hacer, por precisión, siguiendo una
notación algebraica. Sin embargo, por simplicidad, ahora se va a realizar una descripción en
lenguaje natural de las propiedades que debe cumplir cada operación.
Sea d,m,a Є Entero:
asignarFecha (d, m, a), debe generar una fecha válida cuyo día sea d, su mes
sea m y su año sea a.
Sea f Є Fecha:
incrementarFecha (f), debe generar la fecha correspondiente a un día posterior a
f.
Sea f Є Fecha:
decrementarFecha (f), debe generar la fecha correspondiente a un día anterior a f.
Sea f,g Є Fecha:
menorFecha (f, g), devolverá cierto si f corresponde a una fecha anterior a g y falso
en caso contrario.
Sea f Є Fecha:
visualizarFecha (f), debe imprimir en pantalla los datos de día, mes y año
correspondientes a la fecha f.
Como se puede observar esta descripción del TAD Fecha no incluye ningún detalle sobre la
forma en que se debe implementar el tipo (datos a utilizar o algoritmos a emplear para
implementar las operaciones). Una posible implementación vendría dada por la siguiente clase
en C++:
Archivo “fecha.h”: Interfaz de la clase (sólo declaración de prototipos)
class Fecha { public: //Operaciones descritas en el TAD void asignarFecha (int d, int m, int a); void incrementarFecha (); void decrementarFecha (); bool menorFecha (Fecha f); void visualizarFecha (); private: //Detalles de implementación //Datos a emplear para la representación int dia; int mes; int anyo; //Funciones auxiliares para implementar //las operaciones públicas bool fechaValida (int d, int m, int a); int diasMes (int m, int a); bool bisiesto (int a); };
bool Fecha::bisiesto (int a) //Método privado que indica si un año es bisiesto { bool b; if ((a % 400) == 0) b = true; else if ((a % 100) == 0) b = false; else if ((a % 4) == 0) b = true; else b = false; return (b); } void Fecha::asignarFecha (int d, int m, int a) //Asigna un día, mes y año a la fecha { //Como especifica el TAD, sólo asigna fechas válidas if (fechaValida (d, m, a)) { dia = d; mes = m; anyo = a; } else cerr << “Error: fecha no valida.” << ende; }
void Fecha::incrementarFecha () //Incrementa en un día la fecha { //En días intermedios del mes sólo es preciso //incrementar el valor de dia if (fechaValida (dia+1, mes, anyo)) dia = dia + 1; else //Si estamos en el último día del mes //es necesario pasar al día 1 del mes siguiente if (fechaValida (1, mes+1, anyo)) { dia = 1; mes = mes + 1; } else //Estamos en el 31 de diciembre y es preciso //pasar al 1 de enero del siguiente año { dia = 1; mes = 1; anyo = anyo + 1; } }
void Fecha::visualizarFecha () //Muestra por pantalla la fecha { string nomMes; switch (mes) { case 1: nomMes = “Enero”; break; case 2: nomMes = “Febrero”; break; case 3: nomMes = “Marzo”; break; case 4: nomMes = “Abril”; break; case 5: nomMes = “Mayo”; break; case 6: nomMes = “Junio”; break; case 7: nomMes = “Julio”; break; case 8: nomMes = “Agosto”; break; case 9: nomMes = “Septiembre”; break; case 10: nomMes = “Octubre”; break; case 11: nomMes = “Noviembre”; break; case 12: nomMes = “Diciembre”; } cout << dia << “ de “; cout << nomMes << “ de “; cout << anyo; } Modifica do: 18/ 02/
Cuestionario de Autoevaluación
1. Un tipo abstracto de datos especifica (marcar la respuesta correcta):
(a) La representación en memoria de la información
(b) La representación en memoria de la información y las operaciones asociadas
(c) Las operaciones válidas para un tipo de datos y sus propiedades
(d) La declaración de tipos en un lenguaje de programación
2. Señala cuál de los siguientes aspectos NO aparece en la especificación de un tipo abstracto de
datos (TAD) (marcar la respuesta correcta):
a) Cuáles son las operaciones válidas del tipo
b) Cuál debe de ser la implementación de las operaciones del tipo
c) Cuál es el conjunto de valores sobre el que se define el tipo
d) Cuáles son las propiedades que cumplen las operaciones del tipo
3. Dada la siguiente la especificación del TAD imagen, donde se indica el Dominio, las Operaciones
y una descripción en lenguaje natural de los Axiomas. Se pide implementar la interfaz de una clase
en C++ para este TAD.
Dominio: Una imagen en niveles de gris es una matriz de números enteros en el rango 0..255, de
un determinado tamaño (especificado por los atributos, número de filas nfilas y número de
columnas ncols), en la que cada elemento almacena el nivel de gris de un punto.
Operaciones : Las operaciones que se podrán realizar sobre una imagen son:
Imagen(entero, entero, entero) → Imagen AsignarImagen (Imagen) → Imagen NFilas() → entero NCols() → entero ObtenGris(Imagen, entero, entero) → entero PonGris(Imagen, entero, entero, entero) → Imagen
Axiomas :
Sea n, m, g, altura y anchura valores enteros, sea I una Imagen, sea f un archivo:
5. Dada la clase declarada en el ejercicio 4 se pide:
a) Declarar un objeto v de la clase VectorEnteros.
b) Declarar un array lista de la 5 objetos de la clase VectorEnteros.
c) Poner a cero todos los elementos del objeto v.
d) Escribir en pantalla el número de elementos de los 3 objetos del array lista.
e) Guardar el valor 5 en la posición 10 del objeto v.
f) Buscar el valor 15 en el primer elemento del array lista, sino lo encuentra buscarlo en el
segundo y si tampoco lo encuentra buscarlo en el tercero.