domingo, 23 de noviembre de 2014

Push en una Pila

Cuando nos explican el concepto de Pilas lo principal es que: "el primero que entra es el primero que sale":


con esto parece que tenemos toda la teoría aprendida, el problema viene cuando intentamos implementarlo y no sabemos por dónde empezar.

Por lo general cuando se explica este tema se hace en C++ y utilizando estructuras, por tanto antes de continuar esta explicación os recomiendo leer previamente el post relacionado a estructuras

Vemos en la imagen que cada elemento de la pila está formado por un círculo con un valor y una flecha que lo une con el siguiente círculo, con estos dos elementos formaremos un elemento de la Pila, de esta forma:

struct ElemPila{
  int valor; //esto representa el valor que está dentro del círculo
  struct ElemPila * siguienteElem; //esto representa la flecha, observar que es un puntero
};

Hasta aquí tenemos definida la estructura, pero aún no la estamos utilizando, ahora veremos como desde el programa utilizamos esta esta estructura para agregar los elementos: 1, 2 y 3:

int main()
{
  / /Creamos el primer elemento, con valor 1 y la flecha que apunta al siguiente elemento apunta a NULL
   struct ElemPila* elem1 = new ElemPila; //reserva memoria para el contenido de la estructura
   elem1->valor = 1; //inicializa el valor de la estructura
   elem1->siguienteElem = NULL; //inicializa el valor hacia donde apunta el siguiente
   
   struct ElemPila* elem2 = new ElemPila;
   elem2->valor = 2;
   elem2->siguienteElem = NULL;
   
   struct ElemPila* elem3 = new ElemPila;
   elem3->valor = 3;
   elem3->siguienteElem = NULL;
      
   return 0;
}

Al ejecutar este programa lo que logramos es tener 3 elementos independientes, así:


Como paso siguiente modificaremos el programa para introducir el puntero que será cabeza de la pila e introduciremos sólo el primer elemento a la pila:

int main()
{
   struct ElemPila* elem1 = new ElemPila;
   elem1->valor = 1;
   elem1->siguienteElem = NULL;
   
   struct ElemPila* elem2 = new ElemPila;
   elem2->valor = 2;
   elem2->siguienteElem = NULL;
   
   struct ElemPila* elem3 = new ElemPila;
   elem3->valor = 3;
   elem3->siguienteElem = NULL;
   
   struct ElemPila* cabezaPila; //creamos el puntero
   cabezaPila = elem1; //hacemos que apunte al primer elemento
   
   return 0;
}



Para incluir el segundo elemento en la pila, lo que tenemos que hacer es que el segundo elemento apunte al primero (elem1) y la cabeza de pila apunte al segundo:

int main()
{
   struct ElemPila* elem1 = new ElemPila;
   elem1->valor = 1;
   elem1->siguienteElem = NULL;

   struct ElemPila* elem2 = new ElemPila;
   elem2->valor = 2;
   elem2->siguienteElem = NULL;

   struct ElemPila* elem3 = new ElemPila;
   elem3->valor = 3;
   elem3->siguienteElem = NULL;

   struct ElemPila* cabezaPila;
   cabezaPila = elem1;

   elem2-> siguienteElem = cabezaPila;
   cabezaPila = elem2;

   return 0;
}


Finalmente incluimos el tercer elemento, siguiendo los mismo pasos que el anterior, que es hacer que el siguiente de elemento 3 apunte al elemento 2 y la cabeza de la pila apunte a elemento 3:

int main()
{
   struct ElemPila* elem1 = new ElemPila;
   elem1->valor = 1;
   elem1->siguienteElem = NULL;
 
   struct ElemPila* elem2 = new ElemPila;
   elem2->valor = 2;
   elem2->siguienteElem = NULL;
 
   struct ElemPila* elem3 = new ElemPila;
   elem3->valor = 3;
   elem3->siguienteElem = NULL;
 
   struct ElemPila* cabezaPila;
   cabezaPila = elem1;
 
   elem2->siguienteElem = cabezaPila;
   cabezaPila = elem2;
 
   elem3->siguienteElem = cabezaPila;
   cabezaPila = elem3;
 
   return 0;
}



