Está en la página 1de 23

Tema 1.

TIPOS ABSTRACTOS DE DATOS


1.1 1.2 1.3 1.4 1.5 Introduccin a la resolucin de problemas Concepto general de abstraccin Tipos abstractos de datos 1.3.1 Concepto y terminologa 1.3.2 Clasificacin de Tipos Abstractos de Datos Especificacin de Tipos Abstractos de Datos 1.4.1 Especificaciones informales 1.4.2 Especificaciones formales Tcnicas de implementacin de TADs 1.5.1 Modelo lgico de memoria 1.5.2 Implementaciones estticas y dinmicas 1.5.3 Representaciones contiguas y enlazadas 1.5.4 Estilos imperativo, funcional y orientado a objetos 1.5.5 Ejemplos de implementaciones

1.1 Introduccin a la resolucin de problemas


La resolucin de un problema comprende desde la descripcin del problema hasta el desarrollo del programa que lo resuelva automticamente. Los pasos a dar seran: 1. Especificacin y anlisis del problema. 2. Diseo de solucin conceptual o algoritmo. 3. Implementacin o desarrollo de dicha solucin (codificacin en un lenguaje de programacin). 4. Validacin. 5. Mantenimiento.
FASE DE RESOLUCIN DEL PROBLEMA
Anlisis.- Comprender y definir el problema. Solucin General (ALGORITMO).Desarrollar una secuencia lgica de pasos que ser utilizada para resolver el problema. Prueba.- Seguimiento exacto de los pasos establecidos para ver si la solucin resuelve realmente el problema. Solucin Especfica (PROGRAMA).- Traducir el algoritmo a un lenguaje de programacin (cdigo). Prueba.- Hacer que la computadora siga las instrucciones. Comprobar los resultados y hacer las correcciones oportunas hasta que las respuestas sean correctas. Uso.- Utilizar el programa.

FASE DE IMPLEMENTACIN

El ALGORITMO es precisamente un conjunto ordenado de pasos que especifican la secuencia de operaciones que se han de realizar para resolver un problema, y todas las acciones concretas a realizar con independencia de los datos a procesar. Las caractersticas fundamentales que debe cumplir todo algoritmo son:

Debe ser preciso e indicar el orden de realizacin de cada paso. Debe estar definido (si se repiten varias veces los mismos pasos partiendo de las mismas condiciones iniciales se debe obtener siempre el mismo resultado). Debe ser finito (debe tener un nmero finito de pasos). Debe ser independiente del lenguaje de programacin que se utilice. Debe contemplar todas las posibilidades. Debe ser fcil de leer e implementar.

La resolucin concreta de un problema implica el estudio de la informacin a manejar, determinando la mejor forma de almacenamiento, y la observacin de qu operaciones hay que realizar sobre ella para obtener los resultados deseados. Por tanto, como definiremos ms adelante, la entidad fundamental a considerar en la resolucin de problemas ser el Tipo abstracto

de datos.
Para disear la solucin ms apropiada al problema planteado, el ingeniero del software cuenta con: Diseo descendente: se descompone el problema en otros ms sencillos mediante diferentes fases de refinamiento, obtenindose abstracciones procedimentales. Abstraccin procedimental: indica el propsito del proceso de forma independiente a su implementacin. La modularidad y la abstraccin procedimental son complementarias, pudiendo ocurrir que se deba cambiar el algoritmo de un mdulo, sin que esto afecte al resto de la solucin planteada. Modularidad: consiste en resolver de forma independiente los subproblemas resultantes de un diseo descendente. Completa el diseo descendente como mtodo de resolucin de problemas, permitiendo proteger la estructura de la informacin asociada a un subproblema. Abstraccin de datos: se basa en el conjunto de valores que pueden tomar los datos, as como en las operaciones que se pueden realizar sobre ellos. Los mdulos que contengan la implementacin tendrn una parte vista o pblica y otra parte privada u oculta. El usuario final slo debe conocer la parte pblica o de acceso al mdulo, sin preocuparse de los detalles de la implementacin.

1.2 Concepto general de abstraccin


La abstraccin es una simplificacin de un objeto o de un proceso de la realidad, resaltando sus propiedades o caractersticas ms importantes. El concepto de abstraccin en programacin puede estudiarse desde dos puntos de vista: 1. Abstraccin de datos. 2. Abstraccin procedimental o funcional.

PROGRAMACIN II- 1 GEI

Las abstracciones funcionales son procesos que realizan una determinada accin, que tienen parmetros para recibir la informacin a procesar y a travs de los que se obtienen los resultados de salida. Lo ms importante es realmente qu hacen sin preocuparnos de cmo lo hacen ni en qu tiempo (Ejemplo: funciones sqrt, cos,...). Por tanto, representan las operaciones ms significativas del problema, operaciones que no estn implementadas directamente en el lenguaje que se est utilizando. Normalmente existe una relacin casi directa entre las abstracciones funcionales obtenidas en el diseo descendente (subproblemas) y los subprogramas. Las abstracciones de datos nos facilitarn definir nuevos tipos de datos, especificando sus posibles valores y las operaciones que trabajen sobre ellos. La forma de acceder a los valores de dichos datos ser solamente utilizando las operaciones definidas sobre dicho tipo abstracto de datos, sin preocuparnos de cmo son representados y tratados por el ordenador.

1.3 Tipos abstractos de datos


