07. TAD en memoria dinámica

Última modificación por Mario Gorga López el 2024/04/09 08:57

Objetivos

Los objetivos de este módulo son:

  • Repaso del concepto de tipo abstracto de datos (TAD).
  • Repaso del concepto de memoria dinámica y punteros.
  • Presentación de operaciones y primitivas para manipular punteros.
  • Mostrar una implementación de los TAD pilas, colas y listas utilizando memoria dinámica.

Introducción

Recordamos que un TAD queda definido a través de un conjunto de valores o dominio y una serie de operaciones que se le pueden aplicar. La implementación de las operaciones no se especifica ya que sólo se especifica como se comportan las operaciones. Esto significa que un mismo TAD puede implementarse de formas diferentes.

Los TAD tratados en el módulo de Fundamentos de Programación han sido: pilas, colas y listas. Se ha visto también una implementación básica del tipo y las operaciones utilizando vectores.

La implementación utilizando vectores tiene algunas desventajas que se enumeran a continuación:

  • En la implementación propuesta utilizando vectores, tanto para la inserción como en el borrado de elementos, se tienen de desplazar todos los elementos hacia la derecha para generar un nuevo espacio, o hacia la izquierda para rellenar el recientemente eliminado respectivamente.
  • No se utiliza la memoria de forma óptima ya que se mantiene reservada una cantidad de espacio durante toda la ejecución. Por otro lado si el tamaño reservado es insuficiente, no es posible modificarlo.

Como alternativa de mejora a la implementación con vectores, en este módulo se presenta una propuesta de implementación de los TAD pila, cola y lista utilizando memoria dinámica.

El espacio necesario para almacenar una estructura dinámica se reserva en tiempo de ejecución. Esto tiene la ventaja de hacer un uso óptimo de la memoria pero por otro lado podría suceder que no hubiera memoria disponible suficiente para continuar la ejecución.

Para trabajar con la memoria dinámica será necesaria la utilización de un tipo de datos denominado puntero.

En éste módulo recordaremos la definición del tipo de datos puntero, trabajaremos con memoria dinámica y veremos también una nueva propuesta de implementación de los TAD pila, cola y lista aplicando todos estos conceptos.

Tipo de datos puntero

Definición

Una variable x de tipo de datos puntero a T, es un apuntador a un objeto de tipo de datos T. A través de la variable x es posible acceder a un objeto de tipo T.

Un puntero es una variable cuyo valor es la dirección de memoria de comienzo del objeto apuntado.

Por ejemplo si la variable x es de tipo puntero a entero, entonces la variable x apunta a un objeto de tipo entero. En la Figura 1 se muestra la representación conceptual de apuntador, donde la variable x apunta a un objeto de tipo entero con valor 10, y la representación física, donde la variable x almacena el valor 0136 que corresponde a la dirección de memoria del objeto apuntado con valor 10.

07_01.svg

Figura 1. Ejemplo de un puntero a entero (Imagen extraída de [1])

[Ejemplo 07_01]

type
    tExample = record
        e: integer;
    end record

    tPointerExample = pointer to tExample;
end type


typedef struct {
   int e;
} tExample;

typedef tExample *tPointerExample;

Operaciones y primitivas de punteros

A continuación se definen las operaciones para obtener (getMemory) y liberar (freeMemory) espacio de memoria en forma dinámica, así como la primitiva utilizada para acceder a objetos apuntados, así como otras primitivas necesarias para la manipulación de punteros.

El valor null es un valor especial que indica que el puntero no apunta a ningún sitio. Si la función getMemory no logra obtener el espacio solicitado retorna el valor null. La función freeMemory además de liberar el espacio en memoria apuntado por el puntero parámetro, asigna el valor null al puntero recibido como parámetro.

Obtener espacio

Esta función que llamaremos getMemory, se utiliza para solicitar al sistema operativo una cantidad determinada de espacio en memoria durante la ejecución de un programa. Si la función getMemory no logra obtener el espacio solicitado retorna el valor null.

