12. Tipos de datos estructurados: tupla

Última modificación por editor1 el 2018/09/17 10:12

Objetivos

  • Entender el concepto de dato estructurado.
  • Acceder y escribir información en un dato estructurado.
  • Identificar la correspondencia con la memoria y su representación en esta.

Introducción

Habíamos visto que las variables eran objetos que almacenaban un valor de un cierto tipo y que durante su vida en el algoritmo, o programa, podían cambiar el valor que contenían. Eso sí, en cada momento solo podían almacenar un valor.

En el tema anterior ampliamos la potencia del concepto de variable al incluir los vectores y las matrices. De esta manera podíamos tratar como una unidad una colección de datos homogénea y creábamos vectores de enteros o matrices de reales o de otro tipo básico.

En este apartado trabajaremos con tuplas. Se trata de colecciones heterogéneas de datos, en las que cada elemento de la colección puede ser de un tipo diferente. También veremos cómo se puede acceder a cada uno de los datos individuales de la tupla y también cómo son almacenados en la memoria del ordenador.

1. Tipo de datos tupla

Una tupla (record en inglés o también struct) es un objeto que contiene otros objetos que denominaremos campos. Cada uno de los campos tiene un nombre para identificarlo y es de un tipo de datos.

Pensemos en la información básica de un documento de identidad. Necesitaríamos mantener diversas variables para almacenar el nombre, los apellidos, la fecha de nacimiento, el número del DNI y la letra asociada:

[Ejemplo 12_01]

var
    prename: string;
    name:  string;
    birthDay: integer;
    birthMonth: integer;
    birthYear: integer;
    idNumber: integer;
    idChar: char;
end var

#include <stdio.h>
#define MAX_NAME_LEN 25

int main(int argc, char** argv) {

   /* Variable definition*/
   char prename[MAX_NAME_LEN];
   char name[MAX_NAME_LEN];
   int birthDay;
   int birthMonth;
   int birthYear;
   int idNumber;
   char idChar;
}

Trabajar con estos datos puede ser complicado porque tenemos que controlar diversas variables. Copiar, por ejemplo, la información de una sola persona requerirá unas cuantas instrucciones.

Una tupla permite definir una estructura en la que poder reunir toda esta información diferente y permite tratar cada uno de los campos individualmente o, si nos interesa, tratar todo el conjunto como una unidad.

[Ejemplo 12_02]

type
    people = record
        prename: string;
        name:  string;
        birthDay: integer;
        birthMonth: integer;
        birthYear: integer;
        idNumber: integer;
        idChar: char;
    end record
end type
var
    you: people;
end var

#include <stdio.h>

#define MAX_NAME_LEN 25

int main(int argc, char** argv) {

   typedef struct {
       char prename[MAX_NAME_LEN];
       char name[MAX_NAME_LEN];
       int birthDay;
       int birthMonth;
       int birthYear;
       int idNumber;
       char idChar;
    } people;

   /* Variable definition */
    people you;
}

Para acceder a un campo de una variable de tipo estructurada en lenguaje algorítmico o en C se emplea el nombre de la variable y el nombre del campo unidos por el delimitador ‘.’ (punto).  Por ejemplo, para referirnos al año de nacimiento almacenado en you nos referiríamos a you.birthYear y you.firstName sería la referencia al nombre de pila de la persona.

Por ejemplo, si queremos llenar la ficha de una persona y escribirla:

[Ejemplo 12_03]

algorithm loadData

    type
        people = record
            prename: string;
            name:  string;
            birthDay: integer;
            birthMonth: integer;
            birthYear: integer;
            idNumber: integer;
            idChar: char;
        end record
    end type

    var
        you: people;
    end var

    {read phase}
    you.prename := readString();
    you.name := readString();
    you.birthDay := readInteger();
    you.birthMonth := readInteger();
    you.birthYear := readInteger();
    you.idNumber := readInteger();
    you.idChar := readChar();

    {write phase}
    writeString(you.name);
    writeString(you.prename);
    writeInteger(you.idNumber);
    writeChar(you.idChar);
    writeInteger(you.birthDay);
    writeInteger(you.birthMonth);
    writeInteger(you.birthYear); 