1.3.1 Concepto y terminologa Para llegar a entender bien el concepto de tipo de datos abstracto, comenzaremos poniendo de relieve otros conceptos que estn muy relacionados, como son los conceptos de tipo de datos y de estructura de datos. En un lenguaje de programacin, el tipo de datos de una variable es el conjunto de valores que sta puede tomar, y lleva asociado un conjunto de operaciones que permiten manipular dichos valores. Los tipos de datos bsicos varan de un lenguaje a otro, siendo los ms usuales los enteros, reales, booleanos y caracteres. La representacin computacional concreta utilizada para los tipos de datos incorporados por el lenguaje es invisible al programador, al que slo se le permiten usar las operaciones previstas para cada tipo. Los tipos de datos estructurados son un paso ms all, pues introducen la idea de genericidad. El lenguaje suministra unos constructores genricos de tipos (array, record,) que el programador ha de completar, sustituyendo los tipos formales del mismo por tipos concretos. Para las estructuras de datos, el lenguaje proporciona tambin operaciones predefinidas para manipular los valores de los nuevos tipos definidos. Sin embargo, la propiedad de genericidad conseguida con las estructuras de datos puede resultar, en muchas ocasiones, un inconveniente en lugar de una ventaja. La razn es que de esta forma no se permite definir tipos, sino ms bien representar unos tipos por otros. Para conseguir tal genericidad, las operaciones que ofrecen los lenguajes de programacin para manipular los valores de los nuevos tipos deben ser tan genricas que no pueden impedir la creacin de valores sin semntica. Por ejemplo, si representamos un nmero racional mediante un registro con dos
ESTRUCTURAS DE DATOS- 1 ETIS 3

campos (numerador y denominador), el usuario del tipo puede generar libremente un valor cuyo denominador sea 0 y, por tanto, estara indefinido. Otro ejemplo es el tipo fecha. Si representamos el tipo fecha mediante un registro con los campos de da, mes y ao, no se puede impedir que se generen valores sin significado como, por ejemplo, el da 30 de febrero, o que se realicen operaciones sobre fechas que no tengan sentido. El concepto de tipo abstracto de datos (TAD), propuesto hacia 1974 por John Guttag y colaboradores, vino a clarificar esta situacin. Un TAD es una coleccin de valores junto con unas operaciones definidas sobre ellos. Las operaciones se definen mediante una

especificacin que es independiente de cualquier representacin. El acceso a los valores queda


limitado al uso de las operaciones que lo manejan (no podemos utilizarlos si no es a travs de estas operaciones). El calificativo abstracto expresa precisamente esta cualidad de independencia de la representacin. Para definir un nuevo tipo, el programador deber comenzar por decidir qu operaciones le parecen relevantes y tiles para operar con los valores pertenecientes al mismo. Es decir, deber comenzar por establecer la interfaz que van a tener los usuarios con dicho tipo. Si estas son las nicas operaciones permitidas al usuario para crear valores del nuevo tipo, resulta imposible para stos construir valores no vlidos del tipo. As, las operaciones slo permitirn crear valores correctos, produciendo un error cuando los datos de entrada sean incorrectos. El usuario del tipo debe estar informado, a travs de una especificacin, de las situaciones de error. Una vez establecida la interfaz con los usuarios del nuevo tipo, el programador es libre de escoger la representacin que ms le convenga. La definicin de la representacin y de las operaciones deber realizarse en un mbito de declaracin inaccesible al resto del programa. Los usuarios del tipo slo conocern el nombre del tipo (lo que les permite declarar variables) y la especificacin de operaciones (lo que les permite manipular las variables declaradas). Si el programador que defini el TAD decidiera posteriormente cambiar la representacin por otra, slo tendra que cambiar el mdulo que contiene la definicin de la representacin y de las operaciones. El resto del programa, una vez recompilado, se vera inalterado en su funcionamiento por el cambio de representacin. De este modo, el tratamiento dado por el lenguaje a los tipos definidos por el programador sera equivalente al que da a sus propios tipos (tipos opacos), y se resume en los siguientes aspectos:

Privacidad de la representacin: los usuarios no conocen la representacin de los


valores en la memoria del computador.

Proteccin: slo se pueden utilizar para el nuevo tipo las operaciones previstas por la
especificacin.

Es muy importante indicar que el conjunto de operaciones de un TAD ha de permitir generar cualquier valor del tipo, ya que los usuarios no tienen otro modo de crearlos.
4 PROGRAMACIN II- 1 GEI

El programador de un TAD ha de crear dos piezas de documentacin bien diferenciadas: La especificacin del TAD. Es lo nico que conoce el usuario del TAD y consiste en el nombre del tipo y la especificacin de las operaciones. Esta especificacin tendr una parte sintctica (nombre de cada operacin y tipos de los parmetros y resultados), y otra semntica para la cual, como veremos, existen distintos modos de llevarla a cabo (informal o formalmente). La implementacin del TAD. Conocida slo por el programador del TAD, se realiza bajo un lenguaje de programacin concreto, y consiste en la representacin del tipo por medio de otros tipos (tipos de datos simples predefinidos o definidos por el programador, estructuras de datos o, a su vez, TADs), y en la realizacin de las operaciones en trminos de dicha representacin. Finalmente, resaltar que un TAD representa una abstraccin en el sentido siguiente: Se destacan los detalles (normalmente pocos) del comportamiento observable del tipo. Es de esperar que este aspecto sea bastante estable durante la vida til del programa. Se ocultan los detalles (probablemente numerosos) de la implementacin. Este aspecto es, adems, ms propenso a cambios. Estas propiedades hacen que el TAD sea el concepto ideal alrededor del cual basar la descomposicin en mdulos de un programa grande y, por tanto, la base del diseo modular. Un tipo abstracto de datos (TAD) se define como un modelo matemtico de los objetos de datos junto con las operaciones que manejan estos objetos de datos, sin tener en cuenta su implementacin.

Un tipo de datos es la representacin concreta o implementacin del modelo de datos especificado en el TAD.

Una estructura de datos define una coleccin de variables en memoria con algn tipo de relacin. 1.3.2 Clasificacin de Tipos Abstractos de Datos A la hora de tratar con TADs nos parece conveniente hacer una primera clasificacin en TADs

simples y TADs contenedores. Mientras que los casos de un TAD simple solamente cambian su
valor pero nunca su estructura (como consecuencia de esto el espacio de almacenamiento que ocupan permanece constante), los casos de un TAD contenedor se caracterizan por su cambio de valor y de estructura. Un caso de un TAD contenedor es una coleccin de un nmero variable de elementos agrupados entre s mediante alguna estructura, por lo que dispone de una serie de
ESTRUCTURAS DE DATOS- 1 ETIS 5