[Ejemplo 07_02]

var
    p: tPointerExample;
end var

p:= getMemory();

if p = null then
    writeString("Error: not enough free memory");
end if


#include <malloc.h>

tPointerExample p;
p = (tPointerExample *)malloc(sizeof(tPointerExample));
if (p==NULL) {
    printf("\n Error: not enough free memory\n");
}

Liberar espacio

Esta función que llamaremos freeMemory, se utiliza para liberar espacio de memoria previamente reservada durante la ejecución del programa. La función freeMemory además de liberar el espacio en memoria apuntado por el puntero parámetro, asigna el valor null al puntero recibido como parámetro.

[Ejemplo 07_03]

var
    p: tPointerExample;
end var

{ ... }
{ p is a punter to previously allocated memory }
freeMemory(p);


#include <malloc.h>
tPointerExample p;

/* ... */
/* p is a pointer to previously allocated memory */
free(p);

Liberar espacio ocupado indirectamente

Para asegurarnos que no se generan sitios de memoria sin referencia o retales (se sugiere referirse a [1], página 49: Referencias colgadas), se define esta función que llamaremos destroy. Esta función no sólo libera el espacio del objeto parámetro, sino que también si este objeto a su vez es una estructura con punteros que apuntan a otros objetos, el espacio de estos últimos también es liberado y así sucesivamente en forma recursiva.

De esta forma se obtiene independencia de la implementación del TAD. La implementación de la función destroy es dependiente de la implementación del TAD elegida y de la estructura de datos concreta. Por este motivo solo se muestra su uso en lenguaje algorítmico.

[Ejemplo 07_04]

var
    e: tExample;
end var

{ ... }
{ e is an object previously initialized }
destroy(e);

Duplicar espacio

En una asignación clásica de variables, por ejemplo: dos variables enteras a y b, al efectuar la asignación a:= b , tendremos que el valor de la variable b se ha copiado en la variable a. Ambas variables ocupan lugares de memoria diferentes y son independientes.

El resultado de la asignación de un puntero p1 a otro puntero p2, es decir p2 := p1, da como resultado que el valor contenido en el puntero origen (p1) se copia en el puntero destino (p2). Después de esta asignación ambos punteros tienen el mismo valor, lo que significa que ambos punteros apuntan al mismo objeto o lugar de memoria. Por lo tanto cualquier modificación de esta área de memoria realizada a través de un puntero, también será visible si accedemos a través del otro puntero.

Si lo que se desea es realizar una copia de un objeto e1 en otro objeto e2, introducimos una nueva operación que llamaremos duplicate y que realiza una copia exacta de un objeto sobre otro del mismo TAD. Esta función retorna un objeto copia exacta del objeto original. Esta función no sólo copia el valor del objeto parámetro, sino que también si este objeto a su vez es una estructura con punteros que apuntan a otros objetos, el espacio de estos últimos también es duplicado y así sucesivamente en forma recursiva.

De esta forma se obtiene independencia de la implementación del TAD. La copia de objetos se realiza con la función duplicate sin tener en cuenta la implementación concreta del TAD. La implementación de la función duplicate es dependiente de la implementación del TAD elegida y de la estructura de datos concreta. Por este motivo solo se muestra su uso en lenguaje algorítmico.

[Ejemplo 07_05]

var
    e1, e2: tExample;
end var

{ ... }
{ e1 is an object previously initialized }
duplicate(e2, e1);

Comparación de objetos

La comparación de dos objetos apuntados por dos punteros p1 y p2, no se puede reducir únicamente a la comparación de dos objetos e1 y e2. Si estos objetos son una estructura con punteros que apuntan a otros objetos, estos últimos quedarían fuera de la comparación. Por lo tanto, al igual que en las operaciones de duplicate y destroy, el algoritmo que implementa esta operación ha de contemplar esta recursividad. Esto significa que no alcanza con aplicar el operador de igualdad: e1 = e2 para realizar la operación.