Si observamos atentamente el código y las imágenes veremos que las variables elem1 y elem2 se quedan apuntando a elementos de la pila que en teoría no se deberían acceder directamente (el acceso es sólo con la cabeza de pila), por tanto podríamos deducir que con una sola variable de tipo ElemenPila sería suficiente. De la misma forma podemos observar en el código que el mecanismo para agregar un elemento es el mismo, por lo tanto podríamos utilizar una función para reutilizar el código. todo esto de esta forma:

#include <iostream>

struct ElemPila{
  int valor;
  struct ElemPila * siguienteElem;
};

using namespace std;


//incluimos una función de impresión para comprobar el orden en que se han guardado
void imprimir(struct ElemPila* cabezaPila)
{
    struct ElemPila* auxiliar = cabezaPila;
    while(auxiliar != NULL)
    {
      cout << auxiliar->valor << " - ";
      auxiliar = auxiliar -> siguienteElem;
    }
    cout << "=====================\n";
}

//función que realiza el push de forma genérica para cualquier elemento
struct ElemPila* push(int pValor, struct ElemPila* elem, struct ElemPila* cabezaPila)
{
   elem = new ElemPila;    
   elem->valor = pValor;
   elem->siguienteElem = NULL;
   elem->siguienteElem = cabezaPila;
   cabezaPila = elem;
   return cabezaPila;
}

int main()
{
   struct ElemPila* cabezaPila = NULL;
   struct ElemPila* elem = NULL;

   cabezaPila = push(1, elem, cabezaPila);   
   imprimir(cabezaPila);
       
   cabezaPila = push(2, elem, cabezaPila);   
   imprimir(cabezaPila);
   
   cabezaPila = push(3, elem, cabezaPila);   
   imprimir(cabezaPila);
   
   return 0;
}


Executing the program....
1 - =====================
2 - 1 - =====================
3 - 2 - 1 - =====================

martes, 21 de octubre de 2014

El elemento neutro de la suma y el producto

Hay recuerdos muy lejanos al aprender a sumar o a multiplicar, en el cole (o en la universidad) nos explicaban las propiedades de la suma y del producto: propiedad transitiva, propiedad distributiva, etc. una de las propiedades (generalmente la primera) es la propiedad del elemento neutro que dice:

A todo número que se le sume cero da el mismo número. -> El 0 es el elemento neutro de la suma

A todo número que se le multiplique por uno da el mismo número. -> El 1 es el elemento neutro de la multiplicación.

¿A que viene todo esto del elemento neutro? Esto va relacionado a operaciones que hacemos de forma acumulativa, por ejemplo:

1. Calcule la suma de los N primeros números enteros.
2. Calcula el factorial de un número N.

En ambos casos, una de las soluciones por las que se podría optar es hacer un bucle que acumule la suma o el producto de esta forma:

//ejemplo 1
Para i=1 hasta N hacer
  suma = suma + i;
Fin_Para

//ejemplo 2
Para i=1 hasta N hacer
  factorial = factorial *  i;
Fin_Para

En ambos casos al estar calculando un valor acumulado, reutiliza el valor de la variable en la iteración anterior (suma utiliza a suma para calcularse a si misma, lo mismo con factorial).
¿Pero qué pasa en la primera iteración? En la primera iteración es necesario que suma y factorial simplemente sean igual a "i", por este motivo es necesario (indispensable) inicializar ambas variables con el valor del elemento neutro. Los algoritmos completos quedarían así:

//ejemplo 1
suma = 0: //variable inicializada con el valor del elemento neutro de la operación que haremos
Para i=1 hasta N hacer
  suma = suma + i //la primera iteración: suma = 0 + i;
Fin_Para

//ejemplo 2
factorial = 1 //variable inicializada con el valor del elemento neutro de la operación que haremos
Para i=1 hasta N hacer
  factorial = factorial *  i: //la primera iteración:  factorial = 1 * i
Fin_Para

Para cualquier cálculo en que utilicemos acumuladores es absolutamente necesario inicializarlos correctamente, por lo general con el valor del elemento neutro de la operación de acumulación.

jueves, 2 de octubre de 2014

Árboles binarios, un ejemplo práctico

Muchas veces nos es difícil entender para qué nos enseñan algo sin explicarnos la aplicación. El tiempo que llevo dando clases particulares he podido ver apuntes con mucho contenido y con pocos ejemplos prácticos, esto desmotiva mucho porque el estudiante piensa ¿Para qué me enseñan esto?.