operaciones asociadas para la seleccin de componentes y operaciones sobre la estructura en su conjunto (adicin y extraccin de componentes, y creacin y eliminacin de estructuras). La Tabla 1.1 muestra algunos ejemplos tpicos de TADs simples y contenedores.
TADs simples TADs contenedores entero, real, carcter, booleano, enumerado, subrango lista, cola, pila, rbol, grafo, conjunto

Tabla 1.1. Ejemplos de TADs simples y contenedores.

Por otro lado, y de acuerdo a Liscov y Guttag (1986), un TAD se dice que es inmutable cuando los 'objetos' (casos) que pertenecen a l no pueden modificarse (se crean y se destruyen, pero no existen operaciones para modificarlo); en otro caso, se dice que es mutable. A veces, tenemos TADs inmutables con representacin mutable. Por ejemplo, dos casos del TAD Racional tales como y 2/4 pueden ser representados de forma unificada, permaneciendo invisible de cara al usuario esta mutabilidad en la representacin. Otro ejemplo de TAD inmutable con representacin mutable puede ser el TAD polinomio (los polinomios 2x y x + x pueden ser representados de igual forma).

1.4 Especificacin de Tipos Abstractos de Datos


Hemos dicho que un TAD es una coleccin de valores junto con una serie de operaciones definidas sobre ellos, y en el cual se separa lo que es el modelo de datos y sus operaciones de la representacin elegida para el modelo y la implementacin de las operaciones.

Figura 1.1. Especificacin e implementacin de un TAD.

Esto nos permite olvidarnos de los detalles de la implementacin, pero debemos conocer cmo se usa el TAD, por lo que necesitamos su especificacin. La especificacin describe el comportamiento del TAD, incluyendo los valores vlidos y las operaciones definidas sobre una parte de estos valores vlidos, pero no describe cmo est realizado, es decir, no se tiene ninguna consideracin de implementacin. La definicin de un TAD nos viene dada, por tanto, mediante su especificacin. Distinguiremos entre especificaciones informales y especificaciones formales. La especificacin de un TAD tiene un doble destinatario, el usuario del TAD y el implementador del TAD, los cuales pueden seguir su trabajo por separado una vez establecida sta. Para que esto pueda ser posible, una especificacin
6 PROGRAMACIN II- 1 GEI

debe ser lo suficientemente precisa pero, a su vez, breve. Las especificaciones formales renen estas dos caractersticas, permitiendo la verificacin formal de los programas usuarios del TAD, as como de los que implementan el TAD. Las especificaciones informales, que no suelen reunir estas caractersticas, pueden ser, no obstante, muy informativas y se pueden escribir de forma que sus destinatarios no tengan ningn inconveniente en entender su significado. 1.4.1 Especificaciones informales Como modelo de especificacin informal asumiremos el propuesto por Liskov y Guttag (1986): 1. Cabecera: Aparece el nombre de las operaciones. 2. Descripcin: Se describe de forma general en qu consiste la abstraccin, sin decir nada acerca de la implementacin. Los casos del TAD pueden describirse en trminos de otros tipos para los cuales se espera que el lector de la especificacin est ms familiarizado. Se pueden utilizar grficos y abstracciones matemticas. Se puede incluir en la descripcin si el TAD es mutable o inmutable. 3. Especificacin de las operaciones: Para la especificacin de una abstraccin operacional seguiremos el siguiente modelo:
nombre de la operacin (entrada) devuelve (salida)

requerimientos: Esta clusula muestra las restricciones de uso. modifica: Esta clusula identifica las entradas que van a ser modificadas. efecto: Esta clusula define el comportamiento. Observamos los siguientes componentes: (a) Cabecera: Es la informacin sintctica. Se indica el nombre de la operacin y el nmero, orden y tipos de sus entradas y salidas. Deben darse nombres para las entradas y pueden darse para las salidas. (b) Cuerpo: Es la informacin semntica. Consta de las siguientes clusulas:

Requerimientos: Restringen el dominio del procedimiento o funcin. Cuando


introducimos requerimientos, obtenemos una abstraccin operacional parcial (en caso contrario se dice que la abstraccin es total). El que use la abstraccin es el responsable de que los requerimientos se cumplan; si stos no se cumplen, los resultados pueden ser impredecibles. Si la abstraccin es total, la clusula de requerimientos puede omitirse. Se supone como requerimiento implcito (y, por tanto, no tiene que ser explicitado en la clusula de requerimientos) que las entradas que figuran en la lista de parmetros de la abstraccin han sido correctamente construidas mediante algn constructor del tipo.

Modifica: Indica los argumentos de entrada que cambian de valor tras una llamada
a la abstraccin operacional.

ESTRUCTURAS DE DATOS- 1 ETIS

Efecto: Se indica el efecto que se produce al ejecutar la operacin para las entradas
que cumplen los requerimientos. Debe definir qu salidas son producidas y tambin qu modificaciones son hechas en la lista de entradas de la clusula modifica. La clusula efecto se escribe bajo la asuncin de que se satisface la clusula

requerimientos, y no se dice nada sobre el efecto de la abstraccin cuando dicha


clusula no se satisface. Ejemplo: El TAD Racional A continuacin mostramos un ejemplo de especificacin informal para el TAD Racional de acuerdo al modelo de especificacin informal descrito anteriormente. Aunque podramos considerar otras posibles formas para definir el TAD Racional, est claro que es un TAD simple, y lo definiremos como un TAD inmutable, por lo que no incluiremos operaciones de modificacin de los casos del TAD. CABECERA:
racional = tipo de datos es crea, simplifica. num, den, suma, resta, multiplica, divide,

DESCRIPCIN: Los valores del TAD racional son nmeros racionales. El TAD racional es inmutable. OPERACIONES (cabecera y cuerpo):
crea(a, b: entero) devuelve (racional) requerimientos: b0.