end algorithm

#include <stdio.h>

#define MAX_NAME_LEN 25

int main(int argc, char** argv) {

   /* Type definition */
   typedef struct {
       char prename[MAX_NAME_LEN];
       char name[MAX_NAME_LEN];
       int birthDay;
       int birthMonth;
       int birthYear;
       int idNumber;
       char idChar;
    } people;

   /* Variable definition */
    people you;

   /*read phase*/
    scanf("%s %s %d %d %d %d %c", you.prename, you.name, &you.birthDay, &you.birthMonth, &you.birthYear, &you.idNumber, &you.idChar);

   /*write phase*/
    printf("Name: %s, %s \n", you.name, you.prename);
    printf("Id: %d-%c \n", you.idNumber, you.idChar);
    printf("BirthDay: %d - %d - %d \n", you.birthDay, you.birthMonth, you.birthYear);

   return 0;
}

Puede parecer que el uso de records es incómodo porque genera unos identificadores muy largos (nombre_variable.nombre_campo o nombre_variable["nombre_campo"]) y seguramente para programas muy sencillos y con pocos datos puede ser cierto. No obstante, no hay que ir a ejemplos muy complicados para ver que se facilitan las cosas. 

Veamos el caso de tener que ordenar los datos de dos personas según su fecha de nacimiento.

[Ejemplo 12_04]

algorithm ageOrder

    type
        people = record
            prename: string;
            name:  string;
            birthDay: integer;
            birthMonth: integer;
            birthYear: integer;
            idNumber: integer;
            idChar: char;
        end record
    end type

    var
        old, young, temp: people;
    end var

    {read phase first people}
    old.prename := readString();
    old.name := readString();
    old.birthDay := readInteger();
    old.birthMonth := readInteger();
    old.birthYear := readInteger();
    old.idNumber := readInteger();
    old.idChar := readChar();

    {read phase second people}
    young.prename := readString();
    young.name := readString();
    young.birthDay := readInteger();
    young.birthMonth := readInteger();
    young.birthYear := readInteger();
    young.idNumber := readInteger();
    young.idChar := readChar();

    {age sort phase}
    if ( (old.birthYear > young.birthYear)
        or (old.birthYear = young.birthYear and old.birthMonth > young.birthMonth)
        or (old.birthYear = young.birthYear and old.birthMonth = young.birthMonth and old.birthDay > young.birthYDay) ) then

        {copy old to temp}
        temp.prename := old.prename;
        temp.name := old.name;
        temp.birthDay := old.birthDay;
        temp.birthMonth := old.birthMonth;
        temp.birthYear := old.birthYear;
        temp.idNumber := old.idNumber;
        temp.idChar := old.idChar;

        {copy young to old}
        old.prename := young.prename;
        old.name := young.name;
        old.birthDay := young.birthDay;
        old.birthMonth := young.birthMonth;
        old.birthYear := young.birthYear;
        old.idNumber := young.idNumber;
        old.idChar := young.idChar;

        {copy temp to young}
        young.prename := temp.prename;
        young.name := temp.name;
        young.birthDay := temp.birthDay;
        young.birthMonth := temp.birthMonth;
        young.birthYear := temp.birthYear;
        young.idNumber := temp.idNumber;
        young.idChar := temp.idChar;

    end if

    {write phase old people}
    writeString(old.name);
    writeString(old.prename);
    writeInteger(old.idNumber);
    writeChar(old.idChar);
    writeInteger(old.birthDay);
    writeInteger(old.birthMonth);
    writeInteger(old.birthYear); 

    {write phase young people}
    writeString(young.name);
    writeString(young.prename);
    writeInteger(young.idNumber);
    writeChar(young.idChar);
    writeInteger(young.birthDay);
    writeInteger(young.birthMonth);
    writeInteger(young.birthYear); 

end algorithm

#include <stdio.h>
#include
<string.h>