Esto pasa generalmente con el tema de árboles, te explican los tipos de árboles existen (binarios, múltiples, balanceados, etc) y cómo recorrerlos (pre-orden, post-orden, etc), sin embargo son muy pocos los ejemplos prácticos que he podido encontrar.

El objetivo de este post es mostrar un ejemplo práctico en que se podría hacer uso de los árboles, aunque también se puede hacer uso de otras soluciones.

Tenemos un fichero de texto que contiene en cada linea la edad de personas encuestadas para un estudio de mercado, lo que nos interesa es leer el fichero y mostrar en pantalla la cantidad de repeticiones por cada edad, es decir la edad que más se repiten en la muestra.

Partimos de la premisa que la información no está ordenada y es una muestra de 1000 personas.

Utilizaremos para cada número una estructura (o clase) que almacene el valor de la edad y el valor de la repetición:

public struct elemMuestra
{
public int edad;
public int frecuencia;
}

El algoritmo que podríamos plantear es el siguiente:

1. Leer número del fichero
2. Buscar el número en la estructura ordenada
2.1. Si el número existe en la estructura, incrementar el número de repeticiones
2.1. Si el número NO existe en la estructura, inserto el número en la estructura
3. Cuando termino de leer todos los números del fichero, imprimo cada número con su frecuencia de repetición guardada en la estructura.

En el punto 2 vemos que utilizamos una estructura ordenada, aquí podríamos utilizar un vector, una lista de punteros, un árbol binario,etc. lo importante es que la estructura que definamos nos sea útil tanto para buscar si el número ya existe en la estructura, como para insertar un nuevo número en la estructura.

Si utilizamos un vector, podríamos realizar una búsqueda binaria, debido a que los índices del vector ayudan a este tipo de búsqueda, sin embargo si utilizamos una lista de punteros o un árbol binario tendríamos que usar una búsqueda secuencial, pero sin duda el recorrido en un árbol podría ser mucho más eficiente que el recorrido en una lista de punteros, debido a que la forma del árbol no requiere recorrer a todos sus elementos sino que al tener un orden binario es como si realmente se hiciera una búsqueda binaria, a la búsqueda en un árbol binario se le llama búsqueda Silaina.

El otro punto a evaluar es el tema de la inserción, cuando ya sabemos que el número que hemos leído de fichero aún no existe en la estructura que hemos escogido, utilizar un vector podría ser lo menos eficiente, puesto que si tenemos ordenados 900 números en 20 edades distintas, y llega una nueva edad que es la menor de todas, debemos desplazar todas las edades a una casilla posterior para insertar la nueva edad de forma ordenada, ese desplazamiento puede ser muy costoso. En una lista de punteros la inserción es relativamente sencilla y en un árbol también.

A nivel de programación los árboles son más fáciles de trabajar con funciones recursivas, por tanto si tenéis poca experiencia en funciones recursivas y la cantidad de elementos de la muestra no es tan grande, puede ser más sencillo de programar con vectores + búsqueda binaria o secuencial, sin embargo si veis que la muestra es muy grande y luego vais a realizar muchas búsquedas sobre la estructura cargada y ordenada es mejor trabajarla con árboles binarios + búsqueda sobre árboles binarios.

Ahora bien, para que la búsqueda en un árbol binario sea aun más eficiente deberíamos lograr que el árbol tenga el menor numero de niveles, para esto se realiza lo que se llama el balanceo de árboles:



domingo, 14 de septiembre de 2014

La importancia de pensar

Hace un tiempo hice un ejercicio de memoria para recordar exactamente qué fue lo que me motivó a ser Ing. Informática, la respuesta está en mi colegio, cuando tenia 8 años se introdujo una nueva asignatura que se llamaba "Computación", no era la primera vez que veía un ordenador (ya tenía uno en casa que sólo usaba para jugar), pero era la primera vez que aprendía a usar un ordenador para resolver problemas simples de matemáticas.

