Curso-lenguaje-C/fundamentos-programacion/PEC6
2024-06-18 20:09:14 +02:00
..
README.md Update PEC6 2024-06-18 20:09:14 +02:00

PEC 6

Volver a la página principal de "Fundamentos de la Programación"

Índice

18. Tipos abstractios de datos

Vamos a ver los tipos abstractos de datos (TAD) y cómo se pueden implementar para representar secuencias de elementos como Listas, Colas y Pilas. Estos TADs emulan conceptos que encontramos fuera del mundo de la programación.

Unos ejemplos son:

  • De listas: una lista de la compra, una lista de tareas pendientes, una lista de reproducción de música.
  • De colas: una cola de espera en un supermercado, una cola de espera en una llamada telefónica.
  • De pilas: una pila de platos, una pila de libros, una pila de cartas.

Todos los TADs tienen una serie de operaciones que se pueden realizar sobre ellos. Por ejemplo, una pila tiene operaciones como push y pop, una cola tiene operaciones como enqueue y dequeue, y una lista tiene operaciones como insert y delete.

18.1. Tipo abstracto de datos (TAD)

Un TAD consiste en un conjunto de valores (dominio) y el conjunto de operaciones que se pueden aplicar a este conjunto de valores.

El concepto de TAD ya existe en los lenguajes de programación bajo la forma de tipos predefinidos como int, float, char, string, etc. Pero también podemos definir nuestros propios TADs. Por ejemplo, en C, el tipo de datos int tiene como dominio todos los enteros en el rango [MININT, MAXINT] y las operaciones que se pueden aplicar: suma, resta, producto, cociente y módulo.

Implementar una TAD significa elegir una representación para el conjunto de valores del tipo de datos, así como codificar sus operaciones utilizando esa representación en un lenguaje de programación. Es posible tener varias implementaciones para un mismo TAD, pero dada la definición de un TAD, el comportamiento ha de ser siempre el mismo para cualquier implementación.

La utilidad de definir un TAD surge al diseñar nuevos tipos de datos. La idea es que el nuevo TAD solo puede ser manipulado a través de sus operaciones.

18.1.1. Ejemplos de especificación e implementación

18.1.1.1. Números naturales

El conjunto de valores que engloba este TAD son todos los números enteros mayores o iguales a 0. Las operaciones que definiremos para aplicar a este conjunto son la suma y la división.

type
  natural = integer;
end type

function suma(x: natural, y: natural): natural
  return x + y;
end function

function division(x: natural, y: natural): natural
  return x / y;
end function
#include <stdio.h>

typedef unsigned int natural;

natural suma(natural x, natural y) {
  return x + y;
}

natural division(natural x, natural y) {
  return x / y;
}

En esta definición no se han excluído los números negativos. La responsabilidad del correcto comportamiento del tipo yace en la definición de las operaciones.

18.1.1.2. Números binarios

El conjunto de valores que engloba este TAD son todos los números en su representación binaria, es decir, expresados en base 2. Las operaciones que definiremos para aplicar a este conjunto son desplazamientoIzq y complemento.

const
  MAXBITS: integer:= 64;
end const

type
  binario: vector[MAXBITS] of boolean;
end type

action desplazamientoIzq(inout b:binario, in n integer)
  var
    i: integer;
  end var

  for i:= n+1 to MAXBITS do
    b[i-n]:= b[i];
  end for

  for i:= MAXBITS-n to MAXBITS do
    b[i]:= 0;
  end for
end action

action complemento(inout b:binario)
  var
    i: integer;
  end var

  for i:= 1 to MAXBITS do
    b[i]:= not b[i];
  end for
end action
#include <stdio.h>

#define MAXBITS 64

typedef int binario[MAXBITS];

void desplazamientoIzq(binario b, int n) {
  int i;

  for (i = n; i < MAXBITS; i++) {
    b[i-n] = b[i];
  }

  for (i = MAXBITS-n; i < MAXBITS; i++) {
    b[i] = 0;
  }
}

void complemento(binario b) {
  int i;

  for (i = 0; i < MAXBITS; i++) {
    b[i] = !b[i];
  }
}

18.2. TAD para representar secuencias de elementos

Una secuencia lineal es un conjunto de tamaño arbitrario de elementos del mismo tipo. Exceptuando el primero y el último, cada elemento tiene un único predecesor y un único sucesor.