#define MAX_NAME_LEN 25

int main(int argc, char** argv) {

   /* Type definition */
   typedef struct {
       char prename[MAX_NAME_LEN];
       char name[MAX_NAME_LEN];
       int birthDay;
       int birthMonth;
       int birthYear;
       int idNumber;
       char idChar;
    } people;

   /* Variable definition */
    people young, old, temp;

   /*read phase first people*/
    scanf("%s %s %d %d %d %d %c", old.prename, old.name, &old.birthDay, &old.birthMonth, &old.birthYear, &old.idNumber, &old.idChar);

   /*read phase second people*/
    scanf("%s %s %d %d %d %d %c", young.prename, young.name, &young.birthDay, &young.birthMonth, &young.birthYear, &young.idNumber, &young.idChar);

   /* age sort phase */
   if ( (old.birthYear > young.birthYear) ||( (old.birthYear == young.birthYear) && (old.birthMonth > young.birthMonth)) || ((old.birthYear == young.birthYear) && (old.birthMonth == young.birthMonth) && ( old.birthDay > young.birthDay)))
    {
        /*copy old to temp*/
         strcpy(temp.prename,old.prename);
         strcpy(temp.name,old.name);
         temp.birthDay = old.birthDay;
         temp.birthMonth = old.birthMonth;
         temp.birthYear = old.birthYear;
         temp.idNumber = old.idNumber;
         temp.idChar = old.idChar;

        /*copy young to old*/
         strcpy(old.prename,young.prename);
         strcpy(old.name,young.name);
         old.birthDay = young.birthDay;
         old.birthMonth = young.birthMonth;
         old.birthYear = young.birthYear;
         old.idNumber = young.idNumber;
         old.idChar = young.idChar;

        /*copy temp to young*/
         strcpy(young.prename,temp.prename);
         strcpy(young.name,temp.name);
         young.birthDay = temp.birthDay;
         young.birthMonth = temp.birthMonth;
         young.birthYear = temp.birthYear;
         young.idNumber = temp.idNumber;
         young.idChar = temp.idChar;
     
    }

   /*write phase old people*/
    printf("Name: %s, %s \n", old.name, old.prename);
    printf("Id: %d-%c \n", old.idNumber, old.idChar);
    printf("BirthDay: %d - %d - %d \n", old.birthDay, old.birthMonth, old.birthYear);

   /*write phase young people*/
    printf("Name: %s, %s \n", young.name, young.prename);
    printf("Id: %d-%c \n", young.idNumber, young.idChar);
    printf("BirthDay: %d - %d - %d \n", young.birthDay, young.birthMonth, young.birthYear);

   return 0;
}

Tened en cuenta que cuando estudiemos la modularidad podremos eliminar parte del código que se repite. La lectura, escritura y copia de las estructuras de datos podrán estar encapsuladas y no será necesario repetir el código.

2. Representación en memoria

Todos los campos de un dato estructurado se mantienen en la memoria ocupando posiciones consecutivas. Lo podemos ejemplificar con un dato más simple que los anteriores, como el que podríamos definir para representar una fecha dd-mm-aaaa.

[Ejemplo 12_05]

type
    date = record
        day: integer;
        month: integer;
        year: integer;
    end record
end type

var
    today: date;
end var

#include <stdio.h>

int main(int argc, char** argv) {

   /* Types definition */
   typedef struct {
       int day;
       int month;
       int year;
    } date;

   /* Variable definition */
    date today;
}

La variable today contiene tres campos de tipo entero. Si cada campo entero ocupa 4 bytes, el dato estructurado ocupa un total de 12 bytes consecutivos. Si el dato se almacena en la memoria a partir de una posición X, podríamos ver:

tupla1.png

Disposición en la que el primer campo empieza donde comienza el record, el segundo campo definido empieza 4 bytes más allá del inicio, puesto que el campo anterior ha ocupado 4 bytes y el tercer campo está a 8 bytes del principio (porque los campos anteriores han ocupado 8 bytes).

Si la variable today contenía la fecha del último día del siglo XX en memoria, tendríamos:

tupla2.png

Debe comprenderse que el lugar dentro de la tupla donde comienza un determinado campo no tiene ninguna relación con el tipo de este campo, solo depende del espacio ocupado por los campos anteriores.

Si hubiéramos definido una tupla para guardar documentos de identidad, los dos campos serían de tipos diferentes:

[Ejemplo 12_06]

type
    dni = record
        idNumber: integer;
        idChar: char;
    end record
end type

var
    identification: dni;
end var

#include <stdio.h>

int main(int argc, char** argv) {

   /* Type definition*/
   typedef struct {
       int idNumber;
       char idChar;
    } dni;

   /* Variable definition */
    dni identification;
}

Como un carácter solo ocupa un byte en la memoria, la representación esquemática de esta tupla, puesta a partir de la posición de memoria X, sería:

tupla3.png

Cada campo estaría almacenado ocupando las posiciones correspondientes a su tipo base.

Por ejemplo, para el dni de valor 12345657-R, la representación sería:

tupla4.png

3. Estructuras complejas

Hemos visto que un tipo estructurado de datos o tupla sirve para agrupar campos de información para representar algún objeto (por ejemplo, una persona). Un tipo estructurado de datos puede ser utilizado como cualquier otro tipo de datos básico y puede contener cualquier otro tipo de datos. A continuación, se muestra un ejemplo de las diferentes combinaciones posibles.

3.1. Tuplas que contienen tuplas

Cuando se define una tupla no existe ninguna limitación para declarar sus campos. Nada impide que un campo de una tupla pueda ser, por su parte, una tupla. De hecho, ya hemos avanzado un poco en este sentido porque llevamos definidos varios tipos de tuplas, pero es interesante fijarse en que las dos últimas que hemos definido (date y dni), por un lado, son tuplas útiles por sí mismas (en un programa grande que trabaje con datos de personas, muy a menudo se deberán tratar documentos de identificación y también se deberán tratar fechas) y la información que contienen forma parte de la tupla people que hemos definido al principio.



Si consideramos esta situación, la definición de people puede simplificarse si ya tenemos definidos los tipos date y dni.

[Ejemplo 12_07]

type
    date = record
        day: integer;
        month: integer;
        year: integer;
    end record

    dni = record
        idNumber: integer;
        idChar: char;
    end record

    people = record
        prename: string;
        name:  string;
        birth: date;
        id: dni;
    end record
end type

var
    you: people;
end var

#include <stdio.h>

#define MAX_NAME_LEN 25

int main(int argc, char** argv) {

   typedef struct {
       int day;
       int month;
       int year;
    } date;

   typedef struct {
       int idNumber;
       char idChar;
    } dni;

   typedef struct {
       char prename[MAX_NAME_LEN];
       char name[MAX_NAME_LEN];
        date birth;
        dni id;
    } people;

   /* Variable definition */
    people you;
}

El uso de estas tuplas con tuplas (tuplas jerárquicas) obliga también a utilizar una notación jerárquica para acceder a estos campos.

Pongamos, por ejemplo, la variable you del tipo tPeople definido en última instancia.

  • you.name es el apellido almacenado.
  • you.birth es una tupla, es decir, la colección de tres valores que configuran la fecha de nacimiento y, para llegar a uno de sus campos, se deberá emplear el operador punto. Para referirnos al día deberemos emplear you.birth.day; para hacerlo con el año, you.birth.year.

Es interesante tener claro a qué objeto se hace referencia en cada momento. Cuando empleamos you.nombre_del_campo estamos indicando dicho campo. Si el campo es de un tipo básico, ya tenemos acceso al valor de este tipo. Si el campo en cuestión es una tupla, tenemos acceso a la tupla completa y para llegar a uno de los campos deberemos emplear de nuevo el operador punto, seguido del nombre del campo al que se quiera acceder.

3.2. Vectores de tuplas