Por este motivo se introduce la operación equal que realiza una comparación recursiva completa de los objetos pasados como parámetro. Para mayor información referirse a [1], página 46: 2) Comparación.

De esta forma, la función equal independiza al código de la implementación concreta del TAD y la estructura. Sin embargo, la implementación de la función equal es dependiente de la implementación del TAD elegida y de la estructura de datos concreta. Por este motivo solo se muestra su uso en lenguaje algorítmico.

[Ejemplo 07_06]

var
    e1, e2: tExample;
end var

{ ... }
{ e1 and e2 are objects previously initialized }
if equal(e1, e2) then
    { ... }
end if

Implementación de los TAD utilizando memoria dinámica

En la implementación con vectores todos los elementos del TAD se encontraban en posiciones consecutivas en memoria. En la implementación con memoria dinámica no necesariamente los elementos estan en posiciones consecutivas de memoria. Esto sucede por el hecho de que cada posición se va reservando en forma dinámica durante la ejecución, por lo que el gestor de memoria del sistema operativo retorna un puntero al primer sitio que encuentra que tiene el tamaño solicitado.

Para encadenar los diferentes elementos del TAD se utilizan los punteros. Cada elemento del TAD tiene siempre al menos un campo que almacena un puntero al siguiente elemento en el TAD. Denominaremos nodo a cada elemento de una estructura implementada con punteros.

A continuación se presenta la implementación de los TAD pila, cola y lista utilizando punteros, es decir memoria dinámica. Para esta implementación con punteros las funciones fullStack, fullQueue and fullList que se utilizaban en la implementación con vectores, ya no se utilizan dado que no se establece un límite máximo de elementos.

Pila (tStack)

Recordamos que el TAD pila es una secuencia lineal de elementos donde sólo se puede insertar y eliminar elementos por la cima de la pila. En la Tabla 1 se listan las operaciones que definen este TAD.

Tabla 1. Lista de operaciones sobre el TAD stack

OperaciónDescripción
 createStack Crea una pila vacía
 push Empila un elemento en la pila
 pop Desempila un elemento de la pila
 top Devuelve el elemento situado en el tope de la pila
 emptyStack Devuelve true si la pila está vacía, false en caso contrario

Implementación del tipo

La implementación propuesta se basa en punteros. El primer elemento de la pila, es decir el que se encuentra en la cima es apuntado por el puntero first, excepto cuando la pila está vacía, en cuyo caso tiene el valor null.

En la Figura 2 se muestra a la izquierda la representación gráfica de una pila, donde el elemento en la cima (top) es el vn. A la derecha se muestra la representación de la pila utilizando punteros. El elemento apuntado por el puntero first es el que está en la cima de la pila. Cada elemento tiene a su vez un puntero al siguiente elemento en la pila. El elemento que está en la base de la pila (el primero que fue insertado) tiene el puntero al siguiente con valor null.

07_02.svg

Figura 2. Representación de una pila (izquierda) e implementación mediante punteros (derecha). Imagen extraída y adaptada de [1].

[Ejemplo 07_07]

type
    tNode = record
        e: elem;
        next: pointer to tNode;
    end record

    tStack = record
        first: pointer to tNode;
    end record
end type


typedef struct tNode {
    elem e;
   struct tNode *next;
} tNode;

typedef struct {
    tNode *first;
} tStack;

Implementación de las operaciones

Tanto en la operación de apilar (push) como en la operación de desapilar (pop), la manipulación de elementos se realiza por el extremo final de la secuencia que es también el tope de la pila.

07_03.svg

Figura 3. Operación desapilar (pop). Imagen extraída y adaptada de [1]

[Ejemplo 07_08]

function createStack(): tStack

    var
        s: tStack;
    end var

    s.first := null;
    retorna s;

end function

action push(inout s: tStack; in e: elem)

    var
        tmp: pointer to tNode;
    end var

    tmp := getMemory();

    if tmp = null then
        writeString("error: not enough free memory");
    else
        duplicate(tmp^.e, e);
        tmp^.next := s.first;
        s.first := tmp;
    end if