El lenguaje que utilizábamos era el Pascal, la clase consistía en leer atentamente un problema y luego dibujar el diagrama de flujo para resolverlo, el profesor pasaba por cada uno de nuestros sitios según íbamos terminando y revisado el diagrama podíamos empezar a programar, nunca antes!! porque lo importante era que se validara que hubiéramos entendido lo que se pedía en el problema y que tengamos claro cómo lo íbamos a enfrentar. Lo más importante de esta clase era la metodología para solucionar los problemas, pensar y luego programar! tuve la suerte de caer en esa clase, fue la base fundamental para decantarme por la rama tecnológica.

Ayer encontré un artículo muy interesante del Blog Tiching en el que plantea los beneficios de que los niños aprendan a programar no solo para fomentar una orientación científica-tecnológica sino para que se acostumbren a estructurar el planteamiento de cualquier tipo de problema. Os recomiendo mucho leer el post, los beneficios de programar con metodología no sólo sirven en un campo puramente técnico sino que sirven para ejercitar y acostumbrarnos a entender los objetivos, plantear estrategias y ejecutar acciones para lograr a cumplir nuestros objetivos.

Cada curso conozco alumnos diferentes, y a pesar de ser de distintos años de carrera, distintas edades e incluso diferentes carreras, todos creen que terminaremos la clase con un programa hecho, en realidad prácticamente no utilizamos el ordenador, lo que se llevan a casa son muchos papeles llenos de pseudocódigo y con las ideas muy claras: Objetivo y Estrategia, esa es la finalidad de la clase, teniendo estas dos cosas, la resolución sale sola.

El próximo post será más técnico sólo quería hacer esta "pausa" por que es parte del aprendizaje entender que antes de programar se debe pensar.


viernes, 1 de agosto de 2014

Más acerca de booleanos

Como ya he comentado en uno de los primeros post Las Variables Booleanas sólo toman dos valores: cierto o falso y su definición depende del lenguaje del programación.

En este post veremos que podemos utilizar variables de tipo entero que cumplan la misma función, asumiendo que sólo podrán tomar dos valores (dos números). En C++ se utilizan con mayor frecuencia los enteros porque el mismo lenguaje asume que el valor 0 hace la función de falso y el valor distinto de 0 hace la función de cierto.

Por ejemplo, el siguiente programa utiliza la variable i para mostrar una cuenta regresiva desde 10 hasta 1 y también sirve como variable de condición de salida para el while:

int main()
{
   //variable i sirve para la impresión y como tope del while
   int i = 10;
   
   //poniendo simplemente la i como condición significa que          
    //iterará mientras la variable i sea diferente de cero
   while(i)
   {
      cout << "i = " << i << endl; 
      i--;//decrementa i hasta que llega a 0 y sale del bucle
   }
   return 0;

}

La salida del programa es esta:

i = 10
i = 9
i = 8
i = 7
i = 6
i = 5
i = 4
i = 3
i = 2
i = 1

Con esto podemos ver que es muy importante conocer las capacidades de cada lenguaje de programación para que se adapten mejor a una solución o a un estilo de programar.

Ahora mostraré otro ejemplo en C++ en el que utilizaré la lógica inversa. El siguiente programa muestra un bucle que itera siempre que la condición No sea igual a 0, para eso se utiliza el símbolo ! que invierte el valor de la variable evaluada:

int main()
{
    //variable i se inicializa con valor 0 (falso)
   int i = 0;
    //Itera mientras el resultado de la parte entera de la división sea igual a 0
   while(!(i/3))
   {
      cout << "i = " << i << endl; 
      i++;
   }
   return 0;
}

La salida del programa es esta:

i = 0
i = 1
i = 2


En conclusión, sabemos que para las instrucciones condicionales y los bucles tenemos la opción de utilizar booleanos, expresiones lógicas y dependiendo del lenguaje de programación también números enteros.




domingo, 20 de julio de 2014

Algo de matrices

Una de las dificultades más comunes son los vectores, sobretodo comprender la diferencia entre el indice de un vector (posición) y el valor contenido en el índice del vector (datos).

Para complicarlo aún más viene el tema de las matrices, que no son más que vectores pero en dos dimensiones, esto es que ahora el índice está formado por dos posiciones.