En general, un TAD que representa secuencias de elementos posee las siguientes operaciones:

  • Inicializar la secuencia.
  • Insertar un elemento en la secuencia.
  • Eliminar un elemento de la secuencia.
  • Consultar el valor de un elemento en la secuencia.
  • Consultar el número de elementos de la secuencia.

¿Dónde insertar un elemento? ¿Qué elemento eliminar? ¿Qué elemento se puede consultar? ¿Cuántos elementos hay? Las respuestas a las anteriores preguntas son las que definen el comportamiento de las operaciones y, por lo tanto, el tipo de secuencia.

18.2.1. El TAD Pila (tStack)

  • Una pila es una secuencia lineal de elementos.
  • Los elementos se insertan únicamente por un extremo de la secuencia. Por eso se dice que es una estructura LIFO (Last In, First Out).
  • La manipulación y acceso a los elementos de la pila se permite solo en un extremo de la secuencia.

Se puede pensar en una pila de libros dentro de una caja.

18.2.1.1. Lista de operaciones sobre tStack
Operación Descripción
initStack Inicializa la pila.
push Empila un elemento en la pila.
pop Desempila un elemento de la pila.
top Devuelve copia del valor del elemento en la cima de la pila.
isEmptyStack Consulta si la pila está vacía (true).
isFullStack Consulta si la pila está llena (true).
heightStack Consulta el número de elementos de la pila.
18.2.1.2. Implementación

Vamos a crear una implementación en un vector, donde tenemos un número máximo de elementos (MAX). Para conocer el espacio disponible de la pila en cada momento se necesita un atributo que indique el número total de elementos y que llamaremos nelem.

Implementación del tipo:

type
  tStack = record
    A: vector[MAX] of elem; {elem represents the type of the elements in th tStack}
    nelem: integer;
  end record
end type

typedef struct {
  elem A[MAX];
  int nelem;
} tStack;

Implementación de las operaciones del tipo:

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

action initStack(out s: tStack)
  s.nelem:= 0;
end action

action push(inout s: tStack, in e: elem)
  if s.nelem = MAX then
    {error full tStack}
  else
    s.nelem:= s.nelem + 1;
    s.A[s.nelem]:= e;
  end if

end action

action pop(inout s: tStack)
  if s.nelem = 0 then
    {error empty tStack}
  else
    s.nelem:= s.nelem - 1;
  end if
end action

action top(in s: tStack, out e: elem)
  if s.nelem = 0 then
    {error empty tStack}
  else
    e:= s.A[s.nelem];
  end if
end action

function isEmptyStack(s: tStack): boolean
  return s.nelem = 0;
end function

function isFullStack(s: tStack): boolean
  return s.nelem = MAX;
end function

function heightStack(s: tStack): integer
  return s.nelem;
end function
#include <stdio.h>
#include <stdbool.h>

void initStack(tStack *s) {
  s->nelem = 0;
}

void push(tStack *s, elem e) {
  if (s->nelem == MAX) {
    printf("\n Full Stack \n");
  } else {
    s->A[s->nelem] = e; /* First position in C is 0 */
    s->nelem++;
  }
}

void pop(tStack *s) {
  if (s->nelem == 0) {
    printf("\n Empty Stack \n");
  } else {
    s->nelem--;
  }
}

void top(tStack s, elem *e) {
  if (s.nelem == 0) {
    printf("\n Empty Stack \n");
  } else {
    *e = s.A[s.nelem-1];
  }
}

bool isEmptyStack(tStack s) {
  return s.nelem == 0;
}

bool isFullStack(tStack s) {
  return s.nelem == MAX;
}

int heightStack(tStack s) {
  return (s.nelem);
}
18.2.1.3. Ejemplo de uso

Vamos a suponer un ejemplo de una pila de libros. Cada libro dispone de un código identificador y un nombre. Definimos las estructuras de datos necesarias para modelar el ejemplo:

type
  tBook = record
    name: string;
    id: integer;
  end record

  tBox = record
    A: vector[MAX] of tBook;
    nelem: integer;
  end record
end type
#include <stdio.h>
#define MAX_NAME_LEN  = 25

typedef struct {
  char[MAX_NAME_LEN] name;
  int id;
} tBook;

typedef struct {
  tBook A[MAX];
  int nelem;
} tBox;

Ok. Ahora vamos a implementar una acción que dada una pila de libros y el código de un libro, encuentre este libro en la pila. En caso de encontrarlo, lo retiramos de la pila; en caso contrario, dejamos la pila como estaba.