end action

action pop(inout s: tStack)

    var
        tmp: pointer to tNode;
    end var

    if s.first = null then
        writeString("error: empty stack");
    else
        tmp := s.first;
        s.first := (s.first)^.next;
        destroy(tmp^.e);
        freeMemory(tmp);
    end if

end action

function top(s: tStack): elem

    var
        e: elem;
    end var

    if s.first = null then
        writeString("error: empty stack");
    else
        duplicate(e, (s.first)^.e);
    end if

    return e;

end function

function emptyStack(s: tStack): boolean
    return (s.first = null);
end function

typedef enum {FALSE, TRUE} boolean;

void createStack(stack *s) {
    s->first = NULL;
}

void push(stack *s, elem e) {

    tNode *tmp;
    tmp = getMemory();
   if (tmp == NULL) {
        printf("\n Error: not enough free memory\n");
    } else {
        duplicate(tmp->e, e);
        tmp->next = s->first;
        s->first = tmp;
    }
}

void pop(stack *s) {

    tNode *tmp;
   if (s->first == NULL) {
        printf("\n Error: empty stack\n");
    } else {
        tmp = s->first;
        s->first = s->first->next;
        destroy(tmp->e);
        freeMemory(tmp);
    }
}

elem top(stack s) {

    elem e;
   if (s.first == NULL) {
        printf("\n Error: empty stack\n");
    } else {
        duplicate (e, s.first->e);
    }

   return e;
}

boolean emptyStack(stack s) {
   return (s.first == NULL);
}

Ejercicio ejemplo

En este ejercicio ejemplo extendemos el TAD pila con una nueva operación: popn. Esta operación retorna la pila sin los n primeros elementos, siendo n un número entero mayor que cero. Si la pila contiene menos de n elementos, entonces devuelve error.

Para resolverlo utilizamos la definición del tipo pila tal como se define en este módulo. Se trabaja directamente con la implementación del tipo pila. No se utiliza ninguna de las operaciones básicas del tipo (pop, top, push, ...).

[Ejemplo 07_09]

action popn(inout s: tStack; in n: integer)

    var
        tmp : pointer to tNode;
        count: integer;
    end var

    if s.first = null then
        writeString("error: empty stack");
    else

        count := 0;
        while ((s.first)^.next ≠ NULL and count<n) do
            tmp := s.first;
            s.first := (s.first)^.next;
            destroy(tmp^.e);
            freeMemory(tmp);
            count := count+1;
        end while

        if count < n then
            writeString("error: stack with less than n elements");
        end if
    end if

end action


void popn(stack *s, int n) {

    tNode *tmp;
   int count;

   if (s->first == NULL) {
        printf("\n Error: empty stack\n");
    } else {

        count = 0;
       while (s->first->next != NULL && count < n) {
            tmp = s->first;
            s->first = s->first->next;
            destroy(tmp->e);
            freeMemory(tmp);
            count = count+1;
        }

       if (count < n) {
            printf("\n Error: stack with less than n elements\n");
        }
    }
}


Cola (tQueue)

Recordamos que una cola (queue) es una secuencia lineal de elementos con la característica que los elementos se insertan por el final de la cola y se extraen por el principio de la cola. En la Tabla 2 se listan las operaciones que definen este TAD.

Tabla 2.  Lista de operaciones sobre el TAD queue

OperaciónDescripción
 createQueueCrea una cola vacía
 enqueue Inserta un elemento en la cola
 dequeue Elimina un elemento de la cola
 head Devuelve el elemento situado al comienzo de la cola
 emptyQueue Devuelve true si la cola está vacía, false en caso contrario

Implementación del tipo

La implementación propuesta se basa en punteros. El primer elemento de la cola, que es por donde se desencolan los elementos, es apuntado por el puntero first, y el final de la cola, que es el extremo por donde se encolan los elementos, es apuntado por el puntero last.