Sobre teoría de matrices se ha escrito mucho, lo que pretendo en este post es simplemente mostrar como acceder a una matriz y determinar si es una matriz identidad o no (¿Qués es una matriz identidad?). Para este fin utilizaremos el recorrido secuencial de una matriz fila a fila y para cada elemento de la fila veremos si tiene el valor correcto que le corresponde a la matriz identidad (1 en la diagonal y 0 en el resto de campos):

int esIdentidad = 1; //variable que indica si la matriz es identidad o no
int x, y = 0; //indice de filas y columnas
int N=10;//dimensión de la matriz

//se asumen que la matriz es de Identidad hasta que se muestre que tenga un valor incorrecto
while(esidentidad)
{
  //recorre todos los elementos de la fila hasta que encuentre algún error o termine la fila
  for(int y=0; y<N && esIdentidad; y++)
  {
    //la diagonal debe tener el valor 1, en caso contrario no puede ser una matriz Identidad
    if(x==y && Matriz[x][y] !=1) esidentidad = 0;
    //los que no están en la diagonal deben ser 0, en caso contrario no puede ser una matriz identidad
    if(x!=y && Matriz[x][y] !=0) esidentidad = 0;
  }
  x++;//cada iteración del while se corresponde con cada fila de la matriz  
}

//resultado de la evaluación
if(esIdentidad) print("La matriz es identidad");
else print("La matriz no es identidad");


Vemos que el el indice de la  matriz está compuesto por dos posiciones: x e y.
Vemos que el valor en una posición determinada de la matriz se accede a través de las posiciones: Matriz[x][y].
Vemos que cuando encuentra un valor que no se corresponde con la matriz identidad cambia la variable esIdentidad para salir inmediatamente de los bucles for y while, puesto que al primer valor incorrecto no hace falta seguir recorriendo la matriz, ya sabemos que no es de Identidad.
Vemos que para recorrer una matriz hace falta 2 bucles anidados, en este caso el while recorre las filas de la matriz y el for recorre las columnas para cada fila.



domingo, 23 de marzo de 2014

Recursividad

Se dice que una función es recursiva cuando su ecuación de solución se utiliza a si misma para resolverse.

Un ejemplo típico de recursividad es la función Factorial, sabemos que el Factorial de un numero es igual al producto de dicho número multiplicado por el factorial del número anterior:

Factorial(4) = 4 * Factorial(3)

Vemos que está definiendo la solución del factorial empleando la misma fórmula del factorial, puesto que:

Factorial(3) = 3 * Factorial(2)
Factorial(2) = 2 * Factorial(1)
....

Pero, ¿Cuándo termina la recursividad? Es absolutamente necesario definir lo que se llama un TOPE, que es cuando la recursividad termina, sino seguiríamos hasta el infinito.

Para el caso del ejemplo típico del factorial, se sabe que esta función se aplica a un conjunto de números enteros mayores o iguales a 0, del cual Factorial(0) = 1, siendo esta fórmula la única que no utiliza la recursividad para su definición, por tanto:

Toda función recursiva necesita un TOPE, dicho tope es una de la soluciones de la función que no se define por si misma.

Si generalizamos podemos decir que, Para toda N tal que N>=0, Factorial(N) se resuelve con las siguientes ecuaciones:

Si N=0, Factorial(N) = 1
Si N>0, Factorial(N) = N * Factorial(N-1)
Para cualquier otro valor de N, No existe conjunto solución para Factorial(N)

Otra característica importante de una función recursiva es que al llamarse a si misma los parámetros deben variar, sino entrará en bucle infinito, por ejemplo esto sería un error:

Factorial(N) = N * Factorial(N)     ---> En este caso caerá en bucle infinito porque NUNCA llegará al TOPE

Por tanto:

En toda función recursiva es necesario que la llamada si misma garantice que en algún punto llegará al TOPE. 

//ejemplo de función recursiva
//asumimos que el valor N que ingresa es >=0
int factorial (int N)
{
    //lo primero que definimos en una función recursiva el el TOPE
    if(N == 0) return 1;
    //La llamada recursiva con N-1 asegura que en algún momento llegará a ejecutar factorial(0)
    return N * Factorial(N-1);
}


¿Por qué es importante definir el tope al principio?
¿Podríamos utilizar la operación unaria -- para llamar de forma recursiva: Factorial(N--)?










domingo, 9 de febrero de 2014

Inserción de valores en un vector ordenado