efecto: Devuelve un nmero racional cuyo numerador es a y cuyo denominador es b. efecto: Devuelve el numerador del nmero racional a. efecto: Devuelve el denominador del nmero racional a. efecto: Devuelve un nmero racional que es la suma de los nmeros racionales a y b. efecto: Devuelve un nmero racional que es la resta de los nmeros racionales a y b. efecto: Devuelve un nmero racional multiplicacin de los nmeros racionales a y b.

num(a: racional) devuelve (entero) den(a: racional) devuelve (entero) suma(a, b: racional) devuelve (racional) resta(a, b: racional) devuelve (racional) multiplica(a, b: racional) devuelve (racional) divide(a, b: racional) devuelve (racional) requerimientos: num(b)0.

efecto: Devuelve un nmero racional que es la divisin de los nmeros racionales a y b. efecto: Devuelve un nmero racional que es la simplificacin del nmero racional a.

simplifica(a: racional) devuelve (racional)

PROGRAMACIN II- 1 GEI

1.4.2 Especificaciones formales Utilizaremos para la escritura de especificaciones formales el siguiente esquema:

Tipo: Nombre del TAD. Sintaxis: Forma de las operaciones. Semntica: Significado de las operaciones.

Para describir la sintaxis de las operaciones asumimos un esquema funcional, suministrando un nombre de funcin para cada operacin e indicando el tipo de los argumentos y el del resultado:
nombre de la funcin (tipo de los argumentos) tipo del resultado

Aunque en la parte de sintaxis de una especificacin formal todas las operaciones del TAD se describen como funciones, esto no obliga a que en la implementacin del TAD todas las operaciones sean funciones, sino que pueden definirse de forma similar como procedimientos, de acuerdo al estilo de programacin adoptado por el implementador o a las restricciones del lenguaje de programacin. El apartado de semntica nos indica el comportamiento de las funciones definidas sobre el TAD. La semntica consiste en un conjunto de reglas de tipo algebraico de la forma:
nombre de la funcin (valores particulares) expresin del resultado

Debemos tener en cuenta los siguientes aspectos: No se definen reglas semnticas (se consideran axiomas) para ciertas funciones, como son algunas funciones constructoras. La expresin del resultado puede ser recursiva, conteniendo referencias a la misma funcin o a otras del TAD. Las expresiones pueden contener referencias a otros tipos que consideramos predefinidos. En particular es importante considerar como predefinido el tipo booleano, con los valores cierto y falso, o el valor predefinido error, para indicar los valores de los argumentos en los que ciertas funciones parciales no estn definidas. Cualquier implementacin del TAD deber cumplir las condiciones impuestas por la semntica. Las reglas han de intentar aplicarse en el orden indicado para la verificacin formal de programas. Para facilitar la escritura de las expresiones en la parte de semntica, se permite emplear expresiones condicionales, que adoptan la forma:
si condicin valor si es cierto valor si es falso

La condicin ser una expresin que toma un valor booleano. Se considera como predefinida la comparacin de igualdad entre valores del mismo tipo, escrita como valor1 = valor2.

ESTRUCTURAS DE DATOS- 1 ETIS

Otra ampliacin de la notacin es permitir la definicin de TADs genricos, que se expresan en base a otro u otros tipos sin especificar exactamente cules son. Ejemplo: El TAD Bolsa A continuacin mostramos como ejemplo la especificacin formal del TAD Bolsa de acuerdo al modelo de especificacin formal descrito anteriormente. Podemos definir el TAD Bolsa (coleccin de elementos, no ordenada, con repeticin) mediante la especificacin:

Tipo: bolsa (elemento) Sintaxis:


bolsavacia bolsa poner(bolsa, elemento) bolsa esvacia(bolsa) booleano cuantos(bolsa, elemento) natural

Semntica: bbolsa, e,felemento:


esvacia(bolsavacia) cierto esvacia(poner(b,e)) falso cuantos(bolsavacia,e) cero cuantos(poner(b,f),e)si f=esucesor(cuantos(b,e)) | cuantos(b,e)

La definicin del TAD Bolsa se apoya en el tipo predefinido booleano y en el tipo natural que asumimos que se han definido formalmente con constructores cero y sucesor. Los constructores de los casos del TAD Bolsa son bolsavacia y poner. En el ejemplo anterior las funciones introducidas son todas ellas totales, es decir, estn definidas para todos los valores de sus argumentos. Cuando las funciones a especificar son parciales se debern incluir reglas semnticas que indiquen que para ciertos valores de los argumentos la funcin tomar el valor predefinido error (por ejemplo, si queremos incluir la funcin predecesor(natural) natural en la definicin del tipo natural, habr que aadir, entre otras, la regla predecesor(cero)error). En la siguiente seccin incluimos la implementacin en C del TAD Bolsa. Finalmente, haremos los siguientes comentarios relativos a la realizacin y uso de especificaciones en general: El usuario de una abstraccin es el mximo responsable de que se cumplan los requerimientos de sta. Aunque, como hemos visto, el efecto de la abstraccin no contempla situaciones para las cuales los requerimientos no se cumplen, es, sin embargo, importante prever la posibilidad de errores de software a la hora de implementar una abstraccin operacional. Una implementacin se dice que es robusta si se autoprotege frente a valores inconsistentes de los datos. De acuerdo con las especificaciones, una llamada con argumentos que no cumplan los requerimientos se considera un error. No obstante, dicho error no debera provocar que el programa aborte, sino que debera dar lugar a un tratamiento excepcional pero controlado. Una
10 PROGRAMACIN II- 1 GEI