En la Figura 4 se muestra a la izquierda la representación gráfica de una cola, donde el primer elemento (head) es v1. A la derecha se muestra la representación de la cola utilizando punteros. El puntero first, indica el primer elemento (el siguiente a ser extraído), mientras que el elemento apuntado por last indica el último insertado. Cada elemento tiene a su vez un puntero al siguiente elemento en la cola. El último elemento de la cola tiene el puntero al siguiente con valor null.

07_04.svg

Figura 4. Representación de una cola (izquierda) e implementación mediante punteros (derecha). Imagen extraída y adaptada de [1]

[Ejemplo 07_10]

type
    tNode = record
        e: elem;
        next: pointer to tNode;
    end record

    tQueue = record
        first: pointer to tNode;
        last: pointer to tNode;
    end record
end type


typedef struct tNode {
    elem e;
   struct tNode *next;
} tNode;

typedef struct {
    tNode *first;
    tNode *last;
} tQueue;

Implementación de las operaciones

Los elementos se encolan (operación enqueue) por el final de la cola y se desencolan (operación dequeue) por el principio de la cola (posición 1). Únicamente se tienen que actualizar los punteros al primer elemento (en caso de eliminar un elemento) o el puntero al final de la cola (en caso de insertar un elemento).

07_05.svg

Figura 5. Operación encolar (enqueue). Imagen extraída y adaptada de [1]

En la Figura 5 se muestra gráficamente el algoritmo seguido para encolar un elemento en la cola (enqueue). En la cola los elementos se insertar por el extremo final. El extremo final de una cola está apuntado por el puntero last. Para insertar un nuevo elemento, primero se ha de obtener espacio para el nuevo nodo, tmp en el dibujo, y a continuación se deben actualizar el valor de los punteros de forma de enlazar tmp a la cola. Por último se debe actualizar el puntero last, que apunta ahora al nuevo elemento, es decir el último insertado.

[Ejemplo 07_11]

function createQueue(): tQueue

    var
        q: tQueue;
    end var

    q.first := null;
    q.last := null;
    return q;

end function

action enqueue(inout q: tQueue; in e: elem)

    var
        tmp: pointer to tNode;
    end var

    tmp := getMemory();

    if tmp = null then
        writeString("error: not enough free memory");
    else
        duplicate(tmp^.e, e);
        tmp^.next := null;

        if q.first = null then
            { empty queue }
            q.first := tmp;
        else
            (q.last)^.next := tmp;
        end if
            q.last := tmp;
    end if

end action

action dequeue(inout q: tQueue)

    var
        tmp: pointer to tNode
    end var

    if q.first = null then
        writeString("error: empty queue");
    else
        tmp := q.first;
        q.first := (q.first)^.next;

        if q.first = null then
            q.last := null;
        enf if

        destroy(tmp^.e);
        freeMemory(tmp);
    end if

end action

function head(q: tQueue): elem

    var
        e: elem
    end var

    if q.first = null then
        writeString("error: empty queue");
    else
        duplicate(e, (q.first)^.e);
    end if

    return e;

end function

function emptyQueue(q: tQueue): boolean
    return (q.first = null);
end function

typedef enum {FALSE, TRUE} boolean;

void createQueue(tQueue *q) {

    q->first = NULL;
    q->last = NULL;
}

void enqueue(tQueue *q, elem e) {
    tNode *tmp;
    tmp = getMemory();
   if (tmp == NULL) {
        printf("\n Error: not enough free memory\n");
    } else {

        duplicate (tmp->e, e);
        tmp->next = NULL;
       if (q->first == NULL) {
           /* empty queue */
            q->first = tmp;
        } else {
            q->last->next = tmp;
        }

        q->last = tmp;
    }
}

void dequeue(tQueue *q) {

    tNode *tmp;
   if (q->first == NULL) {
        printf("\n Error: empty queue\n");
    } else {

        tmp = q->first;
        q->first = q->first->next;
       if (q->first == NULL) {
            q->last = NULL;
        }

        destroy(tmp->e);
        freeMemory(tmp);
    }
}