action findBook(inout s: tBox, in id: integer)
  var
    b: tBook;
    aux: tBox;
    found: boolean;
  end var

  found:= false;
  initStack(aux);

  while not isEmptyStack(s) and not found do
    top(s, b);
    
    if b.id = id then
      push(aux, b);
    else
      found:= true;
    end if

    pop(s);

  end while

  while not isEmptyStack(aux) do
    top(aux, b);
    push(s, b);
    pop(aux);
  end while

end action
#include <stdio.h>
#include <stdbool.h>

void findBook(tBox *s, int id) {
  tBook b;
  tBox aux;
  bool found;

  found = false;
  initStack(&aux);

  while (!isEmptyStack(*s) && !found) {
    top(*s, &b);

    if (b.id == id) {
      push(&aux, b);
    } else {
      found = true;
    }

    pop(s);
  }

  while (!isEmptyStack(aux)) {
    top(aux, &b);
    push(s, b);
    pop(&aux);
  }
}

18.2.2. El TAD Cola (tQueue)

  • Una cola es una secuencia lineal de elementos.
  • Los elementos se insertan por el final de la cola y se extraen por el principio. Por eso se dice que es una estructura FIFO (First In, First Out).
  • La manipulación y el acceso a los elementos de la cola solo se permite en los extremos.

Un ejemplo de una cola es una cola de personas esperando para comprar una entrada en la taquilla de un teatro.

18.2.2.1. Lista de operaciones sobre tQueue
Operación Descripción
initQueue Inicializa la cola.
enqueue Encola un elemento en la cola.
dequeue Desencola un elemento de la cola.
head Devuelve copia del valor del elemento en la cabeza de la cola.
isEmptyQueue Consulta si la cola está vacía (true).
isFullQueue Consulta si la cola está llena (true).
lengthQueue Consulta el número de elementos de la cola.
18.2.2.2. Implementación

Vamos a crear una implementación en un vector, donde tenemos un número máximo de elementos (MAX). Para conocer el espacio disponible de la cola en cada momento se necesita un atributo que indique el número total de elementos y que llamaremos nelem.

Implementación del tipo:

type
  tQueue = record
    A: vector[MAX] of elem;
    nelem: integer;
  end record
end type
typedef struct {
  elem A[MAX];
  int nelem;
} tQueue;

Implementación de las operaciones:

Los elementos se encolan por el final de la cola y se desencolan por el principio. Tened en cuenta que cada vez que se elemina un elemento se han de desplazar todos los elementos de la cola una posición a la izquierda.

action initQueue(out q: tQueue)
  q.nelem:= 0;
end action

action enqueue(inout q: tQueue, in e: elem)
  if q.nelem = MAX then
    {error full tQueue}
  else
    q.nelem:= q.nelem + 1;
    q.A[q.nelem]:= e;
  end if
end action

action dequeue(inout q: tQueue)
  var
    i: integer;
  end var

  if q.nelem = 0 then
    {error empty tQueue}
  else
    for i:= 1 to q.nelem-1 do
      q.A[i]:= q.A[i+1];
    end for

    q.nelem:= q.nelem - 1;
  end if
end action

action head(in q: tQueue, out e: elem)
  if q.nelem = 0 then
    {error empty tQueue}
  else
    e:= q.A[1];
  end if
end action

function isEmptyQueue(q: tQueue): boolean
  return q.nelem = 0;
end function

function isFullQueue(q: tQueue): boolean
  return q.nelem = MAX;
end function

function lengthQueue(q: tQueue): integer
  return q.nelem;
end function
#include <stdio.h>
#include <stdbool.h>

void initQueue(tQueue *q) {
  q->nelem = 0;
}

void enqueue(tQueue *q, elem e) {
  if (q->nelem == MAX) {
    printf("\n Full Queue \n");
  } else {
    q->A[q->nelem] = e; /* first position in C is 0 */
    q->nelem++;
  }
}

void dequeue(tQueue *q) {
  int i;

  if (q->nelem == 0) {
    printf("\n Empty Queue \n");
  } else {
    for (i = 0; i < q->nelem-1; i++) {
      q->A[i] = q->A[i+1];
    }

    q->nelem--;
  }
}

void head(tQueue q, elem *e) {
  if (q.nelem == 0) {
    printf("\n Empty Queue \n");
  } else {
    *e = q.A[0];
  }
}

bool isEmptyQueue(tQueue q) {
  return (q.nelem == 0);
}