manera de hacer explcita la previsin de errores es aadir sistemticamente a las listas de argumentos de todas las abstracciones operacionales un parmetro de error en el que se devolver indicacin de si el resultado es normal o excepcional. Otra forma de llevar a cabo el tratamiento de errores es mediante el manejador de excepciones del que disponen algunos lenguajes de programacin. Una especificacin debe ser totalmente independiente de cualquier implementacin, incluso del lenguaje de programacin bajo el que recaer la implementacin. Por esta razn, las especificaciones se escriben en un lenguaje de especificacin, no en un lenguaje de programacin. Existe, pues, cierta informacin adicional de cara al usuario de la abstraccin que le permitir usar esta en un lenguaje de programacin concreto, como puede ser si la operacin es funcin o procedimiento, si los parmetros son por valor o por referencia o, en su caso, la informacin de los parmetros aadidos para el tratamiento de errores. Toda esta informacin adicional debe proporcionarse al usuario por el programador de la implementacin como un complemento de la especificacin; los entornos de desarrollo usualmente disponen de herramientas para la generacin automtica de este tipo de documentacin. Hemos asumido que las operaciones de destruccin de los casos de un TAD no se especifican. No obstante, si el lenguaje de programacin sobre el que recaiga la implementacin del TAD no dispone de mecanismos automticos de destruccin, podra ser necesaria la existencia de operaciones de destruccin en el conjunto de operaciones del TAD. Este es el caso del lenguaje C.

1.5 Tcnicas de implementacin de TADs


En esta seccin mostraremos algunas tcnicas generales para implementar TADs, prestando especial inters a la implementacin de TADs contenedores. Distinguiremos entre implementaciones estticas y dinmicas, dependiendo de si la asignacin de memoria se ha realizado en tiempo de compilacin o en tiempo de ejecucin, y de representaciones contiguas o enlazadas, dependiendo de si los elementos que constituyen un caso del TAD se almacenan en direcciones contiguas de la memoria del computador o se almacenan en direcciones enlazadas entre s mediante punteros. 1.5.1 Modelo lgico de memoria Para entender un poco mejor algunos conceptos, vamos a recordar cmo est organizada la memoria del ordenador. La memoria se divide en dos partes: memoria principal y almacenamiento secundario (unidades de disco y cinta).
ESTRUCTURAS DE DATOS- 1 ETIS 11

La memoria principal tiene acceso directo, es decir, acceso a cualquier posicin empleando un tiempo bastante menor que el requerido para el acceso a la memoria secundaria. El modelo de memoria principal que veremos nos ayudar a entender cmo se realiza la gestin del espacio de memoria disponible, es decir, cmo es asignada la memoria cuando se crean estructuras de datos y cmo se libera nuevamente dicho espacio de memoria cuando una estructura de datos no se vuelve a utilizar. Como ya se ha indicado, una estructura de datos es esttica cuando se asigna un espacio fijo de memoria antes de ejecutar el programa, en tiempo de compilacin, sin poder variarla durante la ejecucin del programa. Por el contrario, la asignacin dinmica de memoria se hace en tiempo de ejecucin del programa, en funcin de las necesidades que surjan.

Figura 1.2. Un modelo lgico de la memoria principal.

Refirindonos a la Figura 1.2, se considera la memoria como un vector o tabla unidimensional de bytes dividida en tres zonas lgicas: memoria esttica y memoria dinmica dividida en pila de ejecucin (stack) y montn (heap). Los datos que se mantienen en memoria durante toda la ejecucin estarn en la memoria esttica y se reserva en tiempo de compilacin sin variar durante el tiempo de ejecucin. Aqu estn incluidas las variables globales e instrucciones del programa. Por otro lado, para los datos de los que a priori no sepamos qu espacio de memoria necesitarn, se reservar dinmicamente la memoria en la zona de memoria dinmica montn pudiendo variar la cantidad asignada durante el tiempo de ejecucin. La pila de ejecucin (stack) aumenta hacia la zona de memoria alta y disminuye hacia la zona de memoria baja. As, cada vez que se hace una llamada a un nuevo proceso se almacenan en la pila de ejecucin todos los datos o referencias de los mismos declarados en dicho proceso. Tambin se guardar informacin necesaria para que, una vez finalizado dicho proceso, se pueda continuar la ejecucin del programa en el punto adecuado, momento en el que se borrar toda esta informacin de la pila de ejecucin devolvindose el control nuevamente al programa.
12 PROGRAMACIN II- 1 GEI

Es importante ver que las memorias dinmicas montn y pila crecen una contra la otra, pudiendo darse situaciones de error cuando hay demasiada memoria dinmica montn asignada y no devuelta, o bien muchas llamadas a procesos que aumenten en exceso la informacin almacenada en la memoria dinmica pila. 1.5.2 Implementaciones estticas y dinmicas Diremos que una implementacin es esttica si la asignacin de la memoria requerida para almacenar los valores del TAD se realiza en tiempo de compilacin. Por otro lado, se dice que una implementacin es dinmica si la asignacin de memoria se realiza en tiempo de ejecucin, mediante instrucciones del lenguaje de programacin. Las implementaciones dinmicas son por tanto ms flexibles y se adaptan mejor a las necesidades reales de la aplicacin concreta sobre las que recaen, ya que normalmente es en tiempo de ejecucin cuando se conocen los requerimientos de memoria de los casos de un TAD contenedor. Como ya comentamos, un TAD es opaco, es decir, debe cumplir las propiedades de privacidad de la representacin y de proteccin. Los lenguajes de programacin deben disponer de mecanismos para la creacin de tipos opacos, como es el caso de C. Ahora bien, para que un tipo de datos sea realmente opaco en C, este debe ser declarado como un tipo de datos puntero en el mdulo de implementacin, quedando declarado como puntero a void en el mdulo de definicin (de este modo se establece el nombre del tipo opaco). El uso de punteros da lugar a la asignacin dinmica de memoria y, por tanto, a la obtencin de implementaciones dinmicas. A continuacin describiremos algunos mecanismos usuales de asignacin dinmica de memoria. 1.5.2.1 Asignacin dinmica de memoria La asignacin dinmica de memoria, o ms concretamente, el uso de punteros, nos permite utilizar la memoria exacta que se necesita para la representacin de los valores de un tipo de dato. Esto es as porque es en tiempo de ejecucin (por medio de instrucciones del lenguaje) cuando creamos nuevas variables dinmicas y colocamos variables puntero apuntando a ellas para poder referenciarlas. De esta forma, un programa puede arrancar con poco espacio de almacenamiento (o ninguno) y crecer slo cuando sea preciso, hasta alcanzar los lmites del sistema de computacin. Cuando un elemento ya no es necesario, su espacio puede liberarse (tambin con instrucciones del lenguaje en tiempo de ejecucin) y as estaremos usando siempre el espacio necesario. En C, como en casi todos los lenguajes, existen varios mecanismos para la creacin de variables dinmicas. Una variable dinmica es una variable cuyo espacio de almacenamiento
ESTRUCTURAS DE DATOS- 1 ETIS 13