elem head(tQueue q) {

    elem e;
   if (q.first == NULL) {
        printf("\n Error: empty queue\n");
    } else {
        duplicate(e, q.first->e);
    }
   return e;
}

boolean emptyQueue(tQueue q) {
   return (q.first == NULL);
}

Ejercicio ejemplo

En este ejercicio ejemplo extendemos el TAD cola con una nueva operación: swapFirst2. Esta operación retorna la cola con los dos primeros elementos intercambiados de posición. Si la cola contiene menos de 2 elementos, entonces devuelve error.

Para resolverlo utilizamos la definición del tipo cola tal como se define en este módulo. Se trabaja directamente con la implementación del tipo cola. No se utiliza ninguna de las operaciones básicas del tipo (enqueue, dequeue, head, ...).

[Ejemplo 07_12]

action swapfirst2(inout q: tQueue)

    var
        tmp: pointer to tNode;
    end var

    if q.first = null then
        writeString("error: empty queue");
    else

        if (q.first)^.next = null then
            writeString("error: queue with only one element");
        else
            tmp := q.first
            q.first := (q.first)^.next;
            tmp^.next:= (tmp.next)^.next;
            (q.first)^.next := tmp;
        end if

    end if

end action


void swapFirst2(tQueue *q) {

    tNode *tmp;
   if (q->first == NULL) {
        printf("\n Error: empty queue \n");
    } else {
       if (q->first->next == NULL) {
            printf("\n Error: queue with only one element \n");
        } else {
            tmp = q->first;
            q->first = q->first->next;
            tmp->next = tmp->next->next;
            q->first->next = tmp;
        }
    }
}

Lista (tList)

Recordamos que una lista es una secuencia lineal de elementos con la característica que es posible insertar, eliminar y consultar elementos en cualquier posición de la secuencia. En la Tabla 3 se listan las operaciones que definen este TAD.

Tabla 3. Lista de operaciones sobre el TAD list.

OperaciónDescripción
 createList crea una lista vacía
 insert inserta un elemento en la lista
 delete elimina un elemento de la lista
 get devuelve el elemento situado en una posición dada de la lista
 end devuelve true si el puntero pasado como parámetro apunta a nulo, false en caso contrario
 emptyList devuelve true si la lista está vacía, false en caso contrario

Implementación del tipo

La implementación propuesta se basa en punteros. Para ello se dispone de un puntero al primer elemento de la lista que denominamos first. Dado que tanto la inserción como el borrado de elementos se pueden realizar en cualquier posición de la lista, será necesario desplazar el puntero del comienzo hasta la posición deseada. Sin embargo, no se debe perder nunca el puntero al comienzo de la lista ya que los desplazamientos son solo en un sentido, por lo que si se pierde esta referencia no será posible nunca más volver al comienzo de la lista.

Esta es una propuesta de implementación alternativa a la indicada en los apuntes de la bibliografía. Las diferencias principales son que en la implementación de los apuntes se mantiene un puntero al elemento anterior al actual (punto de interés) y un elemento fantasma. Este elemento fantasma existe para que la utilización y mantenimiento de este puntero al elemento anterior sea homogénea no importando si el elemento actual o punto de interés es el primer elemento. De esta forma se facilita su implementación y no hay que diferenciar el primer caso en operaciones como la inserción y el borrado de elementos de la implementación aquí propuesta.

Asimismo, en la implementación de los apuntes la inserción y borrado se realizan a partir de la posición del elemento actual mientras que en la implementación aquí propuesta la inserción y borrado se realizan en la posición explícita indicada como parámetro de la operación.

En la Figura 6 se muestra a la izquierda la representación gráfica de una lista. A la derecha se muestra la representación de la lista utilizando punteros. El puntero first, apunta al primer elemento de la lista. Cada elemento ocupa una posición (ordinal en la lista) indicada en la figura como index. Cada elemento tiene a su vez un puntero al siguiente elemento en la lista. El último elemento de la lista tiene el puntero al siguiente con valor null.