bool isFullQueue(tQueue q) {
  return (q.nelem == MAX);
}

int lengthQueue(tQueue q) {
  return (q.nelem);
}
18.2.2.3. Ejemplo de uso

Siguiendo el ejemplo de la cola de una taquilla. Cada cliente de la cola tiene asociado el ordinal en la cola desde que abrió la taquilla y la cantidad de entradas que desea adquirir. Definimos las estructuras de datos necesarias para modelar el ejemplo:

type
  tClient = record
    num: integer;
    quantity: integer;
  end record

  tTicketOffice = record
    A: vector[MAX] of tClient;
    nelem: integer;
  end record
end type
typedef struct {
  int num;
  int quantity;
} tClient;

typedef struct {
  tClient A[MAX];
  int nelem;
} tTicketOffice;

Ahora necesitamos implementar una acción que atienda el primer cliente de la cola en la taquilla. La acción recibe como parámetros la cola q de clientes y un número available que indica la disponibilidad de entradas. En caso de haber suficientes entradas disponibles, se actualiza la cantidad de entradas disponibles y se devuelve el valor verdadero en el parámetro sold. Una vez el cliente es atendido, se elimina de la cola.

action serverCliente(inout q: tTicketOffice, in available: integer, out sold: boolean)
  var
    c: tClient;
  end var

  sold:= false;

  if not isEmptyQueue(q) then
    head(q, c);

    if c.quantity <= available then
      dequeue(q);
      available:= available - c.quantity;
      sold:= true;
    end if

    dequeue(q);

  end if
end action
#include <stdio.h>
#include <stdbool.h>

void serverClient(tTicketOffice *q, int *available, bool *sold) {
  tClient c;

  *sold = false;

  if (!isEmptyQueue(*q)) {
    head(*q, &c);

    if (c.quantity <= *available) {
      *available -= c.quantity;
      *sold = true;
    }

    dequeue(q);
  }
}

18.2.3. El TAD Lista (tList)

  • Una lista es una secuencia lineal de elementos.
  • Los elementos se insertan, se eliminan y se consultan en cualquier posición de la lista.

Para entender mejor el concepto de lista, podemos pensar en una lista de la compra del supermercado.

18.2.3.1. Lista de operaciones sobre tList
Operación Descripción
initList Inicializa la lista.
insert Inserta un elemento en la lista.
delete Elimina un elemento de la lista.
get Devuelve copia del valor del elemento en la posición i.
isEnd Consulta si la posición i es el final de la lista (true).
isEmptyList Consulta si la lista está vacía (true).
isFullList Consulta si la lista está llena (true).
lengthList Consulta el número de elementos de la lista.
18.2.3.2. Implementación

Vamos a crear una implementación en un vector, donde tenemos un número máximo de elementos (MAX). Para conocer el espacio disponible de la lista en cada momento se necesita un atributo que indique el número total de elementos y que llamaremos nelem.

Implementación del tipo:

type
  tList = record
    A: vector[MAX] of elem;
    nelem: integer;
  end record
end type
typedef struct {
  elem A[MAX];
  int nelem;
} tList;

Implementación de las operaciones:

Los elementos se insertan en cualquier posición de la lista y se eliminan de la misma forma.

Cada vez que se inserta un elemento en una posición i, se han de desplazar todos los elementos de la lista una posición a la derecha. Cada vez que se elimina un elemento de una posición i, se han de desplazar todos los elementos de la lista una posición a la izquierda.

action initList(out l: tList)
  l.nelem:= 0;
end action

action insert(inout l: tList, in e: elem, in index: integer)
  var
    i: integer;
  end var

  if l.nelem = MAX then
    {error full tList}
  else
    for i:= l.nelem to index step -1 do
      l.A[i+1]:= l.A[i];
    end for

    l.nelem:= l.nelem + 1;
    l.A[index]:= e;
  end if
end action

action delete(inout l: tList, in index: integer)
  var
    i: integer;
  end var

  if l.nelem = 0 then
    {error empty tList}
  else
    for i:= index to l.nelem-1 do
      l.A[i]:= l.A[i+1];
    end for

    l.nelem:= l.nelem - 1;
  end if
end action

action get(in l: tList, in index: integer, out e: elem)
  if l.nelem = 0 then
    {error empty tList}
  else
    e:= l.A[index];
  end if
end action

function isEnd(l: tList, pos: integer): boolean
  return pos = l.nelem;
end function