Si en un programa los datos que hacen referencia a una entidad (por ejemplo, una persona) son lo suficientemente complejos como para crear tuplas, es muy probable que nuestros algoritmos o programas tengan que trabajar con colecciones de este tipo de objeto. La estructura de datos adecuada para hacerlo es el vector (o matriz). No existe ningún problema en crear vectores de tuplas.

Veamos un ejemplo: queremos mantener la información básica de los estudiantes de un aula.

[Ejemplo 12_08]

type
    date = record
        day: integer;
        month: integer;
        year: integer;
    end record

    dni = record
        idNumber: integer;
        idChar: char;
    end record

    people = record
        prename: string;
        name:  string;
        birth: date;
        id: dni;
    end record
end type

var
    studentGroup: vector[50] of people;
end var

#include <stdio.h>

#define MAX_NAME_LEN 25

int main(int argc, char** argv) {

   /* Types definition */
   typedef struct {
       int day;
       int month;
       int year;
    } date;

   typedef struct {
       int idNumber;
       char idChar;
    } dni;

   typedef struct {
       char prename[MAX_NAME_LEN];
       char name[MAX_NAME_LEN];
        date birth;
        dni id;
    } people;

   /* Variable definition */
    people studentGroup[50];
}

También en este caso, al hacer referencia a una posición determinada del vector, nos estaremos refiriendo a un record completo y, para acceder a sus campos, deberemos emplear el punto y el identificador del campo. Por ejemplo:

  • studentGroup[12] es la tupla que contiene toda la información del estudiante que está en la posición 12 del vector.
  • studentGroup[12].name es el campo de texto que contiene los apellidos del estudiante que está en la posición 12 del vector.
  • studentGroup[12].birth.year es el campo que contiene el año de nacimiento del estudiante de la posición 12 del vector.

En caso de que la declaración de la variable sea de una matriz, solo se deberá tener en cuenta la doble indexación de la matriz. Veamos el ejemplo de una escuela que tiene 6 grupos de 50 estudiantes respectivamente:

[Ejemplo 12_09]

type
    date = record
        day: integer;
        month: integer;
        year: integer;
    end record

    dni = record
        idNumber: integer;
        idChar: char;
    end record

    people = record
        prename: string;
        name:  string;
        birth: date;
        id: dni;
    end record
end type

var
    school: matrix[6][50] of people;
end var

#include <stdio.h>

#define MAX_NAME_LEN 25

int main(int argc, char** argv) {

   /* Types definition */
   typedef struct {
       int day;
       int month;
       int year;
    } date;

   typedef struct {
       int idNumber;
       char idChar;
    } dni;

   typedef struct {
       char prename[MAX_NAME_LEN];
       char name[MAX_NAME_LEN];
        date birth;
        dni id;
    } people;

   /* Variable definition */
    people school[6][50];
}

En este caso, bien en lenguaje algorítmico o en C:

  • school[2] hace referencia a un vector, un grupo completo de estudiantes (el de segunda posición).
  • school[2][12] hace referencia a una tupla, la correspondiente al estudiante 12 del grupo 2.
  • school[2][12].name serían los apellidos del estudiante 12 del grupo 2.
  • school[2][12].birth.year sería el año de nacimiento del estudiante 12 del grupo 2.

3.3. Tuplas que contienen vectores

De manera similar, muchas veces en que queremos definir tuplas para representar una determinada entidad, encontramos que una de las informaciones que queremos tener es realmente una colección de informaciones más simples. Pongamos por caso que queremos crear un dato estructurado para mantener la información de una asignatura. Seguramente querremos tener el nombre de la asignatura, las calificaciones de las 5 actividades de evaluación continua ('A', 'B', etc.), la calificación del examen y la calificación final. Un record que corresponde a estas necesidades podría ser:

[Ejemplo 12_10]

type   
    subject = record
        name: string;
        PEC:  vector[5] of char;
        test: real;
        finalMark: real;
    end record
end type

var
    computerScience: subject;
end var

#include <stdio.h>

#define MAX_NAME_LEN 25