requerido se asigna en tiempo de ejecucin. Las variables dinmicas no tienen nombre, y la nica forma de acceder a ellas es por medio de los punteros. Podemos crear variables dinmicas y asignarles un puntero con la instruccin malloc. Al terminar de trabajar con la variable dinmica debemos liberarla con la instruccin free. Para poder usar estas instrucciones es necesario importar la librera <stdlib.h>. Las variables dinmicas se alojan en el montn (heap) (ver Figura 1.2). Para llevar el control de las variables dinmicas que no estn siendo utilizadas en un momento determinado de la ejecucin de un programa, internamente se mantiene una lista de direcciones disponibles (no usadas) en la memoria libre. Inicialmente, esta lista contiene todas las direcciones de la memoria libre, de modo que cada vez que se solicita crear una variable dinmica, se accede a la lista de direcciones disponibles y se asigna a la variable puntero una direccin de la lista (si la variable dinmica requiere ms de una direccin se asigna la primera de ellas a la variable puntero), eliminndose de la lista dicha direccin (o direcciones, si la variable dinmica requiere ms de una). La instruccin free produce el efecto contrario, es decir, la direccin (o direcciones) de la variable dinmica a la cual apunta la variable puntero (que es un parmetro de entrada) se aade(n) a la lista de direcciones disponibles. Veamos a continuacin ejemplos de cmo pueden usarse estas instrucciones.

typedef int tipoelem; typedef tipoelem * puntero; puntero p; tipoelem x;

Hasta el momento lo nico que hemos hecho ha sido declarar la variable p de tipo puntero. El tipo puntero es un tipo apuntador a elementos del tipo tipo. En el ejemplo el tipo tipo es int, aunque podra haber sido cualquier otro tipo de datos de los permitidos por el lenguaje. La interpretacin de esta declaracin es por tanto la siguiente: la variable p es una variable del tipo puntero que en tiempo de ejecucin apuntar a una variable dinmica del tipo int. Las variables dinmicas se alojan, en tiempo de ejecucin, en la memoria montn, mientras que las variables puntero pueden alojarse en el segmento de datos (si son globales), en el segmento de pila (si son locales), o incluso en la memoria montn (si son variables dinmicas formando parte de una estructura con representacin enlazada, como veremos ms adelante). Una vez que hemos declarado las variables puntero, podemos asignarles una direccin concreta de la memoria montn mediante malloc:

p = (puntero) malloc ( sizeof(tipoelem) );

14

PROGRAMACIN II- 1 GEI

La instruccin malloc se utiliza cuando se requiere una variable dinmica para albergar un dato de un tipo asociado (simple o estructurado). Otro aspecto importante es la existencia de una direccin constante (NULL) que puede ser asignada a las variables puntero. La direccin NULL puede ser utilizada como inicializacin de una variable puntero, aunque su mayor utilidad es como una marca para detectar cundo se han recorrido todas las variables dinmicas en una estructura de datos con representacin enlazada, como veremos despus. Cuando ejecutamos una instruccin malloc y no hay espacio libre en la memoria montn para la variable dinmica requerida, se asigna el valor NULL a la variable puntero pasada como parmetro. Una variable puntero puede ser asignada a otra variable puntero del mismo tipo (directamente con la sentencia de asignacin =), puede asignrsele la direccin constante NULL, puede pasarse como parmetro de una funcin, puede ser comparada con otra variable puntero o con NULL mediante los operadores relacionales = y <> y, como ya hemos visto, usarse para acceder a las variables dinmicas. Con una variable dinmica podremos hacer todo lo que el lenguaje permita con su tipo de datos. No obstante, una caracterstica importante de las variables dinmicas es que su mbito es todo el programa, es decir, se puede acceder a ellas desde cualquier parte de este, y su extensin es desde el momento en que se crean (con malloc) hasta el momento en que se destruyen (con
free). La siguiente instruccin libera la memoria montn de la variables dinmica apuntada por p

pasando por tanto las direcciones ocupadas en dicha memoria a la lista de direcciones disponibles, para que as puedan ser usadas de nuevo.
free(p);

Hay que tener por tanto bastante cuidado en no dejar variables dinmicas sin ninguna variable puntero apuntando a ellas, ya que si as fuese sera imposible liberarlas de memoria. A las variables dinmicas que se han quedado sin ninguna variable puntero apuntando a ellas se las denomina basura, ya que no se podrn utilizar (puesto que se han perdido sus referencias) y adems no pertenecen a la lista de disponibles, por lo que tampoco se podrn asignar de nuevo sus direcciones a variables puntero va malloc. El siguiente cdigo deja una variable dinmica como basura en la memoria libre:
p = (puntero) malloc ( sizeof(tipoelem) ); q = (puntero) malloc ( sizeof(tipoelem) ); p=q;

ESTRUCTURAS DE DATOS- 1 ETIS

15