07_06.svg

Figura 6. Representación de una lista (izquierda) e implementación mediante punteros (derecha).

[Ejemplo 07_13]

type
    tNode = record
        e: elem;
        next: pointer to tNode;
    end record

    tList = record
        first: pointer to tNode;
    end record
end type


typedef struct tNode {
    elem e;
   struct tNode *next;
} tNode;

typedef struct {
    tNode *first;
} tList;

Implementación de las operaciones

Tanto la inserción de elementos (operación insert) como el borrado (operación delete) se pueden realizar en cualquier posición de la lista. Cada vez que se inserta un nuevo elemento, unicamente habrá que encontrar la posición correspondiente y modificar el encadenado de punteros. Lo mismo sucede al eliminar un elemento de la lista: se deben actualizar los punteros al siguiente del elemento anterior al eliminado.

07_07.svg

Figura 7. Operación eliminar (delete).

En la Figura 7 se muestra gráficamente el algoritmo seguido para eliminar un elemento de la lista (delete). En el ejemplo de la figura se elimina el elemento con ordinal 2, es decir el que está en la segunda posición comenzando por el elemento apuntado por first. Se recorre primero la lista buscando la posición index=2. Se mantiene un puntero al elemento anterior (prev) para actualizar luego los punteros del elemento anterior al elemento siguiente al eliminado. La memoria ocupada por el elemento a eliminar (tmp) es liberada.

[Ejemplo 07_14]

function createList(): tList
    l.first := null;
end function

action insert(inout l: tList; in e: elem; in index: integer)

    var
        tmp: pointer to tNode;
        prev: pointer to tNode;
    end var

    tmp := getMemory();
    si tmp = null then
        writeString("error: not enough free memory");
    else

        duplicate(tmp^.e, e);
        if index = 1 then
            { no previous element }
            tmp^.next := l.first;
            l.first := tmp;
        else

            prev := getNth(l, index-1);
            if prev ≠ null then
                { standard case }
                tmp^.next := prev^.next;
                prev^.next := tmp;
            else
                writeString("error: not enough elements");
            end if

        end if
    end if

end action

action delete(inout l: tList; in index: integer)

    var
        tmp: pointer to tNode;
        prev: pointer to tNode;
    end var

    if index = 1 then

        { no previous element }
        tmp := l.first;
        if tmp = null then
            writeString("error: empty list");
        else
            l.first := tmp^.next;
            destroy(tmp^.e);
            freeMemory(tmp);
        end if

    else

        prev := getNth(l, index-1);
        if prev ≠ null then
            { standard case }
            tmp := prev^.next;

            if tmp = null then
                writeString("error: index position doesn't exist");
            else
                prev^.next := tmp^.next;
                destroy(tmp^.e);
                freeMemory(tmp);
            end if

        else
            writeString("error: index position doesn't exist");
        end if

    end if
end action

function get(l: tList; index: integer): elem

    var
        e: elem;
        curr: pointer to tNode;
    end var

    curr := getNth(l, index);
    if curr = null then
        writeString("error: index position doesn't exist");
    else
        duplicate(e, curr^.e);
    end if

    return e;
end function

function end(n: pointer to tNode): boolean
    return (n = null);
end function

function emptyList(l: tList): boolean
    return (l.first = null);
end function

{ Auxiliary function }
function getNth(l: tList; index: integer): pointer to tNode

    var
        i: integer;
        prev: pointer to tNode;
    end var

    i := 1;
    prev := l.first;
    while i < index and prev ≠ null do
        prev := prev^.next;
        i := i+1;
    end while

    return prev;

end function

typedef enum {FALSE, TRUE} boolean;

void createList(tList *l) {
    l->first = NULL;
}

