Tema 3. TADs. Acerca del tema de los TADs (TAD = estructura de datos + operaciones), hay algunas operaciones de los tipos pila, cola y lista que en los años anteriores no siempre se han utilizado de la manera correcta. Para la especificación de cada operación, véanse los anexos del tema 3 de teoría. La operación creap(p) NO ‘crea’ la pila (por ejemplo, si p se pasa como parámetro a una subrutina, ya existe...): simplemente la inicializa en función de cómo el TAD ha sido implementado (en caso de representación vectorial inicializa el campo tope a cero y en caso de representación dinámica inicializa la pila a la dirección nil). Lo mismo ocurre con creaq(q) y creal(l) que no crean sino inicializan respectivamente una cola q y una lista l (véanse las implementaciones). Un error frecuente, en el caso de diseño recursivo, ha sido inicializar el TAD cada vez que se llamaba recursivamente la subrutina. De esta manera, en cada llamada se vuelve a inicializar el TAD perdiendo lo que había (es como si cada vez que quisiéramos grabar un fichero en un disquete volviéramos a formatearlo: así borraríamos los datos contenidos en otros ficheros del disquete...). El problema es más grave, si la representación del TAD es dinámica (es decir, con punteros) ya que no devolveríamos a la zona heap de memoria el espacio ocupado por los nodos: simplemente perdemos su referencia y ellos se quedan perdidos en la memoria (ocupando memoria!). En función del problema que tenemos que resolver, la inicialización del TAD (es decir, la llamada al procedimiento crea) puede que tenga que hacerse ANTES de la invocación a la subrutina recursiva o en el mismo caso base. Se sugiere mirar los ejercicios y exámenes resueltos. En el caso del TAD pila, una operación que a veces no ha sido utilizada correctamente es la que permite desapilar el elemento que está en el tope de la pila. Este procedimiento desapila el tope SIN enseñar el elemento, como se comenta en el tercer corto del vídeo anexo. Si esta operación no sólo despilara sino que también permitiese conocer el elemento, el procedimiento tendría otra especificación y se declararía con dos parámetros (véase la pizarra del cuarto corto del vídeo) o como función que devuelve el elemento y lo desapila. En el corto se simulan las siguientes operaciones sobre una pila de CDs: a:=topep(p) desapilar(p) apilar(p,e) {y no desapilar(p,e) como si enseñase lo que hay en la pila y lo desapilara} En el caso del TAD cola, la operación que no siempre se ha utilizado correctamente es desencolar ya que (al igual que desapilar en la pila) desencola el primer elemento pero sin enseñarlo. La operación primero(q), al contrario, enseña lo que hay en la primera posición pero sin desencolar. En el segundo corto del vídeo se simulan tres de las operaciones sobre una cola de... estudiantes del grupo de FI del primer cuatrimestre: encolar(q,e) desencolar(q) e:=primero(q) El último TAD es la lista con punto de interés. En este TAD nuestro interés en dónde hacer una determinada operación puede cambiar (de una posición de la lista a otra) mientras que en la pila todas las operaciones se hacían por un extremo del TAD (política LIFO) y en una cola (política FIFO) las operaciones primero y desencolar por un extremo y la operación encolar por el otro. El punto de interés (o posición distinguida) en una lista l puede estar inicialmente al principio (invocando el procedimiento principio(l) ) o al final (invocando el procedimiento fin(l) ). Si nos hemos posicionado al principio, podemos movernos a la posición siguiente (procedimiento siguiente(l) ) sin la necesidad de eliminar ningún elemento (como teníamos que hacer en una pila o una cola). Las operaciones de este TAD que parece no han quedado del todo claras son: suprime(l) y sobretodo inserta(l,e). Como está explicado en la especificación de las operaciones de la lista con punto de interés (véanse los anexos del tema 3 de teoría) cuando invocamos el procedimiento suprime suprimimos el elemento que está en la posición distinguida y después, el punto de interés no cambia. Es como si estuviésemos viendo un partido de tenis y una persona nos quitase la visual. Después de ‘suprimir’ la persona (pidiéndole educadamente quitarse), nuestro interés queda en el mismo punto ya que seguiríamos pidiendo que se quitase a la persona que se pusiese en la misma posición (distinguida) del que se quitó. En el tercer corto del vídeo se simulan algunas operaciones sobre una lista (de estudiantes del grupo de FI del primer cuatrimestre): principio(l) siguiente(l) e:=recupera(l) suprime(l) Como se puede apreciar, después de suprimir el segundo elemento (es decir, la alumna), nuestro interés se queda en la segunda posición. Acerca de la operación inserta(l,e) que nos permite insertar un cierto elemento en una cierta posición distinguida de la lista, es como si tuviéramos manía al elemento (persona) que está en la posición y siempre quisiéramos insertar ANTES de él. Es decir, el punto de interés después de insertar un nuevo elemento cambia en una posición (de i a i+1, véase la especificación de la operación) aunque seguimos teniéndole manía al elemento delante del cual hemos insertado ... En el quinto corto del vídeo se simulan algunas operaciones sobre una lista (siempre de estudiantes del grupo de FI del primer cuatrimestre): principio(l) siguiente(l) inserta(l,e) fin(l) inserta(l,e) Después de insertar el elemento (es decir, la alumna) en la segunda posición, es decir, delante del alumno que ESTABA en la segunda posición, nuestro interés se queda en ... fastidiar al pobre alumno (nada personal!) y, si decidiéramos insertar otra persona (otra invocación a la operación inserta) seguiría siendo antes de él. Afortunadamente (por él...) decidimos cambiar nuestro interés por el final, dónde insertamos otro elemento (alumna). Por último, hay que mencionar los punteros, utilizados para la representación dinámica de los TADs. El error más común ha sido por cada variable auxiliar aux puntero a un nodo (o a un entero, a un real, etc.) pedir dinámicamente espacio en memoria invocando SIEMPRE la primitiva new. Ejemplo. Var aux, aux2:^integer; new(aux2) ; aux2^ :=2 ; aux :=aux2 ; Si queremos, por ejemplo, utilizar la variable aux para hacer una copia de la dirección de memoria del entero (para el cual se ha pedido espacio en memoria invocando la primitiva new: new(aux2) pide espacio para un entero y escribe la dirección de memoria en aux2) no debemos pedir espacio para otro entero (es decir, no debemos invocar new(aux) , simplemente hacemos la asignación aux:= aux2 ). Cuidado también cuando en una guarda tenemos algo como (aux^. sig<>nil) con aux puntero a un nodo. Hay que estar seguros que aux<>nil, es decir, que existe el nodo del cual queremos mirar el campo sig (¿cómo podríamos mirar un campo de un nodo que no existe?) Agradecimientos: a los alumnos del grupo de FI del 1er cuatrimestre / actores de los cortos!