Beruflich Dokumente
Kultur Dokumente
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.
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.
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
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
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).
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:
Modifica: Indica los argumentos de entrada que cambian de valor tras una llamada
a la abstraccin operacional.
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
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.
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.
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:
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.
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.
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.
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:
14
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;
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
16
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:
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:
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
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:
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:
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
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:
estructura p;
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
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
*/
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 */
23