ETSI Telecomunicación Memoria Dinámica Tema 2. Memoria Dinámica Contenido § Gestión de Memoria Dinámica − Introducción. Datos estáticos y dinámicos − Asignación dinámica de memoria § Tipo Puntero − Declaración de variables de tipo puntero − Operaciones con punteros − Gestión dinámica de memoria − Punteros a registros § Operaciones sobre Listas Enlazadas − Otras clases de listas enlazadas § Ejemplo de Gestión de Memoria Dinámica en C/C++ § Ejercicios Propuestos § Bibliografía Elementos de Programación Página 1 ETSI Telecomunicación Memoria Dinámica 2.1. Gestión Dinámica de Memoria En este apartado se analizarán las causas que llevan a los lenguajes de programación a ofrecer elementos y características necesarias para permitir la gestión de memoria de manera dinámica. Se verán cuáles son las ventajas e inconvenientes de esta técnica, así como las diferencias entre tipos de datos estáticos y dinámicos en cuanto a su almacenamiento en memoria principal y su tratamiento por parte de los programas (algoritmos) que los manejan. Así mismo, se establecerán las bases teóricas necesarias para la comprensión del funcionamiento del mecanismo de gestión dinámica de memoria. 2.1.1. Introducción Los tipos de datos, tanto simples como estructurados, vistos hasta ahora en los temas anteriores de las asignaturas de Introducción a los Computadores (IC) y Elementos de Programación (EP), sirven para describir datos o estructuras de datos cuyos tamaños y formas se conocen de antemano. Sin embargo, en muchos programas es necesario que las estructuras de datos estén diseñadas de manera que su tamaño y forma varíe a lo largo de la ejecución de aquellos. Con esto se consigue, fundamentalmente, que estos programas funcionen de manera más eficiente y con un aprovechamiento óptimo de los recursos de almacenamiento en memoria principal. Las variables de todos los tipos de datos vistos hasta el momento son denominadas variables estáticas, en el sentido en que se declaran en el programa, se designan por medio del identificador declarado, y se reserva para ellas un espacio en memoria en tiempo de compilación de los programas. El contenido de la variable estática puede cambiar durante la ejecución del programa o subprograma donde está declarada, pero no así el tamaño en memoria reservado para ella. Esto significa que la dimensión de las estructuras de datos a las que se refieren estas variables debe estar determinada en tiempo de compilación, lo que puede suponer una gestión ineficiente de la memoria, en el sentido de que puede implicar el desperdicio (por sobredimensionamiento) o la insuficiencia (por infradimensionamiento) de memoria. Sin embargo, son muchos los lenguajes de programación que ofrecen la posibilidad de crear y destruir variables en tiempo de ejecución, de manera dinámica, a medida que van siendo necesitadas durante la ejecución del programa. Puesto que estas variables no son declaradas explícitamente en el programa y no tienen identificador (nombre) asignado, se denominan variables anónimas. El pseudolenguaje utilizado en las asignaturas de IC y EP permite el uso de este tipo de variables. Para ello, ofrece los mecanismos y la sintaxis necesaria para su creación, a la vez que proporcionará una manera de referirse a estas Elementos de Programación Página 2 ETSI Telecomunicación Memoria Dinámica variables para el acceso a los datos que contienen y la asignación de valores a los mismos. Todo esto se lleva a cabo mediante el empleo del tipo puntero, cuyas características se expondrán en los siguientes apartados. Datos estáticos: su tamaño y forma es constante durante la ejecución de un programa y, por tanto, se determinan en tiempo de compilación. El ejemplo típico son los arrays. Tienen el problema de que hay que dimensionar la estructura de antemano, lo que puede conllevar desperdicio o falta de memoria. Datos dinámicos: su tamaño y forma es variable (o puede serlo) a lo largo de un programa, por lo que se crean y destruyen en tiempo de ejecución. Esto permite dimensionar la estructura de datos de una forma precisa: se va asignando memoria en tiempo de ejecución según se va necesitando. 2.1.2. Asignación dinámica de memoria Cuando se habla de asignación dinámica de memoria se hace referencia al hecho de crear variables anónimas −es decir, reservar espacio en memoria para estas variables en tiempo de ejecución del programa− así como liberar el espacio reservado para dichas variables anónimas, cuando ya no son necesarias, también durante el tiempo de ejecución. Instrucciones de programa Datos estáticos Límite de datos estáticos Zona dinámica fragmentada (heap) Límite de la pila Pila Puntero de la pila (stack pointer) Figura 2.1. Esquema de asignación de memoria Elementos de Programación Página 3 ETSI Telecomunicación Memoria Dinámica La zona de la memoria principal del computador donde se reservan espacios para asignarlos a variables dinámicas se denomina heap o montón. Cuando el sistema operativo carga un programa para ejecutarlo y lo convierte en proceso, le asigna cuatro partes lógicas en memoria principal: instrucciones, datos (estáticos), pila y una zona libre. Esta zona libre (heap) es la que va a contener los datos dinámicos. En cada instante de la ejecución del programa, el heap tendrá partes asignadas a datos dinámicos y partes libres disponibles para asignación de memoria, como puede observarse en la figura 2.1. El mecanismo de asignación-liberación de memoria durante la ejecución del programa hace que esta zona esté usualmente fragmentada (ver figura 2.1), siendo posible que se agote su capacidad si no se liberan las partes utilizadas ya inservibles. (La pila también varía su tamaño dinámicamente, pero la gestiona el sistema operativo, no el programador.) Para trabajar con datos dinámicos son necesarias dos cosas: • Subalgoritmos predefinidos en el lenguaje (pseudolenguaje) que permitan gestionar la memoria de forma dinámica (asignación y liberación). • Algún tipo de dato con el que sea posible acceder a esos datos dinámicos (ya que con los tipos vistos hasta ahora en las asignaturas de IC y EP sólo se puede acceder a datos con un tamaño y forma ya determinados). 2.2. Tipo Puntero El tipo puntero y las variables declaradas de tipo puntero se comportan de manera diferente a las variables estáticas estudiadas en los temas anteriores de las asignaturas de IC y EP. Hasta ahora, cuando se declaraba una variable de un determinado tipo, ésta podía contener ‘directamente’ un valor de dicho tipo, simplemente llevando a cabo una asignación de ese valor a la variable. Con las variables de tipo puntero esto no es así. Las variables de tipo puntero permiten referenciar datos dinámicos, es decir, estructuras de datos cuyo tamaño varía en tiempo de ejecución. Para ello, es necesario diferenciar claramente entre: • la variable referencia o apuntadora, de tipo puntero, • y la variable anónima referenciada o apuntada, de cualquier tipo, tipo que está asociado siempre al puntero. Físicamente, el puntero no es más que una dirección de memoria. En la figura 2.2 se muestra un ejemplo, a modo de esquema teórico, de lo que podría ser el contenido de varias posiciones de memoria principal, en la que se puede ver cómo una variable apuntadora o puntero, almacenado en la posición de memoria 7881(16, contiene, a su vez, otra dirección de memoria, la 78AC(16, la de la variable referenciada o anónima, que contiene el dato 6677(16. Elementos de Programación Página 4 ETSI Telecomunicación Memoria Dinámica De esta manera se ilustra cómo el puntero contiene una dirección de memoria que ‘apunta’ a la posición de memoria donde se almacena un dato de cierto tipo asociado al puntero. Dirección Contenido ... 7881(16 ... 78AA(16 78AB(16 78AC(16 78AD(16 78AE(16 ... Puntero Variable referenciada ... 78AC(16 ... AACC(16 6743(16 6677(16 89FF(16 DC34(16 ... Figura 2.2. Esquema de posiciones de memoria con punteros Definición: un puntero es una variable cuyo valor es la dirección de memoria de otra variable Según su definición, un puntero se ‘refiere’ indirectamente a un valor, por lo que no hay que confundir una dirección de memoria con su contenido (ver figura 2.3). VARIABLES C car = ‘z’ Dirección ... ... 7C16(16 7C17(16 7C18(16 ... ‘z’ ... ... ... Dirección de la variable ‘car’ = 7C17(16 Contenido de la variable ‘car’ = ‘z’ Figura 2.3. Esquema de posiciones de memoria donde se muestra la diferencia entre la dirección de una variable y su contenido Una variable de tipo puntero no puede apuntar a cualquier variable anónima; debe apuntar a variables anónimas de un determinado tipo. El tipo de la variable anónima debe ser incluido en la especificación del tipo de la variable puntero. Elementos de Programación Página 5 ETSI Telecomunicación Memoria Dinámica 2.2.1. Declaración de variables de tipo puntero La declaración de una variable de tipo puntero1 en el pseudolenguaje de la asignatura consiste en un tipo base, un asterisco ‘*’ y el nombre de la variable. La forma general de la declaración de una variable de tipo puntero es, según la notación BNF, la siguiente (en la correspondiente sección de VARIABLES): <Def_TipoPuntero> ::= <TipoBase> *<TipoPuntero> donde <TipoBase> es el tipo base del puntero, que puede ser cualquier tipo válido, simple o compuesto. Ejemplos de declaración de variables de tipo puntero son los siguientes: VARIABLES N *contador // Puntero a una variable de tipo natural (N) C *car // Puntero a una variable de tipo carácter (C) Así, la variable contador del ejemplo anterior no contiene un valor de tipo natural (N), sino la dirección de memoria donde estará almacenado un valor de tipo natural. El valor almacenado en la variable anónima de tipo natural será accesible a través del puntero contador. También es posible, como para el resto de los tipos simples o compuestos vistos en las asignaturas de IC y EP, declarar nuevos tipos puntero, mediante la inclusión de los mismos en la correspondiente sección de TIPOS del algoritmo, siguiendo la sintaxis vista más arriba. A partir de estos nuevos tipos, pueden declararse nuevas variables. Por ejemplo, TIPOS N *TipoPtrNatural // Tipo puntero a un número natural C *TipoPtrCaracter // Tipo puntero a un carácter ... VARIABLES TipoPtrNatural contador, ptr // Variables de tipo puntero // a un número natural TipoPtrCaracter car // Variable de tipo puntero a un carácter ptr 33 Figura 2.4. Representación gráfica de punteros 1 Para abreviar, se suele llamar puntero a una variable de tipo puntero, por lo que, a partir de ahora, se utilizará más asiduamente ese primer término por ser de uso más común y conciso. Elementos de Programación Página 6 ETSI Telecomunicación Memoria Dinámica El hecho de que la variable ptr, declarada en el ejemplo anterior, esté apuntado a un dato de tipo natural de valor, por ejemplo, 33, puede representarse gráficamente como en la figura 2.4 (siendo muy útil este tipo de representación para posteriores operaciones donde intervienen punteros de una manera más compleja, como en el caso de las listas enlazadas que se analizarán al final del capítulo). Un puntero puede apuntar a cualquier tipo de dato predefinido del pseudolenguaje o bien definido por el usuario, tanto tipos simples como tipos compuestos. Es importante tener en cuenta, en el caso de tipos definidos por el usuario, que primero debe declararse el tipo de datos al que apuntará el puntero (un array, un registro, etc.) y, posteriormente, el tipo de datos puntero a ese tipo definido por el usuario. Por ejemplo, TIPOS REGISTRO TipoComplejo R parteReal, parteImaginaria FINREGISTRO TipoComplejo *TipoPtrComplejo /* TipoPtrComplejo es un tipo puntero a un registro */ VARIABLES TipoComplejo *ptr1 // Puntero a un registro TipoPtrComplejo ptr2 // Puntero a un registro De esta forma, en el ejemplo anterior, ptr1 y ptr2 son variables de tipo puntero que contendrán direcciones de memoria donde estarán almacenadas variables (anónimas) de tipo TipoComplejo. Es importante, por tanto, que el dato al que apunte el puntero sea del tipo base del que se ha declarado éste. Puede considerarse como una buena norma de estilo, la declaración del tipo de datos puntero a <TipoBase> inmediatamente después de la propia declaración de <TipoBase>, como se ha hecho en el ejemplo anterior para TipoPtrComplejo y TipoComplejo, respectivamente. 2.2.2. Operaciones con Punteros Las operaciones que se pueden llevar a cabo con punteros son: § Operaciones específicas de punteros. § Asignación de punteros. § Comparación de punteros. Operadores específicos de punteros Para trabajar con punteros se utilizan dos operadores específicos: el operador de dirección (&) y el operador de indirección (*). Elementos de Programación Página 7 ETSI Telecomunicación Memoria Dinámica El operador de dirección (&) es un operador monario (sólo requiere un operando) que devuelve la dirección de memoria del operando. Por ejemplo, ... VARIABLES Z valor, dato = -333 Z *ptrValor ... INICIO ... valor = 999 ptrValor = &valor En el ejemplo anterior se consigue que el puntero ptrValor contenga la dirección de memoria donde está almacenado el dato que contiene la variable valor, es decir, 999. Puede decirse que la instrucción ptrValor = &valor significa ‘ptrValor recibe la dirección de valor’. Esto puede verse gráficamente en la figura 2.5. ptrValor = &valor Dirección ... ... 7C16 7C17 7C18 -333 999 ... dato valor ... 7C17 ptrValor Figura 2.5. Operador de dirección El operador de contenido o indirección (*) es el operador complementario del operador de dirección (&). Es también un operador monario, que devuelve el valor de la variable anónima ubicada en la dirección a la que apunta el puntero. Continuando con el ejemplo anterior, si ptrValor contiene la dirección de memoria de la variable valor, entonces es posible hacer la siguiente asignación: dato = *ptrValor dato = *ptrValor Dirección ... ... 7C16 7C17 7C18 999 999 ... dato valor ... 7C17 ptrValor Figura 2.6. Operador de indirección Elementos de Programación Página 8 ETSI Telecomunicación Memoria Dinámica Esta asignación colocará el valor de la posición de memoria 7C17(16, es decir el número 999, en la variable dato, como se esquematiza en la figura 2.6. Nota: no se debe confundir el operador * de las declaraciones de punteros (N *ptrNatural) con el operador de indirección usado en los ejemplos anteriores (*ptrNatural = 939). Asignación de punteros Justo después de declarar un puntero con la sintaxis vista en el apartado 2.2.1, el puntero contiene un valor indeterminado. Por ello, no es correcto, desde el punto de vista del pseudolenguaje, hacer uso del puntero (por ejemplo a la derecha de una asignación en la que aparece precedido del operador de indirección) antes de asignarle valor al mismo. Una primera manera de inicializar el valor de un puntero es asignarle el valor nulo. De esta manera podemos considerar que el puntero, en lugar de apuntar a una posición indeterminada cuyo acceso sería incorrecto (porque podría contener cualquier dato incluso de otros programas), no estará apuntando ‘a ninguna parte’. La manera de que dispone el pseudolenguaje de realizar esta inicialización es mediante la asignación al puntero, independientemente de su tipo, de la constante predefinida NULO. Podríamos considerar que el pseudolenguje ‘garantiza que no existe ningún dato en la posición NULO’. Por ejemplo, si se declara la variable ptrReal como un puntero a un número real (R), sería posible hacer la siguiente asignación, indicando que ptrReal ‘no apunta a ninguna parte’ en este momento: ptrReal = NULO Esto se representará gráficamente como en la figura 2.7. ptrReal = NULO ptrReal Figura 2.7. Asignación de NULO a una variable de tipo puntero Es posible asignar el valor de una variable puntero a otra variable puntero, siempre que ambas sean del mismo tipo. Por ejemplo, R *ptrReal1, *ptrReal2=NULO ... ptrReal1 = ptrReal2 En este último ejemplo la variable ptrReal1 apuntará a donde apunte la variable ptrReal2, en este caso a NULO. Es importante tener en cuenta que si ptrReal1, antes de la Elementos de Programación Página 9 ETSI Telecomunicación Memoria Dinámica asignación, estaba apuntando a una variable anónima de tipo real (y, por tanto, tenía un valor distinto del valor NULO), ésta será a partir de ahora inaccesible, puesto que la única manera de acceder a ella era a través del puntero ptrReal1 y ahora éste apunta a otra variable anónima o, como en este ejemplo, a NULO. Comparación de punteros Es posible comparar dos variables de tipo puntero en una expresión relacional usando operadores relacionales de igualdad (==), desigualdad (!=) y comparación (<, >, <=, >=). Dos variables puntero son iguales si ambas apuntan a la misma variable anónima o ambas están inicializadas al valor NULO. Los punteros que constituyen los operandos de estas operaciones relacionales binarias deben ser siempre del mismo tipo. Sin embargo, siempre es posible comparar cualquier puntero (igualdad o desigualdad) con el valor NULO. Ejemplo, SI (ptr1 < ptr2) ENTONCES Escribir(“ptr1 apunta a una dirección menor que ptr2”) FINSI 2.2.3. Gestión Dinámica de Memoria Por gestión dinámica de memoria se entiende el hecho de crear variables anónimas, es decir, reservar espacio en memoria para estas variables en tiempo de ejecución, y también de liberar el espacio ocupado en memoria por una variable anónima, asimismo en tiempo de ejecución, cuando esa variable ya no es necesaria. Por tanto, antes de asignar a la variable anónima de un puntero un determinado valor (por ejemplo, *ptrNatual = 333) es necesario reservar memoria para almacenar dicho valor. Reservar memoria significa que el sistema le asigna al puntero (ptrNatural) una dirección de memoria libre para su variable anónima donde podrá guardar el valor asignado (en este caso 333). Si la reserva de memoria no se realiza como paso previo a la asignación anterior, se producirá una ‘violación de acceso a memoria’ porque el puntero estará inicialmente apuntando a una dirección indeterminada o nula donde no es posible guardar el dato. El pseudolenguaje de las asignaturas de introducción a la programación consta de dos subalgoritmos predefinidos para la gestión de memoria dinámica: ASIGNAR y LIBERAR. Ambos tienen un único parámetro, de tipo puntero (a cualquier tipo de dato). El subalgoritmo ASIGNAR, como su propio nombre indica, asigna (reserva) memoria para la variable anónima del puntero cuyo identificador se le pasa como parámetro. No es necesario indicar el tamaño del dato que se reserva, ya que la función ASIGNAR se encarga de reservar tanto espacio en memoria como sea necesario para almacenar el tipo de dato base del puntero. Por su parte, el sugprograma LIBERAR libera la memoria asignada a la variable anónima del puntero cuyo identificador se Elementos de Programación Página 10 ETSI Telecomunicación Memoria Dinámica pasa como parámetro. Como ‘efecto lateral’ derivado de la llamada a la función LIBERAR para un determinado puntero, se le asigna a éste la constante NULO. Por ejemplo, VARIABLES R *ptr1, *ptr2 // 1) Declaración de punteros INICIO ASIGNAR(ptr1) // 2) Reserva memoria ASIGNAR(ptr2) // 2) Reserva memoria *ptr1 = 99.9 // 3) Asignación de valor a la variable anónima *ptr2 = -33.3 // 3) Asignación de valor a la variable anónima *ptr1 = *ptr2 // 4) Asigna –333 al dato apuntado por ptr1 ptr1 = ptr2 /* 5) Asignación de punteros. ¡Se pierde la referencia al dato previamente apuntado por ptr1!!!! */ LIBERAR(ptr2) /* 6) Se libera la memoria a la que apunta ptr2 Se pone ptr2 a NULO */ *ptr1 = -999 // ¡Error! La posición está liberada 1) 2) ptr1 3) ptr1 4) ptr1 ¿? ? ptr2 ptr2 99.9 ptr2 ¿? ? ptr1 -33.3 ptr2 -33.3 -33.3 6) 5) ptr1 ptr1 -33.3 ptr2 -33.3 ptr2 -33.3 ¿? Figura 2.8. Ilustración de un ejemplo de gestión dinámica de memoria Elementos de Programación Página 11 ETSI Telecomunicación Memoria Dinámica En la figura 2.8 se ilustra el funcionamiento del ejemplo anterior. Como puede comprobarse, hay que diferenciar claramente entre la asignación de punteros (operación 5 en el ejemplo) y la asignación de valores a las variables anónimas correspondientes a esos punteros (operaciones 3 y 4). Por otro lado, es importante señalar la necesidad de una correcta gestión de la memoria dinámica que evite dejar posiciones de memoria inaccesibles, como ocurre como resultado de la operación 5 en el ejemplo anterior, así como acceder a posiciones de memoria incorrectas (no asignadas al puntero), como ocurre en la operación 6 de dicho ejemplo. Es importante señalar aquí que la función ASIGNAR del pseudolenguaje se comporta de manera ‘ideal’, en el sentido en que se considera que la memoria tiene una capacidad teórica ‘infinita’ y siempre es posible reservar espacio para nuevos datos. En esto la semántica de la función ASIGNAR difiere de la de las funciones equivalentes en los lenguajes de programación ‘reales’ (incluyendo C/C++) donde sí existen las inevitables limitaciones de recursos de almacenamiento y no siempre se satisfacen las peticiones de reserva, por lo que dichas funciones necesitan devolver algún dato que indique si la reserva ha sido efectuada correctamente. 2.2.4. Punteros a registros Hasta ahora, en los apartados anteriores, en los que se ha definido el tipo puntero y sus operaciones básicas, no se ha mostrado la ‘verdadera utilidad’ de los punteros. En los ejemplos vistos hasta ahora, los punteros manejados han sido meros punteros a datos simples, como reales, naturales o caracteres. Aunque puede considerarse que se trata de una verdadera gestión dinámica de memoria, dado que es necesario asignar memoria antes de usar esos datos y liberarla cuando dejan de usarse, realmente no aporta grandes ventajas a las variables de tipo estático. La situación cambia cuando el tipo base de las variables puntero es un tipo complejo, como es el caso de los registros. En ese momento, se pone de manifiesto la verdadera ‘potencialidad’ de los punteros como elementos para una eficiente gestión de memoria en tiempo de ejecución, como se verá en el siguiente apartado sobre listas enlazadas. Como se ha comentado en los apartados anteriores, es posible declarar tipos y variables de tipo puntero de cualquier tipo base, ya sea éste simple o complejo. Un tipo especial, de gran utilidad, es el tipo puntero a registro. La declaración de un puntero a un registro sigue la sintaxis vista en los apartados anteriores. Por ejemplo, TIPOS REGISTRO TipoComplejo R real, imag FINREGISTRO TipoComplejo *TipoPtrComplejo Elementos de Programación Página 12 ETSI Telecomunicación Memoria Dinámica VAR TipoPtrComplejo ptr En este último ejemplo la variable ptr es un puntero a un dato de tipo registro, concretamente de tipo TipoComplejo. En el momento en que tenga lugar una asignación de memoria a ptr éste apuntará a una posición de memoria donde se almacenan los datos correspondientes a los dos campos que contiene el registro: real y imag. Esto puede representarse gráficamente como en la figura 2.9. ASIGNAR(ptr) ptr real imag ¿? ¿? Figura 2.9. Asignación de memoria para un puntero a registro Para acceder a los campos del registro puede combinarse el operador de indirección (*) con la notación punto, ayudándose de los paréntesis para tener que evitar establecer una precedencia en estos operadores. Por ejemplo, podría hacerse lo siguiente: (*ptr).real = 3.33 (*ptr).imag = -9.99 Además de esta notación, el pseudolenguaje introduce una nueva notación mediante el operador -> (podemos llamarlo ‘flechita’) como simplificación del uso combinado del operador de indirección y la notación punto. Basta con intercalar este nuevo operador entre el identificador del puntero y del campo para indicar que se accede a ese campo de la variable anónima de tipo registro. Por ejemplo, las dos operaciones equivalentes a las anteriores serían: ptr->real = 3.33 ptr->imag = -9.99 El resultado, de una u otra forma, sería el ilustrado en la figura 2.10. ptr->real = 3.33 ptr->imag = -9.99 ptr real 3.33 imag -9.99 Figura 2.10. Asignación a campos de registros apuntados por punteros Elementos de Programación Página 13 ETSI Telecomunicación Memoria Dinámica Como se ha comentado más arriba, el tipo base de un puntero puede ser tan complejo como se quiera, incluyendo registros o arrays que, a su vez, contienen otros registros o arrays y así sucesivamente. De la misma manera, es posible declarar registros que contienen campos de tipo puntero e, incluso, arrays de punteros. La utilidad y el uso de cada uno de estos tipos dependerán de la aplicación. Sirvan simplemente como muestra los siguientes ejemplos, cuyo funcionamiento se ilustra en la figura 2.11: TIPOS REGISTRO TpConstitucion // Registro con dos punteros R *peso, *altura FINREGISTRO C TpNombre[1..100] REGISTRO TpDatosPersonales TpNombre nombre TpConstitucion fisico /* Este campo es un registro con punteros */ N edad FINREGISTRO TpDatosPersonales *TpPtrDatosPersonales // Puntero a registro TpPtrDatosPersonales TpArrayPtrs[1..200] // Array de punteros TpNombre *TpPtrArray // Puntero a un array de caracteres VARIABLES TpArrayPtrs grupo TpPtrArray miNombre ... INICIO ... ASIGNAR(grupo[1]) /* Reservo memoria para el primer puntero del array /* ASIGNAR(grupo[1]->fisico.peso) /* Reservo memoria para el campo ‘peso’ del campo ‘fisico’ del primer puntero del array */ *(grupo[1]->fisico.peso) = 99.9 ASIGNAR(miNombre) // Reservo memoria para el array de caracteres *miNombre[1]=’A’ // O bien “(*miNombre)[1] = ‘A’” ... Elementos de Programación Página 14 ETSI Telecomunicación Memoria Dinámica grupo ... nombre ‘A’ fisico ‘n’ ... ‘e’ edad ‘z’ peso 33 altura 99,9 1.99 Figura 2.11. Estructuras complejas basadas en punteros 2.3. Operaciones sobre Listas Enlazadas Los punteros y la asignación dinámica de memoria permiten la construcción de estructuras enlazadas. Una estructura enlazada es una colección de nodos, cada uno de los cuales contiene uno o más punteros a otros nodos. Cada nodo es un registro en el que uno o más campos son punteros. La estructura enlazada más sencilla es la lista enlazada. Una lista enlazada consiste en un número de nodos, cada uno de los cuales contiene uno o varios datos, además de un puntero; el puntero permite que los nodos formen una estructura a modo de ‘cadena’ o de ‘lista enlazada’. En la figura 2.12 puede verse una representación que ilustra este concepto. Como puede observarse en esta figura, un puntero ‘externo’ apunta al primer nodo de la lista. El primer nodo es un registro que contiene (además de los campos ‘de datos’) un puntero que apunta al segundo nodo de la lista, y así sucesivamente. El puntero del último nodo apuntará a NULO, indicando que es el último nodo de la lista. Puntero externo Puntero a siguiente Campos de datos de un nodo (registro) Último nodo de la lista (siguIente es NULO) Figura 2.12. Esquema de lista enlazada con punteros Elementos de Programación Página 15 ETSI Telecomunicación Memoria Dinámica Para ilustrar la listas enlazadas se mostrará a continuación cómo definir, crear y manipular (insertar elementos, buscar, eliminar, etc.) una lista enlazada cuyos nodos contienen un único campo de datos (además de un puntero al siguiente nodo) de tipo carácter (C). Como se ha dicho anteriormente, los elementos de las listas (en este caso caracteres) se guardan como campos de registros que, además, contienen un puntero que sirve de enlace con el siguiente nodo de la lista que contiene el siguiente elemento. Para este ejemplo se va a declarar el tipo TpLista, que se definirá como un tipo puntero a un tipo registro TpNodo que contiene un campo de tipo carácter y un puntero de tipo TpLista. Nótese que la declaración de TpLista es ‘recursiva’, en el sentido de que necesita TpNodo para efectuarse, a la vez que TpNodo hace uso nuevamente de TpLista. Esta ‘licencia’ se le concede al pseudolenguaje de la asignatura para aumentar su expresividad y permitir este tipo de declaraciones en las que se declara un tipo en base a otro tipo que, a su vez, vuelve a recurrir al primero en su definición. TIPOS TpNodo *TpLista REGISTRO TpNodo C caracter // Elemento del nodo TpLista sig // Puntero al siguiente nodo FINREGISTRO Otra manera de hacer esto, completamente equivalente a la anterior, consiste en declarar primero el registro, declarando el puntero sig como un puntero al mismo TpNodo para, posteriormente, declarar TpLista. De esta manera, TIPOS REGISTRO TpNodo C caracter // Elemento del nodo TpNodo *sig // Puntero al siguiente nodo FINREGISTRO TpNodo *TpLista lista a b c Figura 2.13. Lista enlazada de caracteres En cualquier caso, se puede dibujar cada nodo de la lista enlazada como una ‘caja’ con dos campos: un carácter y un puntero. De esta forma, en la figura 2.13 se muestra una lista enlazada con tres caracteres donde lista es una variable de tipo TpLista (puntero ‘externo’) Elementos de Programación Página 16 ETSI Telecomunicación Memoria Dinámica que apunta al primer nodo de la lista enlazada. Para ello sería necesaria, en primer lugar, la siguiente declaración de datos: VARABLES TpLista lista Con esta estructura declarada, para almacenar una cadena de caracteres de cualquier tamaño bastará con ir leyendo (por ejemplo, por teclado) los caracteres uno a uno e ir creando para cada uno un nuevo nodo (variable anónima de tipo registro) donde almacenarlo, haciendo siempre que cada nodo enlace con el siguiente y que el último nodo de la lista apunte a NULO. Si la entrada se realiza por teclado, puede establecerse un convenio para indicar dónde acaba la introducción de caracteres. Por ejemplo, el retorno de carro (carácter 13 en la tabla ASCII) puede servir de indicador de fin de la entrada de datos. A partir de lo expuesto, pueden identificarse tres operaciones básicas sobre listas enlazadas: • Creación de una lista enlazada vacía. • Inserción de un nuevo nodo en la lista enlazada. • Eliminación de un nodo en la lista enlazada. Creación de una lista enlazada vacía El subalgoritmo para la creación de una lista enlazada vacía (sin ningún elemento inicialmente) es relativamente simple: basta con inicializar el puntero ‘externo’ a NULO. Este algoritmo recibirá como parámetro de ES un elemento de tipo TpLista. ALGORITMO CrearLista(ES TpLista lista) INICIO lista = NULO FIN CrearLista Inserción de un nuevo nodo en la lista enlazada En la operación de inserción de un nodo en una lista enlazada pueden distinguirse dos casos claramente diferenciados: • Inserción de un nodo al principio de una lista. • Inserción de un nodo después de un determinado nodo existente (por ejemplo, de manera que la lista se mantenga ordenada en orden ascendente). Elementos de Programación Página 17 ETSI Telecomunicación Memoria Dinámica 1) Inserción de un nuevo nodo al principio de una lista enlazada Para insertar un nuevo nodo al comienzo de una lista enlazada deben seguirse los siguientes pasos: 1. Crear un nodo para una variable anónima auxiliar, previamente declarada de tipo TpLista. ASIGNAR(ptr) // ptr antes declarado como ‘TpLista ptr’ 2. Asignar un valor (carácter) al campo de datos de la nueva variable anónima. ptr->caracter = ‘d’ 3. Hacer que el campo siguiente del nuevo nodo apunte donde actualmente apunte el puntero ‘externo’ de la lista, es decir, lista. ptr->sig = lista 4. Por último, debe actualizarse el puntero ‘externo’ de la lista, es decir, lista, para que apunte al primer nodo de la lista, que será el que se acaba de introducir. lista = ptr Todo estos pasos se dan en el siguiente subalgoritmo para insertar un nuevo elemento al principio de una lista de caracteres (cuyo funcionamiento se ilustra gráficamente en la figura 2.14), que recibirá como parámetros la lista (puntero de tipo TpLista) y el carácter a insertar en el nuevo nodo: ALGORITMO InsertarAlPrincipio(ES TpLista lista; E C car) VARIABLES TpLista ptr // Puntero auxiliar INICIO ASIGNAR(ptr) // Nuevo nodo ptr->caracter = car ptr->sig = lista lista = ptr FIN InsertarAlPrincipio Elementos de Programación Página 18 ETSI Telecomunicación Memoria Dinámica 1) y 2) ASIGNAR(ptr) ptr->caracter = ‘d’ ptr d 3) ptr->sig = lista 4) lista=ptr ptr a 4) 3) lista a b c Figura 2.14. Inserción de un nodo al principio de una lista enlazada de caracteres 2) Inserción de un nuevo nodo en una posición determinada de la lista Cuando se desea insertar un nuevo elemento en una lista enlazada ordenada, de manera que esta permanezca ordenada después de la inserción, los pasos a seguir serían los siguientes (para el caso de una lista de caracteres ordenados alfabéticamente en orden ascendente): 1. Si la lista está vacía o el primer elemento de la lista es mayor (alfabéticamente) que el elemento a insertar, se procede como en el caso anterior en el que se insertaba el elemento al principio de la lista. En otro caso, se sigue con los puntos siguientes. 2. Crear el nuevo nodo, reservando espacio para el mismo y almacenando en su campo de datos (caracter) el nuevo elemento que se desea insertar. Es aconsejable utilizar, para ello, una variable auxiliar de tipo TpLista, por ejemplo, la variable nuevoNodo. Opcionalmente, puede ponerse el puntero sig del nuevo nodo apuntando inicialmente a NULO. ASIGNAR(nuevoNodo) nuevoNodo->caracter = ‘d’ nuevoNodo->sig = NULO 3. Se utiliza una nueva variable auxiliar ptr, de tipo TpLista, para recorrer cada uno de los nodos de la lista hasta encontrar en lugar exacto donde debe insertarse el nuevo elemento; para ello, ptr estará siempre apuntando al nodo anterior al nodo cuyo elemento se está comparando con el elemento a insertar, de manera que una vez que se localice el lugar de inserción sea posible enlazar correctamente el nuevo nodo en la lista, manteniéndola ordenada. Esto se haría de la siguiente manera, para el ejemplo que se viene mostrando: Elementos de Programación Página 19 ETSI Telecomunicación Memoria Dinámica ptr = lista MIENTRAS (ptr->sig != NULO) Y (nuevoNodo->carácter > ptr->sig->caracter) HACER ptr = ptr->sig FINMIENTRAS nuevoNodo->sig = ptr->sig ptr->sig = nuevoNodo El algoritmo quedaría de la siguiente manera (gráficamente, en la figura 2.15): ALGORITMO InsertarOrdenada(ES TpLista lista; E C car) VARIABLES TpLista nuevoNodo, ptr INICIO SI lista == NULO O lista->carácter >= car ENTONCES InsertarAlPrincipio(lista, car) SINO ASIGNAR(nuevoNodo) nuevoNodo->caracter = car nuevoNodo->sig = NULO ptr = lista MIENTRAS ptr->sig != NULO Y (car > ptr->sig->caracter HACER ptr = ptr->sig FINMIENTRAS nuevoNodo->sig = ptr->sig ptr->sig = nuevoNodo FINSI FIN InsertarOrdenada Elementos de Programación Página 20 ETSI Telecomunicación Memoria Dinámica 2) ASIGNAR(nNodo) nNodo->caracter = ‘c’ nNodo->sig = NULO nNodo c 3) /* Localización del lugar de inserción (bucle)*/ ptr lista a b 3) /* Enlace de punteros para mantener la ordenación */ d nNodo c lista a b d ptr Figura 2.15. Inserción de un nodo en una lista ordenada Eliminación de un nodo de una lista enlazada Como en el caso de la inserción de nuevos nodos, en la operación de eliminar un nodo de una lista enlazada pueden también diferenciarse dos situaciones: • Borrar el primer nodo de la lista enlazada. • Borrar un determinado nodo de la lista enlazada. 1) Borrar el primer nodo de una lista enlazada Los pasos a seguir son los siguientes: 1. Declaración de un puntero auxiliar, que se inicializará de manera que apunte al primer nodo de la lista, que es el que se desea borrar. ptr = lista Elementos de Programación Página 21 ETSI Telecomunicación Memoria Dinámica 2. Se actualiza el puntero ‘externo’ de la lista (lista) para que apunte al segundo elemento de la misma, si existe, o bien a NULO. Para ello, basta con hacer: lista = lista->sig 3. Por último, es importante liberar la memoria correspondiente al nodo que se desea eliminar y al que está apuntando actualmente el puntero auxiliar ptr. LIBERAR(ptr) El algoritmo (ilustrado en la figura 2.16) quedaría de la siguiente manera: ALGORITMO EliminarPrimero(ES TpLista lista) VARIABLES TpLista ptr INICIO SI lista != NULO ENTONCES // Si no, no hay que eliminar nada ptr = lista lista = lista->sig LIBERAR(ptr) FINSI FIN EliminarPrimero 1) ptr lista a 2) y 3) b c b c ptr lista a Figura 2.16. Borrar el primer nodo de una lista enlazada 2) Borrar un determinado nodo de la lista enlazada Para borrar un nodo concreto de la lista enlazada ordenada (diferente del primero) son necesarios dos punteros auxiliares: uno apuntando al nodo a borrar, que tendrá el nombre de Elementos de Programación Página 22 ETSI Telecomunicación Memoria Dinámica ptr en el algoritmo que sigue, y otro puntero que apunte al nodo anterior al nodo que debe eliminarse, que se denominará ant. De este modo, la operación de ‘enlace’ de los nodos para ‘saltar’ el nodo eliminado será bastante simple, como se muestra en algoritmo BorrarOrdenada. La localización del nodo a borrar debe tener en cuenta el hecho de que la lista está ordenada y de que puede que el elemento no exista. Si la lista no estuviese ordenada, lo único que sería diferente en este algoritmo sería la forma de localizar el nodo a borrar. ALGORITMO BorrarOrdenada(ES TpLista lista; E C car) VARIABLES TpLista ptr, ant=NULO INICIO SI lista != NULO ENTONCES // Si no, no hay que hacer nada ptr = lista MIENTRAS ptr != NULO Y ptr->caracter != car HACER ant = ptr ptr = ptr->sig FINMIENTRAS SI ptr != NULO ENTONCES // Encontrado SI ant == NULO ENTONCES // Es el primer elemento lista = lista->sig SINO ant->sig = ptr->sig FINSI LIBERAR(ptr) FINSI FINSI FIN BorrarOrdenada En la figura 2.17 se muestra la operación de eliminación de un nodo (que contiene la letra ‘b’) en una lista enlazada ordenada. ant ptr a b lista c Figura 2.17. Eliminación de un nodo en una lista enlazada ordenada Elementos de Programación Página 23 ETSI Telecomunicación Memoria Dinámica Otras operaciones con listas enlazadas Puede completarse el conjunto de operaciones básicas con listas enlazadas incorporando dos nuevas operaciones, de gran utilidad: visualizar una lista escribiéndola, por ejemplo, en la salida estándar (pantalla) y eliminar todos los nodos de una lista. Esta última operación es necesaria en situaciones donde es preciso ‘eliminar’ una lista completa y liberar la memoria ocupada por todos sus nodos. A continuación se muestran ambos subalgoritmos. ALGORITMO EscribirLista(E TpLista lista) VARIABLES TpLista ptr = lista INICIO MIENTRAS ptr != NULO HACER Escribir(ptr->caracter) ptr = ptr->sig FINMIENTRAS FIN EscribirLista ALGORITMO BorrarLista(ES TpLista lista) VARIABLES TpLista ptr INICIO MIENTRAS lista != NULO HACER ptr = lista lista = lista->sig LIBERAR(ptr) FINMIENTRAS FIN BorrarLista 2.3.1. Otras clases de listas enlazadas Otras clases de listas enlazadas son las listas doblemente enlazadas. A diferencia de las listas enlazadas vistas en los apartados anteriores, las listas doblemente enlazadas contienen dos punteros en cada nodo, además de los campos de datos propiamente dichos. Uno de estos punteros, de manera similar a las listas enlazadas ‘simples’, apunta al siguiente nodo de la lista, mientras que el otro puntero apunta al nodo anterior, tal y como se ilustra en la figura 2.18. Disponer de dos enlaces en lugar de uno tiene varias ventajas: § La lista puede recorrerse en cualquier dirección. Esto simplifica la gestión de la lista, facilitando las inserciones y las eliminaciones. Elementos de Programación Página 24 ETSI Telecomunicación § Memoria Dinámica Mayor tolerancia a fallos. Se puede recorrer la lista tanto con los enlaces ‘hacia delante’ como con los enlaces ‘hacia atrás’, con lo que si algún enlace queda invalidado por algún error, se puede reconstruir la lista utilizando el otro enlace. El inconveniente principal de estas listas es que a la hora de realizar operaciones de inserción o eliminación de nodos es mayor el número de punteros que hay que ‘mover’ para mantener la lista correctamente enlazada. Eso requiere que la implementación de las operaciones deba ser más ‘cuidadosa’ que en el caso de las listas simples. Existe un caso especial de lista doblemente enlazada donde el puntero que apuntan al nodo anterior del primer nodo de la lista, en lugar de estar apuntando a NULO, apunta al último elemento de la lista, mientras que el puntero que apunta al nodo siguiente del último nodo de la lista, en lugar de apuntar a NULO, apunta al primer nodo de la lista. Este tipo de lista se denomina lista doblemente enlazada circular y permite, entre otras cosas, hacer recorridos completos de la lista sin necesidad de empezar en el primer nodo y sin tener que cambiar el ‘sentido’ del recorrido. También es posible implementar listas simples circulares, donde el campo sig del último nodo de la lista apunta al primer nodo de la lista. lista datos datos datos Figura 2.18. Lista doblemente enlazada La forma de construir una lista doblemente enlazada es similar a la de la lista enlazada simple, con la principal diferencia de que hay que mantener dos enlaces en lugar de uno. Por tanto, el registro que constituye cada nodo debe contener, además de los campos de datos, dos campos de tipo puntero a un nodo. Siguiendo con listas de caracteres, puede hacerse: TIPOS TpNodo *TpListaDoble REGISTRO TpNodo C caracter TipoListaDoble *ant, *sig FINREGISTRO Las operaciones básicas realizables con listas doblemente enlazadas coinciden con el caso de las listas enlazadas simples, y son: § Creación de una lista doblemente enlazada. § Inserción de un nodo en una lista doblemente enlazada. § Eliminación de un nodo en una lista doblemente enlazada. Elementos de Programación Página 25 ETSI Telecomunicación Memoria Dinámica Creación de una lista doblemente enlazada Una lista doblemente enlazada se crea de la misma forma que una lista enlazada ‘simple’, esto es, inicializando el puntero ‘externo’ de la lista a NULO para indicar que la lista está vacía. ALGORITMO CrearListaDoble (ES TpListaDoble listaDoble) INICIO listaDoble = NULO FIN CrearListaDoble Inserción de un nodo en una lista doblemente enlazada En la operación de insertar un nodo en una lista doblemente enlazada pueden distinguirse dos casos claramente diferenciados, como en el caso de las listas simples: § Inserción de un nodo al principio de la lista. § Inserción del nodo después de un determinado nodo existente (por ejemplo, para mantener la lista ordenada alfabéticamente en orden ascendente). 1) Inserción de un nodo al principio de una lista doblemente enlazada El algoritmo para insertar un nodo al comienzo de una lista doblemente enlazada que ha sido previamente creada (precondición necesaria) se muestra a continuación: ALGORITMO InsertaAlPrincipioDoble(ES TpListaDoble listaDoble; E C car) VARIABLES TpListaDoble ptr INICIO ASIGNAR(ptr) // Reserva de memoria ptr->caracter = car // Inicialización de campos del nuevo nodo ptr->ant = NULO ptr->sig = NULO SI (listaDoble != NULO) ENTONCES // Lista no vacía ptr->sig = listaDoble listaDoble->ant = ptr FINSI listaDoble = ptr // Nuevo primer nodo de la lista FIN InsertaAlPrincipioDoble 2) Inserción de un nodo en una posición determinada de una lista doblemente enlazada Para insertar un nodo en una lista doblemente enlazada ordenada, de manera que se mantenga ordenada después de la inserción, es necesario, en primer lugar, localizar la posición de la lista donde debe insertarse el nodo. Un algoritmo para insertar un nodo en una posición determinada de una lista enlazada doble es el siguiente: Elementos de Programación Página 26 ETSI Telecomunicación Memoria Dinámica ALGORITMO InsertarOrdenadaDoble (ES TpListaDoble listaDoble; E C car) VARIABLES TpListaDoble ptr, ant, nuevo INICIO SI listaDoble == NULO O listaDoble->caracter >= car) ENTONCES InsertaAlPrincipoDoble(listaDoble, car) SINO ASIGNAR(nuevo) ant = listaDoble ptr = listaDoble->sig // Apunta al segundo nodo o NULO MIENTRAS ptr != NULO Y car > ptr->caracter HACER ant = ptr ptr = ptr->sig FINMIENTRAS SI ptr == NULO ENTONCES // Se inserta al final nuevo->ant = ant ant->sig = nuevo SINO // Se inserta en medio de la lista nuevo->sig = ptr nuevo->ant = ant ant->sig = nuevo ptr->ant = nuevo FINSI FINSI FIN InsertarOrdenadaDoble En la figura 2.19 se muestra el funcionamiento de este algoritmo. Nótese el ‘movimiento’ de punteros necesario para mantener la lista enlazada. listaDoble ant ptr a c k v f Figura 2.19. Inserción en una lista doblemente enlazada Elementos de Programación Página 27 ETSI Telecomunicación Memoria Dinámica Eliminación de un nodo de una lista doblemente enlazada En la operación de eliminar un nodo de una lista doblemente enlazada pueden distinguirse dos casos claramente diferenciados, como en el caso de las listas simples: § Borrar el primer nodo de la lista. § Borrar un determinado nodo existente (por ejemplo, de forma que se mantenga la lista ordenada alfabéticamente en orden ascendente después de la operación de borrar). 1) Borrar el primer nodo de la lista doblemente enlazada ALGORITMO EliminarPrimeroDoble(ES TpListaDoble listaDoble) VARIABLES TpListaDoble ptr INICIO SI listaDoble != NULO ENTONCES // Si no, no hacemos nada ptr = listaDoble listaDoble = listaDoble->sig LIBERAR(ptr) FINSI FIN EliminarPrimeroDoble 2) Borrar un nodo de una posición determinada de la lista Para borrar un nodo de una posición determinada de la lista se necesitan dos punteros auxiliares: uno apuntando al nodo a borrar, que se denominará ptr en el algoritmo que se muestra a continuación; y otro que apunte al nodo anterior al nodo que se va a eliminar, que se llamará ant. Esto permitirá dejar la lista correctamente enlazada después de la liberación de la memoria correspondiente al nodo borrado. ALGORITMO BorrarOrdenadaDoble(ES TpListaDoble listaDoble; E C car) VARIABLES TpListaDoble ant, ptr INICIO ant = NULO ptr = listaDoble MIENTRAS ptr != NULO Y ptr->caracter != car HACER ant = ptr ptr = ptr->sig FINMIENTRAS SI ptr != NULO ENTONCES // Se ha encontrado el elemento SI ant == NULO ENTONCES // El elemento a borrar es el primero Elementos de Programación Página 28 ETSI Telecomunicación Memoria Dinámica listaDoble = listaDoble->sig SI listaDoble != NULO ENTONCES // Hay más nodos en la lista listaDoble->ant = NULO FINSI SINO SI ptr->sig == NULO ENTONCES // Borrar el último ant->sig = NULO SINO // El elemento a borrar está en medio de la lista ant->sig = ptr->sig ptr->sig->ant = ant FINSI LIBERAR(ptr) FINSI FIN BorrarOrdenadaDoble Elementos de Programación Página 29 ETSI Telecomunicación Memoria Dinámica 2.4. Ejemplo de Gestión de Memoria Dinámica en C/C++ En C/C++ la sintaxis de declaración de punteros es similar a la vista para el pseudolenguaje en los apartados anteriores: se usa el asterisco (*) a continuación del tipo base para indicar que se trata de un puntero a ese tipo base. Por otro lado, la operación de asignación de memoria se lleva a cabo mediante la función new. Para ello se asigna al puntero el resultado de ‘invocar’ la función new para el tipo base del mismo, como se muestra en el ejemplo a continuación. La función delete equivale al subalgoritmo ASIGNAR del pseudolenguaje para la liberación de memoria. El siguiente ejemplo se presenta como una simple muestra de la sintaxis de C/C++ en el manejo de memoria dinámica mediante el uso de punteros2: #include <iostream> #include <stdlib.h> struct TpNodo{ char car; TpNodo *sig; }; typedef TpNodo *TpPtrNodo; void main(){ TpPtrNodo ptr; ptr = new TpNodo; // Asignación de memoria ptr->car = 'a'; ptr->sig = NULL; // NULL equivale a NULO en el pseudolenguaje cout << ptr->car << endl; delete ptr; // Liberación de memoria apuntada por ptr system("pause"); } 2 En la asignatura de Laboratorio de Programación se presentarán estos conceptos de manera más extensa y detallada. Elementos de Programación Página 30 ETSI Telecomunicación Memoria Dinámica Ejercicios Propuestos 1) Dada la siguiente declaración de tipos adjunta: TIPOS Nodo *Lista REGISTRO Nodo Z elem Lista sig FINREGISTRO a) Diseña un algoritmo que imprima cada uno de los elementos de una lista. b) Diseña un algoritmo que devuelva una copia de una lista. c) Diseña un algoritmo que devuelva la longitud de una lista. d) Diseña un algoritmo que elimine el último elemento de una lista. e) Diseña un algoritmo que ordene los elementos de una lista. 2) Dada la declaración de tipos anterior y las variables l1 y l2 de tipo Lista, y siendo l1 la siguiente lista enlazada: l1 3 5 7 a) ¿Qué diferencia existe entre las dos ALGORITMO Ejemplo(E Lista p) instrucciones l2=l1 y Copiar(l1, l2), la INICIO cual duplica una lista? SI p !=NULO ENTONCES b) ¿Qué valor contiene l2->sig tras realizar la p->elem = 3 secuencia de instrucciones?: FINSI l2=l1; LIBERAR(l1) FIN Ejemplo c) Dado el algoritmo Ejemplo adjunto, donde p se pasa por valor, y el estado inicial de la lista l1, ¿qué ocurrirá a la lista apuntada por l1 tras la siguiente llamada: Ejemplo(l1)? 3) Dada la declaración de tipos adjunta: a) Diseña un algoritmo que inserte un elemento al inicio de una lista. b) Diseña un algoritmo que inserte un elemento al final de una lista. c) Diseña un algoritmo que elimine el primer elemento de una lista. d) Diseña un algoritmo que elimine el último elemento de una lista. e) Diseña un algoritmo que elimine los datos correspondientes a una persona con un nombre determinado de una lista. f) Diseña un algoritmo que devuelva una copia de una lista. g) Diseña un algoritmo que devuelva la longitud de una lista. h) Diseña un algoritmo que ordene una lista por el nombre de las personas. REGISTRO Persona C nombre[0..99] N teléfono FINREGISTRO Nodo *Lista REGISTRO Nodo Persona elem Lista sig FINREGISTRO Elementos de Programación Página 31 ETSI Telecomunicación Memoria Dinámica 4) Dadas las definiciones de tipos adjuntas, diseñar los siguientes algoritmos: a) Borrar todos los nodos de una lista enlazada y liberar Nodo *TipoPuntero toda la memoria. REGISTRO Nodo b) Duplicar una lista enlazada. N valor c) Borrar el nodo que contiene el máximo valor de una TipoPuntero sig lista enlazada. FINREGISTRO d) Intercambiar el valor n-ésimo con el m-ésimo de la lista. e) Concatenar dos listas enlazadas. f) Borrar el n-ésimo elemento de la lista. g) Diseñar el algoritmo con la siguiente cabecera: ALGORITMO TipoPuntero Busca(E TipoPuntero lista; E N elem) que devuelva el puntero al nodo que contiene el natural elem, si existe, y NULO en caso contrario. h) Dada la siguiente cabecera: ALGORITMO TipoPuntero InsBusca(ES TipoPuntero lista; E N elem) añada elem a lista, si no está en ella, y siempre devuelve un puntero al nodo que contiene elem. 5) Sea el tipo TipoLista adjunto. Resolver los siguientes apartados en base a este tipo. Nodo *TipoLista REGISTRO Nodo N valor TipoLista sig FINREGISTRO a) Diseña un algoritmo Purgar(...) que elimine todos los elementos duplicados una lista. b) Diseña un algoritmo BorrarUltimo(...) que elimine de una lista el último nodo que contiene la información k. c) Dadas dos listas enlazadas ordenadas, l1 y l2 de tipo TipoLista, escribe un algoritmo que mezcle las dos listas (pasadas como parámetros) en otra, de forma que esta última esté también ordenada. l1 y/o l2 pueden estar vacías. Resolverlo de dos formas: • Sin modificar las listas l1 y l2, creando una lista nueva. • Modificando las listas l1 y l2, sin reservar memoria adicional. 6) Hay muchas aplicaciones en las que se debe almacenar en la memoria un vector de grandes dimensiones. Si la mayoría de los elementos del vector son ceros, éste puede representarse más eficientemente utilizando una lista enlazada con punteros, en la que cada nodo es un registro con tres campos: el dato en esa posición si es distinto de cero, el índice de esa posición y un puntero al siguiente nodo. Por ejemplo, para un vector de longitud 7 la lista 25 1 14 5 representa el vector (25, 0, 0, 0, -14, 0, 0). Dado un tipo TVector de longitud constante Dim diseña algoritmos para: a) Calcular su Producto_Escalar, pasándole como entrada v1 y v2 de tipo TVector. Devolverá un vector nuevo. b) Insertar un valor teniendo en cuenta que si ya existe, se debe eliminar el valor anterior. Si la posición está fuera de rango, no hará nada. Y si es cero el valor a insertar no deberá quedar guardado en la lista ningún nodo de índice cero. Elementos de Programación Página 32 ETSI Telecomunicación Memoria Dinámica c) Obtener el valor en una determinada posición del vector. d) Mejorar los apartados anteriores redefiniendo el tipo TVector como un registro de dos campos, uno de ellos contendrá el vector en el formato anterior y el otro indicará la longitud, la cual ya no será necesariamente constante. 7) Una forma de almacenar un número natural de valor mayor que el permitido en una computadora es introducir cada dígito, de tipo natural, en un nodo de una lista enlazada. Por ejemplo, la siguiente lista representa al número 357: numero 3 5 7 a) Escribe un algoritmo que tome como parámetro un puntero a una lista enlazada que represente un número de la forma indicada y devuelva el número correspondiente en una variable de tipo natural. b) Diseña también un algoritmo que lea por teclado una sucesión de dígitos, de tipo carácter, y los introduzca como dígitos de tipo natural en una lista enlazada. c) Diseña dos algoritmos que realicen, respectivamente, la suma y producto de números representados de esta forma. 8) Un polinomio en x, de tipo entero, de grado arbitrario se puede representar mediante una lista enlazada, donde cada nodo contiene 1) el coeficiente, 2) el exponente de un término del polinomio, y 3) un puntero al siguiente nodo. Por ejemplo, el polinomio P(x)≡25x –14x5 se representaría como indica la figura adjunta: p 25 1 -14 5 a) Define el tipo TPolinomio que represente un polinomio mediante una lista enlazada con punteros. b) Escribe una función que evalúe un polinomio en un punto x: ALGORITMO Z Evaluar(E TPolinomio p; E Z valor) c) Escribe una función que obtenga el coeficiente del término de un determinado grado: ALGORITMO TPolinomio Coeficiente(E TPolinomio p1; E N grado) d) Escribe una función que sume 2 polinomios p1 y p2: ALGORITMO TPolinomio Sumar(E TPolinomio p1, p2) e) Escribe una función que realice la derivada de un polinomio P, con la siguiente cabecera: ALGORITMO TPolinomio Derivada(E TipoPolinomio p) 9) Supón que tienes diseñado el tipo conjunto (colección no ordenada de elementos distintos) mediante una lista enlazada dinámica con los tipos adjuntos: a) Queremos diseñar las operaciones intersección Elemento *Conjunto y unión de dos conjuntos: ¿Cómo serían las REGISTRO Elemento cabeceras de los dos algoritmos que realizaran Z elem estas tareas?. Al diseñar estos algoritmos, Conjunto sig ¿modificas alguno de los dos conjuntos con los FINREGISTRO que operas? ¿Se ve eso reflejado en las cabeceras? b) Diseña los algoritmos de acuerdo con las cabeceras del apartado a). Elementos de Programación Página 33 ETSI Telecomunicación Memoria Dinámica Bibliografía Aho, A. V., Hopcroft, J. E., & Ullman, J. D. (1988). Estructuras de Datos y Algoritmos. Addison Wesley Iberoamericana. Cerrada, J. A., & Collado, M. (1995). Programación I. UNED. Dale, N., & Weems, C. (1989). Pascal (2 ed.). McGraw Hill. Helman, P., Veroff, R., & Carrano, F. (1991). Intermediate Problem Solving and Data Structures. Walls & Mirrors (2 ed.). The Benjamin/Cummings Publishing. Horowitz, E., & Sahni, S. (1999). Fundamentals of Data Structures in Pascal (4 ed.). W. H. Freeman & Co. Joyanes, L. (1996). Fundamentos de Programación. Algoritmos y Estructuras de Datos. (2 ed.). McGraw Hill. Langsam, Y., Augenstein, M. J., & Tanenbaum, A. M. (1995). Data Structures using C and C++ (2 ed.). Prentice Hall. Weiss, M. A. (1995). Estructuras de Datos y Algoritmos. Addison Wesley Iberoamericana. Elementos de Programación Página 34