Estructuras de Datos Dinámicas Programación Modular Curso 2002-2003 David Bueno Vallejo ÍNDICE I.1. INTRODUCCION A LAS ESTRUCTURAS DE DATOS DINÁMICAS............................................1 I.2. EL TIPO PUNTERO...................................................................................................................................3 I.2.1. OPERACIONES CON PUNTEROS.............................................................................................................. 6 DESREFERENCIACIÓN.............................................................................................................................. 7 ASIGNACIÓN Y COMPARACIÓN.............................................................................................................. 7 PASO DE PUNTEROS COMO PARÁMETROS .......................................................................................... 8 I.3. APLICACIÓN: LISTAS ENLAZADAS..................................................................................................8 I.3.1. OPERACIONES BASICAS SOBRE LISTAS ENLAZADAS. .......................................................................... 9 INSERTAR UN NODO AL PRINCIPIO .................................................................................................... 10 ELIMINAR EL PRIMER NODO. .............................................................................................................. 11 INSERTAR UN NODO EN UNA LISTA ENLAZADA ORDENADA ........................................................ 12 ELIMINAR UN NODO DE UNA LISTA ENLAZADA.............................................................................. 14 Estructuras de Datos Dinámicas I.1. 1 INTRODUCCION A LAS ESTRUCTURAS DE DATOS DINÁMICAS Hasta ahora, todos los tipos de datos que se han visto, ya sean simples o estructurados, tienen una propiedad común: son estáticos. Esto significa que las variables que se declaran en un programa de alguno de estos tipos mantendrán la misma estructura durante la ejecución del mismo. Son variables estáticas, se definen en tiempo de compilación. Ejemplos: o Si se declara una variable entera, real, etc, puede cambiar su contenido, pero no el tamaño asignado por el compilador para ella. o Si se declara un Array de 5 elementos de tipo Z, éste podrá cambiar su contenido, pero no su estructura. Sin embargo, hay muchas situaciones en las que no sólo debe cambiar el contenido o valor de una variable, sino también su tamaño. Esto es, hay situaciones en las que en tiempo de compilación no se puede determinar la cantidad de memoria necesaria para almacenar cierta información, sino que hay que esperar al tiempo de ejecución. La técnica usada para manejar estas situaciones es la asignación dinámica de memoria. Con este tipo de asignación se tendrán variables dinámicas o referenciadas (ya se verá el motivo por el que se denominan de esta segunda forma), que pueden crearse y destruirse en tiempo de ejecución. Ejemplo: Si se pide diseñar un programa para gestionar una agenda de teléfonos con los datos de personas (nombre, apellidos, dirección, cumpleaños, teléfono, email, etc...). ¿Qué estructura puede utilizarse para realizarla? Con los conocimientos adquiridos hasta este tema, parece razonable utilizar un array de personas. Una posible definición sería: Constantes MAXAG = 50 Tipos REGISTRO TPersona TCadena nombre, apellidos, dirección, email Fecha cumpleaños N telefono TPersona TAgenda[1..MAXAG] Variables TipoAgenda ag Si se usa un array de caracteres de MAXAG posiciones pueden aparecer los siguientes problemas: 1. ¿Qué pasa si se quieren insertar más personas de MAXAG en la agenda? ? Hay que recompilar el programa para ampliar MAXAG 2. Si el número de personas de la agenda es mucho menor que MAXAG se está desperdiciando memoria que podría ser utilizada por otros programas Estructuras de Datos Dinámicas 2 Solución: ... Sería deseable no tener que reservar al principio memoria para MAXAG Ag[1] Nombre1,... Nombre2,... personas, sino que cada vez que se fuera a insertar una persona en la Ag[2] Nombre2,... Ag[3] agenda, se pidiera sólo la memoria Nombre1,... Nombre3,... necesaria para los datos de esa Ag[4] Nombre4,... persona. Como puede verse en la Ag[5] Fig1.a., cuando se define un array, se Nombre4,... reserva una zona contigua de Nombre3,... memoria del tamaño Ag[50] MAXAG*Tamaño(TPersona). En la Fig.1.b., se supone disponible la Variable estática: Ag [1..50] posibilidad de reservar memoria de Fig 1. a) Agenda estática b) Reserva dinámica de memoria forma dinámica, es decir, conforme se va necesitando. Pero parece que falta algo más. Cuando se reserva memoria los datos no tienen porque estar contiguos, ni siquiera ordenados, ya que el sistema operativo devuelve una porción de memoria libre del tamaño solicitado y ésta puede estar en cualquier lugar de la memoria. En la Fig 1.b., puede verse que los datos no están ni contiguos ni ordenados. Por lo tanto parece necesario otra herramienta que permita relacionar cada uno de los registros. Esa herramienta está representada en Fig 2.b. por una flecha que une los distintos registros. Es decir, si se pueden unir los registros mediante esas flechas (a las que llamaremos punteros y que se desarrollaran en el próximo apartado), no importará el lugar donde se encuentre cada registro. También Ag[1] Nombre1,... Nombre2,... * puede verse que cada uno de estos Ag[2] elementos no tienen nombre Nombre2,... Ag[3] (variables anónimas). En la versión * Nombre1,... Nombre3,... con array, todos los elementos Ag[4] Ag Nombre4,... tenían un nombre (ag[1], ag[2], Ag[5] etc...). Hasta este punto se dispone Nombre4,... * de un conjunto de elementos Ag[50] Nombre3,... * unidos, pero como no tienen nombre se necesita una referencia para el principio. Para ello es Variable estática: Ag [1..50] * Variable sin nombre (Anónimas) necesario definir la variable ag en la Fig. 2.b, que apuntará al primer Fig. 2.a) Agenda con memoria Estática b) Agenda con memoria dinámica elemento. ... En este apartado se ha intentado mostrar la necesidad de utilizar reserva dinámica de memoria, y las herramientas necesarias, para poder llevarla a cabo. En el siguiente apartado se verá con más detalle como se puede reservar memoria durante la ejecución, Estructuras de Datos Dinámicas 3 además de las características de un nuevo tipo llamado puntero que servirá para unir las distintas porciones de memoria reservada en el ejemplo anterior. Estructuras de Datos Dinámicas 4 I.2. EL TIPO PUNTERO. Siempre que se habla de punteros hay que diferenciar claramente entre lo que es: - Variable referencia o apuntadora (o simplemente puntero) - Variable referenciada o apuntada. Una variable de tipo Puntero (variable referencia) contendrá una referencia, esto es, la dirección en memoria de una variable de un tipo determinado (variable referenciada). Por tanto, siempre existirá asociado al tipo Puntero, otro tipo que es el de la variable referenciada. Cuando se defina un tipo Puntero, en el pseudocodigo hay que escribir: Tipos Tipo *TipoP donde Tipo es el tipo de la variable referenciada. Por ejemplo: Z *TipoP Para declarar una variable de ese tipo: Variables TipoP p En un momento dado, la variable p será un puntero, o lo que es lo mismo, contendrá la dirección en memoria de una variable de tipo entero, que a su vez, contendrá un entero. En el siguiente ejemplo se muestra como quedaría en memoria la representación de una variable puntero (ptr) que apunta a un entero con valor 100: Dirección de memoria MEMORIA ptr Contenido de la memoria Variable 23423 23419 234343 23420 324237 23421 28 23422 100 23423 Anónima Fig. 3. Representación de la memoria de la variable puntero ptr Dir 23419 p 23423 Dir 23423 100 Estructuras de Datos Dinámicas 5 Así es como realmente aparece en memoria, pero de forma más gráfica se representará esa situación como: p 100 de la variable puntero p saldrá una flecha hacia la variable referenciada correspondiente. Para acceder a la variable apuntada (referenciada) hay que hacerlo a través de la variable puntero, ya que aquella no tiene nombre (por esto, también se le denomina variable anónima). La sintaxis para ello es *p. En nuestro ejemplo *p = 100. Si se quiere acceder a un campo de un registro se utilizará p-> Ejemplo: Tipos REGISTRO TipoRegistro Z num C car FINREGISTRO TipoRegistro *TipoPuntero Variables TipoPuntero p Con esto: *p p->num p->car Será un registro con dos campos. Será una variable de tipo entero. Será una variable de tipo carácter. p 4 'A' Si se desea que una variable de tipo Puntero (a algo), en un momento determinado de la ejecución del programa no apunte a nada, se le asignará la palabra reservada NIL. Gráficamente se representará como: p = NIL p En este punto ya se sabe como definir tipos Punteros y como declarar variables de tipo Puntero. Pero ¿por qué los punteros nos ayudan en la creación dinámica de variables conforme se van necesitando? Cuando se declara una variable de tipo Puntero, por ejemplo: TipoP p, la variable p se establece estáticamente, en tiempo de compilación, pero la variable referenciada o Estructuras de Datos Dinámicas 6 anónima no se crea en este momento, sino que se creará después, en tiempo de ejecución (variable referenciada <=> variable dinámica), mediante una llamada a una función predefinida encargada de localizar y reservar una zona de memoria de tamaño suficiente para contener a la variable referenciada. Esta función tendrá como argumento el tamaño de la variable referenciada a crear, y devolverá la dirección de memoria de la zona reservada. Dicha dirección de memoria se deberá almacenar en una variable de tipo puntero con objeto de acceder posteriormente a la variable dinámica creada. Ejemplo: p := Reservar(Tamaño(Z)) La ejecución de esta sentencia creará una variable anónima de tamaño suficiente para almacenar un entero, y hará que p apunte a ella. p Tamaño de un entero La función predefinida Tamaño(Tipo), previamente utilizada, nos devuelve el nº de bytes que ocupa una variable del tipo de datos especificado como parámetro. Para simplificar, en nuestro pseudolenguaje, la sentencia de asignación anterior (para cualquier tipo de datos de la variable referenciada) se sustituirá por la siguiente sentencia: Asignar(p) No se puede olvidar que crear una variable no es más que reservarle una zona de memoria. Al igual que se pueden crear variables, también se pueden eliminar (liberar la memoria que ocupan para que pueda ser reutilizada) dinámicamente mediante el procedimiento predefinido Liberar(p,t), cuya ejecución hace que se libere el espacio de memoria de tamaño t apuntado por p, haciendo además que p tome el valor NIL. Gráficamente: p Se libera Al igual que se abrevia la sintaxis para crear variables dinámicas, también se simplificará para eliminarlas. Para ello, se prescindirá del parámetro t en la operación Liberar. I.2.1. OPERACIONES CON PUNTEROS Estructuras de Datos Dinámicas 7 1.- Desreferenciación 2.- Asignación 3.- Comparación. 4.- Paso de punteros como parámetros. Sea el siguiente ejemplo de TipoPuntero: Tipos REGISTRO Complejo R PartReal, PartImag FINREGISTRO Complejo *PunteroComplejo Variables PunteroComplejo punt1, punt2 DESREFERENCIACIÓN Si se realiza la llamada: Asignar(punt1) punt1 La nueva variable no tiene valor alguno definido. El programador podrá, por ejemplo, asignarle: punt1->PartReal = 5.0 punt1->PartImag = 1.2 punt1 5.0 1.2 La operación mediante la cual nos referimos a la variable anónima añadiendo ^ al nombre de la variable puntero se denomina desreferenciación. ASIGNACIÓN Y COMPARACIÓN Al igual que en cualquier tipo de datos visto anteriormente, en el tipo puntero también están predefinidas las operaciones de asignación (=) y comparación de igualdad/desigualdad (==, !=). A una variable puntero se le puede asignar el valor contenido en otra variable puntero del mismo tipo. Esto implica que tras la operación ambas variables apuntarán a la misma zona de memoria, es decir, a la misma variable referenciada. Ejemplo: punt2 = punt1 Estructuras de Datos Dinámicas 8 punt1 5.0 1.2 punt2 La palabra reservada NIL se le puede asignar a cualquier variable puntero sea cual sea el tipo de ésta. Ejemplo: punt2 = NIL punt2 La operación de comparación de igualdad con punteros se utiliza para saber si dos variables puntero (del mismo tipo) apuntan a la misma variable referenciada (punt1 == punt2) o para determinar si una variable puntero contiene el valor NIL (punt1 == NIL). PASO DE PUNTEROS COMO PARÁMETROS El paso de parámetros por valor y por referencia de punteros se comporta de igual forma que con cualquier otro tipo de datos. Así: ? Si un parámetro real puntero p se pasa por valor, el parámetro formal q correspondiente obtendrá una copia del valor de dicho puntero. Ambas variables puntero referenciarán a la misma variable dinámica. Una modificación de esta variable dinámica dentro del subprograma, perdurará tras la ejecución del mismo. En cambio una modificación de q, no afectará al parámetro real p. ? Si un parámetro real puntero p se pasa por referencia, el parámetro formal q correspondiente obtendrá una referencia a dicho puntero. Cualquier modificación de q, se verá reflejado en p. I.3. APLICACIÓN: LISTAS ENLAZADAS Lo que realmente hace de los punteros una herramienta potente es la circunstancia de que pueden apuntar a variables que a su vez contienen punteros (Ejemplo, registros en los que uno de sus campos es a su vez un puntero). Ejemplo: Se desarrollará mediante punteros el tratamiento de la agenda que introdujo en la problemática de las variables estáticas/dinámicas. A continuación se definen los tipos y las variables: Estructuras de Datos Dinámicas 9 Tipos TNodo *TPuntANodo REGISTRO TNodo TPersona info TPuntANodo sig FIN Variables TPuntANodo ag Nombre2,... Nombre1,... Ag En la Fig. 4 se vuelve a representar la agenda en memoria. Ahora es fácil comprender que la variable ag es un puntero de tipo TpuntAnodo y que cada elemento tiene los datos de una persona y un puntero al siguiente elemento de la agenda. Para saber cual es el último elemento es necesario que su puntero al siguiente elemento este a NIL. Nombre4,... Nombre3,... Fig 4. Agenda en memoria Si se desplegase la agenda de forma lineal se tendría lo siguiente: ag Nom br e1, .. Nom br e2, .. Nombr e3, .. Nombre4,. . A este tipo de estructuras se les denomina Lista Enlazada y a cada uno de los registros que forman la misma se le denomina nodo. En general, un nodo de una lista enlazada puede contener toda la información que se desee (todos los campos que se quieran), más un campo de tipo Puntero, que apuntará al siguiente registro. I.3.1. OPERACIONES BASICAS SOBRE LISTAS ENLAZADAS. En este apartado se verán alguna de las operaciones que se pueden realizar sobre las listas enlazadas. Como elemento de cada nodo se utilizará un entero. Esto dará lugar a una lista de enteros: Tipos TNodo *ListaEnt REGISTRO TNodo Z dato ListaEnt sig FINREGISTRO Variables ListaEnt lista, ptr Estructuras de Datos Dinámicas 10 INSERTAR UN NODO AL PRINCIPIO En la lista del ejemplo siguiente se quiere insertar al principio de la misma un nodo con un valor 5 en su campo entero. lista 3 9 2 Los pasos que habría que dar una vez creada la lista, para insertar dicho nodo serían: 1.- Asignar(ptr) ptr 2.- ptr->dato = 5 ptr 5 3.- ptr->sig = lista lista 3 2 ptr 5 4.- lista = ptr 9 Estructuras de Datos Dinámicas 11 lista 3 9 2 ptr 5 ELIMINAR EL PRIMER NODO. Partiendo de la lista: lista 3 2 9 3 2 9 3 2 9 1.- ptr = lista ptr lista 2.- lista = lista->sig ptr lista 3.- Liberar(ptr) ptr 2 lista 9 Estructuras de Datos Dinámicas 12 INSERTAR UN NODO EN UNA LISTA ENLAZADA ORDENADA Se quiere insertar, por ejemplo, el valor 7 en la lista: lista 2 9 6 Además de lista y ptr será necesario otra variable de tipo Puntero nuevonodo. Las acciones a realizar son: 1.- Asignar(nuevonodo) nuevonodo 2.- nuevonodo->dato = 7 nuevonodo 7 3.SI lista == NIL ENTONCES (* lista vacia *) nuevonodo->sig := NIL lista = nuevonodo SINOSI nuevonodo->dato <= lista->dato ENTONCES (* insertar al principio *) nuevonodo->sig = lista lista = nuevonodo SINO (* insertar en medio o al final *) ptr = lista MIENTRAS (ptr->sig != NIL) AND ((nuevonodo->dato)>(ptr->sig->dato)) HACER ptr = ptr->sig FINMIENTRAS nuevonodo->sig = ptr->sig ptr->sig = nuevonodo FINSI Estructuras de Datos Dinámicas 13 ptr lista 2 9 6 7 nuevonodo Estructuras de Datos Dinámicas ELIMINAR UN NODO DE UNA LISTA ENLAZADA El nodo a eliminar será el que contenga el número almacenado en valor. Además de lista se necesitan 2 variables de tipo Puntero ptr y ant. SI lista != NIL ENTONCES (* lista no vacia *) SI (lista->dato == valor) ENTONCES (* eliminar el primer nodo *) ptr = lista lista = lista^.sig Liberar(ptr) SINO (* buscar dato en resto de la lista *) ant = lista ptr = lista^.sig MIENTRAS ( (ptr != NIL) AND (ptr^.dato != valor)) HACER ant = ptr ptr = ptr->sig FINMIENTRAS SI (ptr != NIL) ENTONCES (* encontrado *) ant->sig = ptr->sig Liberar(ptr) FINSI FINSI FINSI 14