ya que la variable dinmica creada con el primer malloc es ahora inaccesible puesto que su nica referencia (la variable puntero p) ha sido modificada con la instruccin p=q. Ahora, la variable dinmica creada con el segundo malloc tiene dos referencias (las variables p y q). A continuacin mostramos a modo de resumen los principales aspectos que caracterizan a las variables puntero y a las variables dinmicas: Variables de tipo puntero: o Se ubican en el segmento de datos (si son globales), en el segmento de pila (si son locales), o en la memoria montn (si son dinmicas). Su mbito y extensin depende por tanto de esta circunstancia. o o o o o Pueden ser utilizadas en instrucciones malloc y free. Pueden ser asignadas a otras variables puntero del mismo tipo. Se les puede asignar la direccin constante NULL. Pueden pasarse como parmetros de una funcin. Pueden compararse con otras variables de tipo puntero o con NULL mediante los operadores relacionales = y <>. o Pueden usarse para acceder a las variables dinmicas.

Variables dinmicas: o o Se crean con instrucciones malloc y se destruyen con instrucciones free. Se ubican en la memoria montn. Su mbito es todo el programa y su extensin es desde el momento en que se crean hasta el momento en que se destruyen. o o Se referencian mediante variables puntero. Su uso est limitado por las restricciones que impone el lenguaje para su tipo de datos.

1.5.3 Representaciones contiguas y enlazadas Nos centramos ahora en la descripcin de algunas tcnicas de representacin que podremos utilizar en la representacin de TADs cuyos casos estn formados por colecciones de elementos, como son los TADs contenedores. Distinguiremos entre representaciones contiguas, si los elementos que constituyen el caso del TAD se representan en direcciones contiguas de memoria, y

representaciones enlazadas, si los elementos constituyentes del TAD se representan en posiciones


de memoria enlazadas unas con otras usualmente mediante punteros. Como veremos, las representaciones enlazadas son ms flexibles que las contiguas ya que permiten insertar nuevos elementos o eliminar elementos existentes en cualquier parte de la estructura utilizando siempre la cantidad de memoria requerida.

16

PROGRAMACIN II- 1 GEI

Cuando estudiamos la representacin adecuada para los casos de un TAD constituidos por colecciones de elementos podemos encontrarnos, por lo general, con las siguientes situaciones: 1. Que la cantidad de elementos de los casos del TAD sea fija y conocida a priori (en tiempo de compilacin). Ante esta situacin probablemente la mejor opcin es representar los casos del TAD haciendo uso del tipo de datos estructurado ARRAY. Puesto que con un array los elementos se representan de forma contigua en la memoria del computador, esta opcin da lugar a una representacin contigua. As, si n es la cantidad fija de elementos de los casos del TAD, podemos representar estos de la siguiente forma:

typedef tipoelem estructura [n];

Podremos acceder a los elementos que constituyen el TAD de la forma


estructura[i], siendo i un ndice entero entre 0 y n-1.

2.

Que la cantidad de elementos de los casos del TAD sea fija, pero se conozca en tiempo de ejecucin. Para esta situacin, en C lo natural es definir estructura como un puntero a tipoelem y se reserva en tiempo de ejecucin memoria para tantos elementos como sea necesario:

typedef tipoelem * estructura;

Ahora, si p es de tipo estructura, usaremos las instrucciones:


estructura p; p= (estructura) malloc (n*sizeof(tipoelem) ); . free(p);

para asignar y liberar respectivamente la memoria requerida (n elementos del tipo


tipoelem). El valor de n se supone que se conoce en tiempo de ejecucin.

Para acceder a los elementos del TAD:

for (i=0; i < n ; i++) *(p+i) = i;

asigna el valor i al i-simo elemento del TAD, para i=0,,n-1. Obsrvese que en C est permitido (y es sintcticamente ms cmodo) usar p una vez que tenga asignada memoria como si fuese un array esttico. Es decir, el for anterior sera absolutamente equivalente a:
ESTRUCTURAS DE DATOS- 1 ETIS 17

for (i=0; i < n ; i++) p[i] = i;

3.

Que la cantidad de elementos de los casos del TAD sea variable en tiempo de ejecucin. Para esta situacin disponemos de varias alternativas. Una consiste en establecer una cantidad mxima max de elementos que podran contener los casos del TAD. Podemos entonces representar stos de la siguiente forma:

typedef struct { tipoelem elementos[max]; short longitud; } * estructura;

Ahora la estructura es un puntero a un registro con dos campos. El primero de ellos,


elementos, es un array que se utiliza para almacenar de forma contigua los elementos

del caso, y el campo longitud almacena el nmero de elementos que contiene el caso en un instante dado (este campo deber incrementarse o decrementarse cada vez que se aade o se elimina un elemento, respectivamente). Una segunda alternativa es la siguiente:

typedef struct { tipoelem * elementos; short longitud; } * estructura;

En esta representacin se presentan los siguientes inconvenientes: 1. No se podrn representar casos del TAD que contengan un nmero de elementos mayor que max, y se desperdiciar memoria si el nmero de elementos de un caso es considerablemente menor que max.

18

PROGRAMACIN II- 1 GEI

2.

Insertar un nuevo elemento o eliminar uno existente en posiciones intermedias de la estructura supone la reubicacin de los restantes, y por tanto un tiempo de ejecucin proporcional al nmero de elementos que constituyen el caso del TAD.

As pues, la representacin contigua puede ser aceptable para esta situacin cuando se sabe en tiempo de compilacin (para la primera alternativa) y en tiempo de ejecucin (para la segunda y tercera alternativas) el nmero aproximado de elementos que contendrn los casos del TAD, y/o cuando se requieran escasas operaciones de insercin y eliminacin de elementos. Por el contrario, si en tiempo de compilacin no se sabe nada, ni siquiera aproximadamente, acerca del nmero de elementos que contendrn los casos del TAD en tiempo de ejecucin, y/o se requieren numerosas operaciones de insercin y eliminacin de elementos, la mejor opcin puede ser la representacin enlazada. La siguiente declaracin de tipos define una estructura con representacin enlazada:

typedef struct celda { tipoelem elemento; struct celda * siguiente; } tipocelda ;

typedef tipocelda * estructura;

estructura p;

Como podemos apreciar, la representacin enlazada se obtiene mediante una