En esta ocasión trabajaremos una estructura fija (vector) como si fuese una estructura variable, esta solución se puede aplicar en escenarios en los cuales sabemos que:

- Como máximo utilizaremos un número determinado de elementos.
- La inicialización (inserción) se realiza una vez o con muy poca frecuencia,
- Y se necesita acceder continuamente a elementos en concreto.

Por ejemplo, sabemos que en un aula no se puede exceder de los 100 alumnos, la matrícula se realiza al principio de curso y durante el curso se puede acceder en cualquier momento al expediente de cualquiera de los alumnos matriculados.
En un aula podrían inscribirse 25 alumnos y en otra 82 alumnos, si se quisiera imprimir un listado de los alumnos matriculados en cada aula no es necesario recorrer el vector de 100 elementos, sino que en el primero se recorrerá 25 veces y en el segundo 82.

Para hacer uso de los vectores de esta forma utilizaremos una variable auxiliar que representará el número de elementos utilizados, así cada vez que necesitemos hacer una operación sobre todos los elementos no recorreremos todos los elementos del vector sino todos los utilizados.

int listaOrdenada[N];
int numelem = 0; //cantidad de elementos ocupados de la lista

Ahora complicaremos un poco el problema y suponemos que los alumnos cuando se matricula no vienen de forma ordenada, pero se requiere almacenarlos de forma ordenada por apellido, esto implica que cada vez que se agrega un alumno a la lista de matriculados se debe buscar la posición del vector en la cual se debe insertar, no siempre será el último.

Este tipo de problema utiliza varios de los conceptos vistos en entradas anteriores, tales como:

- El uso de vectores para almacenar información del mismo tipo.
- El uso de vectores como estructura que ocupa un espacio de memoria fija de la cuál sólo utilizaremos una parte de forma dinámica.
- El uso de los iteradores for y while para realizar una acción sobre cada elemento de un vector.
- Recorridos de vectores en forma ascendente y en forma descendente
- El uso de comprador if para determinar la posición en la cual se debe insertar el nuevo elemento.

Para simplificar el problema supondremos que los alumnos son números enteros y sus apellidos son números mayores que cero y os invito a que modifiquéis el código para adaptarlo a un vector de una estructura de alumnos que incluya los nombres y los apellidos.

Os dejo el código a continuación en C:

#include <stdio.h>
#include <stdlib.h>
#define N 100


int main(int argc, char *argv[])
{
  //definición de variables
  int listaOrdenada[N];
  int numelem = 0; //cantidad de elementos ocupados de la lista
  int numero;
  int i, j;
  //inicializa vector
  for(i=0; i<N; i++) listaOrdenada[i] = 0;

  //proceso
  //mensajes iniciales
  printf("Ingrese los numeros de la lista \n\nLos numeros deben ser mayores que 0 \nPara introducir pulse <ENTER> \n");
  printf("como maximo puede ingresar %d numeros \n", N);
  printf("Para finalizar pulse 0 y <ENTER>\n");

  //Lee valor a ingresar en el vector
  printf("Numero: ");
  scanf("%d", &numero);
  printf("Numero ingresado %d \n", numero);

  while(numero != 0 && numelem < N)
  {
    i = 0;
    //ubica la posición para insertar el número
    while(i<numelem && listaOrdenada[i] < numero) i++;
    //en el caso que se inserte en la última posición: i == numelem
    if(i==numelem) {
       listaOrdenada[i]=numero;
       numelem++;
    }
    else{
       //tiene que desplazar a todos los elementos posteriores
       j = numelem;
       numelem++;
       //recorre la lista de forma inversa para desplazar los valores
       while(j>i){
         listaOrdenada[j] = listaOrdenada[j-1];
         j--;
       }
       listaOrdenada[i] = numero;
    }
 
    //imprime la lista de elementos 
    for(i=0; i<numelem; i++) printf("%d \t",listaOrdenada[i]);

    printf("\n\nNumero: ");
    scanf("%d", &numero);
    printf("Numero ingresado %d \n", numero);

  }

  printf("Elementos utilizados: %d \n", numelem);

  system("PAUSE");
  return 0;
}



martes, 21 de enero de 2014

Listas dinámicas