int main(int argc, char** argv) {

   /* Type definition */
   typedef struct {
       char name[MAX_NAME_LEN];
       char PEC[5];                       
       float test;
       float finalMark;
    } subject;

   /* Variable definition */
    subject computerScience;
}

También en este caso tendremos que ir con cuidado con las referencias.

  • computerScience hace referencia a la tupla completa; toda la información de la asignatura.
  • computerScience.PEC hace referencia a un vector que contiene las calificaciones de todas las PECs.
  • computerScience.PEC[3] en lenguaje algorítmico hace referencia a la calificación de la actividad 3 obtenida en la asignatura (recordemos que la primera tiene el índice 1), y en lenguaje C hace referencia a la calificación de la actividad 4 obtenida en la asignatura (recordemos que la primera posición tiene el índice 0).

3.4. Y, generalizando…

Vemos que esta estructuración de datos puede ir creciendo en función de nuestras necesidades. Podemos crear un record que tenga algún campo donde se almacene una colección de datos del mismo tipo (un vector) y que cada dato sea a la vez un dato estructurado.

Cualquier entidad real de una cierta dimensión se puede representar mediante una estructura de datos de complejidad creciente. No incluiremos el código, pero podemos pensar en algo que nos resulta tan familiar como un hospital (no creáis, un hospital pequeñito).

Tendremos que guardar:

  • Información sobre la plantilla de médicos (Los datos personales de cada médico, la especialidad médica, la cuenta de domiciliación bancaria, la nómina, etc.).  Seguramente vale la pena definir un record para guardar la información de un médico porque de cada médico queremos la misma información. Y un vector de records irá bien para almacenar toda la plantilla.
  • Información sobre los pacientes (Los datos personales de cada paciente, su número de seguro médico y la compañía, el historial médico, la cuenta de donde cobrar los extras de su estancia, etc.). También parece lógico usar un vector de records.
  • Información sobre las habitaciones (el número de cada habitación, la capacidad, qué pacientes la ocupan, etc.) También tiene lógica usar un vector de records.

Y, si nos fijamos en las informaciones que hemos descrito como campos de las tuplas, veremos que muchas de estas informaciones tienen mucha probabilidad de ser estructuradas, por ejemplo:

  • Los datos personales de médicos y de pacientes responden a la misma estructura, por lo tanto, es conveniente definir una tupla como la que hay descrita, del tipo tPeople y que podemos utilizar tanto en médicos como pacientes.
  • El historial médico no es más que la secuencia de diagnósticos y tratamientos del paciente. Posiblemente cada diagnóstico y cada tratamiento lo podremos esquematizar en un record. Y un historial será un vector de diagnósticos y tratamientos.
  • Seguro que en muchas ocasiones habrá fechas que almacenar (ingreso en clínica, alta médica, etc.), por lo tanto, el record tDate que hemos definido será muy útil, no solo como integrante de la tupla de tipo tPeople.

Y podríamos continuar en esta tarea de refinamiento con el convencimiento de que, si es necesario, todo un mundo se puede representar en un dato estructurado.

Sin embargo, no seamos tan ambiciosos y creemos solo los datos estructurados adecuados para el tratamiento de nuestra información. Debemos practicar el refinado arte de equilibrar los esfuerzos y los resultados.

Resumen

En esta unidad hemos visto cuál es el significado de un dato estructurado, el record, especialmente dedicado a agrupar datos más simples y heterogéneos que son los campos y que se distinguen, cada uno de ellos, por un nombre identificador. Hemos visto su definición en lenguaje algorítmico y también en lenguaje de programación.

También hemos visto que la estructuración de datos se puede hacer crecer tantas veces como sea necesario y que los campos de un dato estructurado pueden ser también datos complejos, ya sean por su parte records, vectores o matrices. Además, hemos visto cómo referenciar un dato estructurado en su totalidad o bien solo alguna de las informaciones discretas que contiene.

Desde la vertiente más técnica, también se ha mostrado de qué manera se mantienen en memoria los datos estructurados y cómo se localiza en ella la ubicación de un campo determinado.

Etiquetas:
Creado por editor1 el 2018/09/17 10:01