void insert(tList *l, elem e, int index) {
    tNode *tmp;
    tNode *prev;
    tmp = getMemory();
   if (tmp == NULL) {
        printf("\n Error: Not enough free memory\n");
    } else {
        tmp->e = e;
       if (index == 1) {
           /* no previous element */
            tmp->next = l->first;
            l->first = tmp;
        } else {
            prev = getNth(l, index-1);
           if (prev != NULL) {
               /* standard case */
                tmp->next = prev->next;
                prev->next = tmp;
            } else {
                printf("\n Error: not enough elements \n");
            }
        }
    }
}

void delete(tList *l, int index) {
    tNode *tmp;
    tNode *prev;

   if (index == 1) {
       /* no previous element */
        tmp = l->first;
       if (*l == NULL) {
            printf("\n Error: empty list\n");
        } else {
            l->first = tmp->next;
            destroy(tmp->e);
            freeMemory(tmp);
        }
    } else {
        prev = getNth(l, index-1);
       if (prev != NULL) {
           /* standard case */
            tmp = prev->next;
           if (tmp == NULL) {
                printf("\n Error: index position doesn't exist\n");
            } else {
                prev->next = tmp->next;
                destroy(tmp->e);
                freeMemory(tmp);
            }
        } else {
            printf("\n Error: index position doesn't exist\n");
        }
    }
}

elem get(tList l, int index) {

    tNode *curr;
    curr = getNth(&l, index);
   if (curr == NULL) {
        printf("\n Error: index position doesn't exist\n");
    } else {
       return curr->e;
    }
}

boolean end(tNode *n) {
   return (n == NULL);
}

boolean emptyList(tList l) {
   return (l.first == NULL);
}

{ Auxiliary function }
tNode *getNth(tList *l, int index) {
   int i;
    tNode *prev;

    i=1;
    prev = l->first;
   while i<index && (prev != NULL) {
        prev = prev->next;
        i = i+1;
    }
   return prev;
}

Ejercicio ejemplo

En este ejercicio ejemplo extendemos el TAD lista con una nueva operación: getIndex. Esta operación devuelve el índice del elemento recibido como parámetro1. Si la lista no contiene el elemento, entonces devuelve error.

Para resolverlo utilizamos la definición del tipo lista tal como se define en este módulo. Se trabaja directamente con la implementación del tipo lista. No se utiliza ninguna de las operaciones básicas del tipo (insert, delete, get, ...).

[Ejemplo 07_15]

function getIndex(l: tList; e: elem): integer

    var
        res: integer;
        i: integer;
        current: pointer to tNode;
        found: boolean
    end var

    found := false;
    res := 0;
    i := 0;
    current := l.first;
    while current ≠ null and found = false do
        found := equal(current^.e, e);
        i := i+1;
        current := current^.next;
    end while

    if found = false then
        writeString("error: element does not exist");
    else
        res := i;
    end if

   return res;

end function

typedef enum {FALSE, TRUE} boolean;

int getIndex(tList l, elem e) {

   int res;
   int i;
    tNode *current;
    boolean found;

    res = 0;
    i = 0;
    current = l.first;
    found = FALSE;

   while (current!=NULL && found==FALSE) {
        found = equal(current->e, e);
        i++;
        current = current->next;
    }

   if (found == FALSE) {
        printf("\n Error: element doesn't exist\n");
    } else {
        res = i;
    }
   return res;
}

Resumen

En este módulo hemos recordado que significa un Tipo Abstracto de Datos (TAD) así como los TAD que respresentan secuencias: pilas, colas y listas.

Asimismo se ha recordado el concepto de punteros y de memoria dinámica.

Se ha mostrado una implementación de los TAD pila, cola y listas utilizando punteros. Para cada tipo de TAD se muestra un ejercicio ejemplo basado en agregar una operación más al TAD, donde en la resolución trabaja directamente con la implementación del TAD.

Bibliografía

[1] Estructuras de datos básicas: Secuencias. Xavier Sáez Pous. Material docente de la UOC.

Etiquetas:
Creado por editor1 el 2018/09/17 11:34