La manera más práctica de trabajar con una lista de objetos del mismo tipo es un Vector (o Array), cuando se instancia un vector se está reservando en memoria una cantidad fija de bytes para almacenar la información, así si tenemos:

int myList[5]; //reserva espacio para 5 variables de tipo int
myList[0] = 1; //almacena el valor 1 en la primera posición del vector

Ahora utilizaré un ejemplo de un problema tipo de examen, habla sobre el registro de reclamaciones de un comercio, cada reclamación debe contener:
  • El nombre de la persona que realiza la reclamación
  • La fecha de la reclamación
  • La descripción de la reclamación
  • El estado de la reclamación: 0 si está abierta, 1 si está resuelta.

Para representar una reclamación podemos utilizar una estructura de este tipo:

typedef struct reclamacion
{
String nombrePersona;
Date fechaReclamacion;
String descripcion;
int estado;
}Reclamacion;

Ahora creamos un Vector para almacenar la lista de reclamaciones:
Reclamacion Lista[100]; //vector de capacidad para 100 reclamaciones

Pero, y ¿Qué pasa si hay más de 100 reclamaciones?

Cuando no sabemos la dimensión que puede tomar una lista lo mejor es reservar memoria de forma dinámica, para eso podemos utilizar los punteros.

Así, sólo creamos una variable de tipo puntero que apunte a la estructura, y modificamos la estructura para que apunte al siguiente elemento de la lista, así;

typedef struct reclamacion
{
String nombrePersona;
Date fechaReclamacion;
String descripcion;
int estado;
struct reclamacion * siguiente; //este puntero apunta a la siguiente reclamación
}Reclamacion;

Reclamacion * listaReclamaciones;
listaReclamaciones = NULL; //inicializamos el puntero a NULL hasta que empiecen a llegar las reclamaciones

//creamos la primera reclamación
Reclamacion * rec1 = new Reclamacion;
rec1.nombrePersona = “Juan”;
rec1.fechaReclamacion = new Date(); //representa que toma la fecha actual (esto varía según el lenguaje de programación)
rec1.descripcion = “ejemplo de reclamacion1”;
rec1.estado = 0; //lo inicializa a 0 porque cuando se crea está abierta
rec1->siguiente = NULL; //inicializa a NULL porque es un puntero

Ahora para agregar la primera reclamación a la lista hacemos que la variable listaReclamaciones ya no apunte a NULL sino que apunte al elemento rec1:

listaReclamaciones = rec1;

Para el caso de una segunda reclamación es distinto y dependerá si se quiere agregar al principio de la lista o al final de la lista, vamos a agregarlo al final, así listaReclamaciones apuntará a rec1 y rec1->siguiente apuntará a rec2:

//creamos la segunda reclamación
Reclamacion * rec2 = new Reclamacion;
rec2.nombrePersona = “Pedro”;
rec2.fechaReclamacion = new Date(); 
rec2.descripcion = “ejemplo de reclamacion2”;
rec2.estado = 0; 
rec2->siguiente = NULL; 

Si hacemos:
listaReclamaciones->siguiente = rec2; //es lo mismo que rec1->siguiente = rec2

Tenemos el problema resuelto, rec1->siguiente ya no apunta a NULL sino que apunta a rec2 y rec2 es la última de la lista.

¿Y que pasa si no sabemos la cantidad de elementos que tiene la lista y queremos agregar un elemento al final?
La solución es tomar un puntero auxiliar y desplazamos hasta el último elemento de la lista y hacer que este último elemento apunte al elemento nuevo que queremos agregar a la lista:

//puntero auxiliar que se desplaza hasta el último elemento
Reclamacion * pAuxiliar = listaReclamaciones; //se inicializa apuntando al primero de la lista

//mientras el siguiente del auxiliar es distinto a nulo, avanza el puntero al siguiente elemento, hasta llegar al último
while(pAuxiliar->siguiente != NULL) pAuxiliar = pAuxiliar->siguiente;
//ahora agregamos el nuevo elemento al final de la lista
pAuxiliar-> siguiente = nuevaRec; //nuevaRec es el nuevo elemento de tipo reclamacion*

Existen muchas variantes:

  • agregar el elemento al inicio de la lista
  • agregar el elemento en medio de la lista (por ejemplo para una lista ordenada)