estructura de datos dinmica recursiva. En esta, los elementos que constituyen los casos
de un TAD se representan mediante celdas enlazadas entre s mediante punteros. De esta forma, cada celda contiene un elemento y un puntero a la siguiente celda (la ltima celda deber contener el valor NULL en el campo siguiente). Los inconvenientes vistos anteriormente con las representaciones contiguas desaparecen en la representacin enlazada, ya que se utiliza una celda por cada elemento, y es posible insertar o eliminar elementos en cualquier parte de la estructura sin hacer reubicaciones, sino nicamente estableciendo nuevos enlaces en un tiempo de ejecucin constante. No obstante presentan el inconveniente de requerir un puntero por cada elemento que constituya el caso del TAD. Por ahora slo hemos puesto de manifiesto las posibles situaciones que se pueden presentar y hemos esbozado posibles representaciones para stas. Los siguientes temas se dedican a describir
ESTRUCTURAS DE DATOS- 1 ETIS 19

por separado algunos de los TADs contenedores ms usados, para los cuales se mostrarn distintas implementaciones con representaciones contiguas y enlazadas, siendo stas analizadas en mayor profundidad y comparadas entre s en trminos de eficiencia. 1.5.4 Estilos imperativo, funcional y orientado a objetos Como hemos comentado, en la especificacin de las operaciones de un TAD no se establecen dependencias con un lenguaje de programacin concreto. La implementacin del TAD en un lenguaje de programacin debe cumplir la especificacin, pero sta podr llevarse a cabo normalmente mediante dos estilos de programacin diferentes: el imperativo y el funcional. Con el estilo imperativo (o procedural), las operaciones son procedimientos y las entradas y salidas son parmetros que se pasan por valor o por referencia dependiendo de si stos son de entrada, de salida o de entrada/salida. Mediante un estilo funcional las operaciones son siempre funciones que deben desarrollar una nica tarea y devolver un nico resultado, teniendo nicamente parmetros por valor que no pueden ser alterados. Normalmente, la eleccin de un estilo imperativo o funcional viene marcada por las caractersticas del lenguaje de programacin sobre el que recaiga la implementacin. Si el lenguaje es imperativo (Pascal, Modula-2, C, etc.), la eleccin ser normalmente una implementacin basada en el estilo imperativo, aunque estos lenguajes usualmente permiten implementar tambin las operaciones mediante funciones. Si el lenguaje es funcional (LISP, Hope, Miranda, etc.), entonces tenemos poca eleccin y la implementacin del TAD se realizar mediante el estilo funcional. El estilo imperativo suele usarse en conjuncin con el diseo iterativo, mientras que el funcional se utiliza bajo diseo recursivo. Por otro lado, tenemos la opcin de la Programacin Orientada a Objetos, para lo cual requerimos de un lenguaje de programacin que la soporte. La terminologa asociada a los TADs cambia en POO. As, un TAD se denomina clase, las operaciones del TAD son mtodos, y los casos o valores del TAD son objetos. La POO constituye de esta forma un nuevo Paradigma de Programacin en el cual las caractersticas deseables que giran en torno al concepto de TAD se resaltan de una forma sencilla y elegante, incluyendo otros conceptos como el de herencia y

polimorfismo, que vienen a potenciar la reusabilidad del software y la jerarquizacin entre objetos
y clases dentro de un programa. Existen otras elecciones como es la Programacin Lgica, que requiere tambin un lenguaje de programacin propio y que se utiliza normalmente en aplicaciones de Inteligencia Artificial. Nosotros adoptaremos, en general, un estilo imperativo, de acuerdo a las caractersticas del lenguaje de programacin sobre el que trabajaremos (C). No obstante, incluimos algunos ejemplos de implementaciones funcionales y posterior uso tambin desde un estilo funcional.

20

PROGRAMACIN II- 1 GEI

1.5.5 Ejemplos de implementaciones Mostraremos finalmente la implementacin en C del TAD Bolsa cuya especificacin se describi anteriormente. El TAD Bolsa es un TAD contenedor que se ha definido como mutable y se ha implementado con un estilo imperativo, utilizando representacin enlazada. Se ha incluido tratamiento de errores. TAD Bolsa bolsa.h
#include "errores.h" typedef void * bolsa; typedef int tipoelem; void bolsavacia(bolsa *b); void poner (bolsa *b, tipoelem e, codigo_error * cod); int esvacia (bolsa b); short cuantos (bolsa b, tipoelem e); void dest (bolsa *b); /* un tipo opaco */

bolsa.c

#include <stdlib.h> #include "errores.h" typedef int tipoelem; typedef struct celda { tipoelem elemento; struct celda * siguiente; } tipocelda ; typedef tipocelda * puntero; typedef puntero bolsa;

void bolsavacia(bolsa *b) { *b=NULL; } void poner (bolsa *b, tipoelem e, codigo_error * cod) { puntero aux; aux=(puntero) malloc(sizeof(tipocelda)); if (aux == NULL) *cod = meminsu; else {
21

ESTRUCTURAS DE DATOS- 1 ETIS

aux->elemento = e; aux->siguiente = *b; *b = aux; *cod = exito; } }

/* equivale a *aux.elemento= e /* equivale a ... */

*/

int esvacia (bolsa b) { if (b==NULL) return 1; else return 0; } short cuantos (bolsa b, tipoelem e) { short cont; puntero aux; aux = b; cont = 0; while (aux!=NULL) { if (aux->elemento == e) cont++; aux = aux->siguiente; } return cont; } void dest (bolsa *b) { puntero aux, aux2; aux = *b; while (aux!=NULL) { aux2 = aux; aux = aux->siguiente; free(aux2); *b = aux ; } }

errores.h

typedef enum{exito,meminsu} codigo_error; /* exito: Operacin con xito meminsu : Memoria insuficiente */

Ejercicio: Implementar en C el TAD Racional descrito anteriormente.


22 PROGRAMACIN II- 1 GEI

ESTRUCTURAS DE DATOS- 1 ETIS

23