function isEmptyList(l: tList): boolean
  return l.nelem = 0;
end function

function isFullList(l: tList): boolean
  return l.nelem = MAX;
end function

function lengthList(l: tList): integer
  return l.nelem;
end function
#include <stdio.h>
#include <stdbool.h>

void initList(tList *l) {
  l->nelem = 0;
}

void insert(tList *l, elem e, int index) {
  int i;

  if (l->nelem == MAX) {
    printf("\n Full List \n");
  } else {
    for (i = l->nelem; i >= index; i--) {
      l->A[i+1] = l->A[i];
    }

    l->nelem++;
    l->A[index] = e;
  }
}

void delete(tList *l, int index) {
  int i;

  if (l->nelem == 0) {
    printf("\n Empty List \n");
  } else {
    for (i = index; i < l->nelem-1; i++) {
      l->A[i] = l->A[i+1];
    }

    l->nelem--;
  }
}

void get(tList l, int index, elem *e) {
  if (l.nelem == 0) {
    printf("\n Empty List \n");
  } else {
    *e = l.A[index];
  }
}

bool isEnd(tList l, int pos) {
  return pos == l.nelem;
}

bool isEmptyList(tList l) {
  return l.nelem == 0;
}

bool isFullList(tList l) {
  return l.nelem == MAX;
}

int lengthList(tList l) {
  return l.nelem;
}
18.2.3.3. Ejemplo de uso

Siguiendo el ejemplo de la lista de la compra del supermercado. Cada artículo tiene asociado un nombre, el tipo de artículo clasificado según sea de panadería, frescos, bebidas, congelados, belleza o desayuno, y la cantidad de este artículo. Definimos las estructuras de datos necesarias para modelar el ejemplo:

type
  tArticleType = {BAKERY, FRESH, DRINKS, FROZEN, BEAUTY, BREAKFAST}
  tArticle = record
    type: tArticleType;
    quantity: real;
  end record

  tBuyList = record
    A: vector[MAX] of tArticle;
    nelem: integer;
  end record
end type
#include <stdio.h>

typedef enum {BAKERY, FRESH, DRINKS, FROZEN, BEAUTY, BREAKFAST} tArticleType;

typedef struct {
  tArticleType type;
  float quantity;
} tArticle;

typedef struct {
  tArticle A[MAX];
  int nelem;
} tBuyList;

Necesitamos implementar una acción que elimine de la lista de la compra l, todos los artículos de un tipo dado (filter) que se recibe como parámetro.

action filterArticleType(inout l: tBuyList, in filter: tArticleType)
  var
    a: tArticle;
    pos: integer;
  end var

  pos := 1;

  while not isEnd(l, pos) do
    get(l, pos, a);

    if a.type = filter then
      delete(l, pos);
    else
      pos := pos + 1;
    end if
  end while
end action
#include <stdio.h>

void filterArticleType(tBuyList *l, tArticleType filter) {
  elem a;
  int pos;

  pos = 0;

  while (!isEnd(*l, pos)) {
    get(*l, pos, &a);

    if (a.type == filter) {
      delete(l, pos);
    } else {
      pos = pos + 1;
    }
  }
}

18.2.4. Sintaxis para la declaración (Definición de un TAD de tipo pila, cola o lista)

Para declarar un tipo pila dentro de un algoritmo, acción o función, se utiliza la siguiente sintaxis:

nombreTipoPila = tStack(tipoElemento)

por ejemplo, tBinaryStack = tStack(tBit) permite declarar el tipo tBinaryStack como un tipo de pila de elementos de tipo tBit. A partir de aquí, se le pueden aplicar todas las operaciones que se han definido para las pilas.

Como véis, en lenguaje algorítmico, la declaración queda como un tipo abstracto (su implementación es "oculta") pero en cambio, en lenguaje C, sí que se "ve" su implementación interna:

typedef struct{
  tBit A[MAXBITS];
  int nelem;
} tBinaryStack;

A partir de aquí, solo se debe utilizar con las funciones y acciones predefinidas para este tipo. De este modo, realmente lo estamos utilizando como un tipo abstracto pila.

De forma similar se puede declarar una cola o una lista:

nombreTipoCola = tQueue(tipoElemento)
nombreTipoLista = tList(tipolemento)

19. Navegación de TAD

19.1. Ejemplos sobre el TAD pila

19.2. Ejemplos sobre el TAD cola

19.3. Ejemplos sobre el TAD lista

Volver arriba