Capítulo 1 Listas abiertas

1.1 Definición

La forma más simple de estructura dinámica es la lista abierta. En esta forma los nodos se organizan de modo que cada uno apunta al siguiente, y el último no apunta a nada, es decir, el puntero del nodo siguiente vale NULL.

En las listas abiertas existe un nodo especial: el primero. Normalmente diremos que nuestra lista es un puntero a ese primer nodo y llamaremos a ese nodo la cabeza de la lista. Eso es porque mediante ese único puntero podemos acceder a toda la lista.

Cuando el puntero que usamos para acceder a la lista vale NULL, diremos que la lista está vacía.

El nodo típico para construir listas tiene esta forma:

struct nodo {
   int dato;
   struct nodo *siguiente;
};

En el ejemplo, cada elemento de la lista sólo contiene un dato de tipo entero, pero en la práctica no hay límite en cuanto a la complejidad de los datos a almacenar.

1.2 Declaraciones de tipos para manejar listas en C

Normalmente se definen varios tipos que facilitan el manejo de las listas, en C, la declaración de tipos puede tener una forma parecida a esta:

typedef struct _nodo {
   int dato;
   struct _nodo *siguiente;
} tipoNodo;

typedef tipoNodo *pNodo;
typedef tipoNodo *Lista;

tipoNodo es el tipo para declarar nodos, evidentemente.

pNodo es el tipo para declarar punteros a un nodo.

Lista es el tipo para declarar listas, como puede verse, un puntero a un nodo y una lista son la misma cosa. En realidad, cualquier puntero a un nodo es una lista, cuyo primer elemento es el nodo apuntado.

Lista enlazada
Lista enlazada

Es muy importante que nuestro programa nunca pierda el valor del puntero al primer elemento, ya que si no existe ninguna copia de ese valor, y se pierde, será imposible acceder al nodo y no podremos liberar el espacio de memoria que ocupa.

1.3 Operaciones básicas con listas

Con las listas tendremos un pequeño repertorio de operaciones básicas que se pueden realizar:

  • Añadir o insertar elementos.
  • Buscar o localizar elementos.
  • Borrar elementos.
  • Moverse a través de una lista, anterior, siguiente, primero.

Cada una de estas operaciones tendrá varios casos especiales, por ejemplo, no será lo mismo insertar un nodo en una lista vacía, o al principio de una lista no vacía, o la final, o en una posición intermedia.

1.4 Insertar elementos en una lista abierta

Veremos primero los casos sencillos y finalmente construiremos un algoritmo genérico para la inserción de elementos en una lista.

Insertar un elemento en una lista vacía

Lista vacía
Lista vacía

Este es, evidentemente, el caso más sencillo. Partiremos de que ya tenemos el nodo a insertar y, por supuesto un puntero que apunte a él, además el puntero a la lista valdrá NULL:

El proceso es muy simple, bastará con que:

  1. nodo->siguiente apunte a NULL.
  2. Lista apunte a nodo.

Insertar un elemento en la primera posición de una lista

Insertar al principio
Insertar al principio

Podemos considerar el caso anterior como un caso particular de éste, la única diferencia es que en el caso anterior la lista es una lista vacía, pero siempre podemos, y debemos considerar una lista vacía como una lista.

Insertado al principio
Insertado al principio

De nuevo partiremos de un nodo a insertar, con un puntero que apunte a él, y de una lista, en este caso no vacía:

El proceso sigue siendo muy sencillo:

  1. Hacemos que nodo->siguiente apunte a Lista.
  2. Hacemos que Lista apunte a nodo.

Insertar un elemento en la última posición de una lista

Este es otro caso especial. Para este caso partiremos de una lista no vacía:

Insertar al final
Insertar al final

El proceso en este caso tampoco es excesivamente complicado:

  1. Necesitamos un puntero que señale al último elemento de la lista. La manera de conseguirlo es empezar por el primero y avanzar hasta que el nodo que tenga como siguiente el valor NULL.
  2. Hacer que nodo->siguiente sea NULL.
  3. Hacer que ultimo->siguiente sea nodo.
Insertado al principio
Insertado al principio

Insertar un elemento a continuación de un nodo cualquiera de una lista

De nuevo podemos considerar el caso anterior como un caso particular de este. Ahora el nodo "anterior" será aquel a continuación del cual insertaremos el nuevo nodo:

Insertar dentro
Insertar dentro

Suponemos que ya disponemos del nuevo nodo a insertar, apuntado por nodo, y un puntero al nodo a continuación del que lo insertaremos.

El proceso a seguir será:

  1. Hacer que nodo->siguiente señale a anterior->siguiente.
  2. Hacer que anterior->siguiente señale a nodo.
Insertado dentro
Insertado dentro

1.5 Localizar elementos en una lista abierta

Muy a menudo necesitaremos recorrer una lista, ya sea buscando un valor particular o un nodo concreto. Las listas abiertas sólo pueden recorrerse en un sentido, ya que cada nodo apunta al siguiente, pero no se puede obtener, por ejemplo, un puntero al nodo anterior desde un nodo cualquiera si no se empieza desde el principio.

Para recorrer una lista procederemos siempre del mismo modo, usaremos un puntero auxiliar como índice:

  1. Asignamos al puntero índice el valor de Lista.
  2. Abriremos un bucle que al menos debe tener una condición, que el índice no sea NULL.
  3. Dentro del bucle asignaremos al índice el valor del nodo siguiente al índice actual.

Por ejemplo, para mostrar todos los valores de los nodos de una lista, podemos usar el siguente bucle en C:

typedef struct _nodo {
   int dato;
   struct _nodo *siguiente;
} tipoNodo;

typedef tipoNodo *pNodo;
typedef tipoNodo *Lista;
...
pNodo indice;
...
indice = Lista;
while(indice) {
   printf("%d\n", indice->dato);
   indice = indice->siguiente;
}
...

Supongamos que sólo queremos mostrar los valores hasta que encontremos uno que sea mayor que 100, podemos sustituir el bucle por:

...
indice = Lista;
while(indice && indice->dato <= 100) {
   printf("%d\n", indice->dato);
   indice = indice->siguiente;
}
...

Si analizamos la condición del bucle, tal vez encontremos un posible error: ¿Qué pasaría si ningún valor es mayor que 100, y alcancemos el final de la lista?. Podría pensarse que cuando indice sea NULL, si intentamos acceder a indice->dato se producirá un error.

En general eso será cierto, no puede accederse a punteros nulos. Pero en este caso, ese acceso está dentro de una condición y forma parte de una expresión "and". Recordemos que cuando se evalúa una expresión "and", se comienza por la izquierda, y la evaluación se abandona cuando una de las expresiones resulta falsa, de modo que la expresión "indice->dato <= 100" nunca se evaluará si indice es NULL.

Si hubiéramos escrito la condición al revés, el programa nunca funcionaría bien. Esto es algo muy importante cuando se trabaja con punteros.

1.6 Eliminar elementos en una lista abierta

De nuevo podemos encontrarnos con varios casos, según la posición del nodo a eliminar.

Eliminar el primer nodo de una lista abierta

Eliminar primer nodo
Eliminar primer nodo
Primer nodo eliminado
Primer nodo eliminado

Es el caso más simple. Partiremos de una lista con uno o más nodos, y usaremos un puntero auxiliar, nodo:

  1. Hacemos que nodo apunte al primer elemento de la lista, es decir a Lista.
  2. Asignamos a Lista la dirección del segundo nodo de la lista: Lista->siguiente.
  3. Liberamos la memoria asignada al primer nodo, el que queremos eliminar.

Si no guardamos el puntero al primer nodo antes de actualizar Lista, después nos resultaría imposible liberar la memoria que ocupa. Si liberamos la memoria antes de actualizar Lista, perderemos el puntero al segundo nodo.

Si la lista sólo tiene un nodo, el proceso es también válido, ya que el valor de Lista->siguiente es NULL, y después de eliminar el primer nodo la lista quedará vacía, y el valor de Lista será NULL.

De hecho, el proceso que se suele usar para borrar listas completas es eliminar el primer nodo hasta que la lista esté vacía.

Eliminar un nodo cualquiera de una lista abierta

En todos los demás casos, eliminar un nodo se puede hacer siempre del mismo modo. Supongamos que tenemos una lista con al menos dos elementos, y un puntero al nodo anterior al que queremos eliminar. Y un puntero auxiliar nodo.

Eliminar un nodo
Eliminar un nodo
Nodo eliminado
Nodo eliminado

El proceso es parecido al del caso anterior:

  1. Hacemos que nodo apunte al nodo que queremos borrar.
  2. Ahora, asignamos como nodo siguiente del nodo anterior, el siguiente al que queremos eliminar: anterior->siguiente = nodo->siguiente.
  3. Eliminamos la memoria asociada al nodo que queremos eliminar.

Si el nodo a eliminar es el último, es procedimiento es igualmente válido, ya que anterior pasará a ser el último, y anterior->siguiente valdrá NULL.

1.7 Moverse a través de una lista abierta

Sólo hay un modo de moverse a través de una lista abierta, hacia delante.

Aún así, a veces necesitaremos acceder a determinados elementos de una lista abierta. Veremos ahora como acceder a los más corrientes: el primero, el último, el siguiente y el anterior.

Primer elemento de una lista

El primer elemento es el más accesible, ya que es a ese a que apunta el puntero que define la lista. Para obtener un puntero al primer elemento bastará con copiar el puntero Lista.

Elemento siguiente a uno cualquiera

Supongamos que tenemos un puntero nodo que señala a un elemento de una lista. Para obtener un puntero al siguiente bastará con asignarle el campo "siguiente" del nodo, nodo->siguiente.

Elemento anterior a uno cualquiera

Ya hemos dicho que no es posible retroceder en una lista, de modo que para obtener un puntero al nodo anterior a uno dado tendremos que partir del primero, e ir avanzando hasta que el nodo siguiente sea precisamente nuestro nodo.

Último elemento de una lista

Para obtener un puntero al último elemento de una lista partiremos de un nodo cualquiera, por ejemplo el primero, y avanzaremos hasta que su nodo siguiente sea NULL.

Saber si una lista está vacía

Basta con comparar el puntero Lista con NULL, si Lista vale NULL la lista está vacía.

1.8 Borrar una lista completa

El algoritmo genérico para borrar una lista completa consiste simplemente en borrar el primer elemento sucesivamente mientras la lista no esté vacía.

1.9 Ejemplo de lista abierta ordenada en C

Supongamos que queremos construir una lista para almacenar números enteros, pero de modo que siempre esté ordenada de menor a mayor. Para hacer la prueba añadiremos los valores 20, 10, 40, 30. De este modo tendremos todos los casos posibles. Al comenzar, el primer elemento se introducirá en una lista vacía, el segundo se insertará en la primera posición, el tercero en la última, y el último en una posición intermedia.

Insertar un elemento en una lista vacía es equivalente a insertarlo en la primera posición. De modo que no incluiremos una función para asignar un elemento en una lista vacía, y haremos que la función para insertar en la primera posición nos sirva para ese caso también.

Algoritmo de inserción

  1. El primer paso es crear un nodo para el dato que vamos a insertar.
  2. Si Lista es NULL, o el valor del primer elemento de la lista es mayor que el del nuevo, insertaremos el nuevo nodo en la primera posición de la lista.
  3. En caso contrario, buscaremos el lugar adecuado para la inserción, tenemos un puntero "anterior". Lo inicializamos con el valor de Lista, y avanzaremos mientras anterior->siguiente no sea NULL y el dato que contiene anterior->siguiente sea menor o igual que el dato que queremos insertar.
  4. Ahora ya tenemos anterior señalando al nodo adecuado, así que insertamos el nuevo nodo a continuación de él.
void Insertar(Lista *lista, int v) {
   pNodo nuevo, anterior;

   /* Crear un nodo nuevo */
   nuevo = (pNodo)malloc(sizeof(tipoNodo));
   nuevo->valor = v;

   /* Si la lista está vacía */
   if(ListaVacia(*lista) || (*lista)->valor > v) {
      /* Añadimos la lista a continuación del nuevo nodo */
      nuevo->siguiente = *lista;
      /* Ahora, el comienzo de nuestra lista es en nuevo nodo */
      *lista = nuevo;
   } else {
      /* Buscar el nodo de valor menor a v */
      anterior = *lista;
      /* Avanzamos hasta el último elemento o hasta que el siguiente tenga
         un valor mayor que v */
      while(anterior->siguiente && anterior->siguiente->valor <= v)
         anterior = anterior->siguiente;
      /* Insertamos el nuevo nodo después del nodo anterior */
      nuevo->siguiente = anterior->siguiente;
      anterior->siguiente = nuevo;
   }
}

Algoritmo para borrar un elemento

Después probaremos la función para buscar y borrar, borraremos los elementos 10, 15, 45, 30 y 40, así probaremos los casos de borrar el primero, el último y un caso intermedio o dos nodos que no existan.

Recordemos que para eliminar un nodo necesitamos disponer de un puntero al nodo anterior.

  1. Lo primero será localizar el nodo a eliminar, si es que existe. Pero sin perder el puntero al nodo anterior. Partiremos del nodo primero, y del valor NULL para anterior. Y avanzaremos mientras nodo no sea NULL o mientras que el valor almacenado en nodo sea menor que el que buscamos.
  2. Ahora pueden darse tres casos:
  3. Que el nodo sea NULL, esto indica que todos los valores almacenados en la lista son menores que el que buscamos y el nodo que buscamos no existe. Retornaremos sin borrar nada.
  4. Que el valor almacenado en nodo sea mayor que el que buscamos, en ese caso también retornaremos sin borrar nada, ya que esto indica que el nodo que buscamos no existe.
  5. Que el valor almacenado en el nodo sea igual al que buscamos.
  6. De nuevo existen dos casos:
  7. Que anterior sea NULL. Esto indicaría que el nodo que queremos borrar es el primero, así que modificamos el valor de Lista para que apunte al nodo siguiente al que queremos borrar.
  8. Que anterior no sea NULL, el nodo no es el primero, así que asignamos a anterior->siguiente la dirección de nodo->siguiente.
  9. Después de 7 u 8, liberamos la memoria de nodo.
void Borrar(Lista *lista, int v) {
   pNodo anterior, nodo;

   nodo = *lista;
   anterior = NULL;
   while(nodo && nodo->valor < v) {
      anterior = nodo;
      nodo = nodo->siguiente;
   }
   if(!nodo || nodo->valor != v) return;
   else { /* Borrar el nodo */
      if(!anterior) /* Primer elemento */
         *lista = nodo->siguiente;
      else  /* un elemento cualquiera */
         anterior->siguiente = nodo->siguiente;
      free(nodo);
   }
}

Código del ejemplo completo

#include <stdlib.h>
#include <stdio.h>

typedef struct _nodo {
   int valor;
   struct _nodo *siguiente;
} tipoNodo;


typedef tipoNodo *pNodo;
typedef tipoNodo *Lista;

/* Funciones con listas: */
void Insertar(Lista *l, int v);
void Borrar(Lista *l, int v);

int ListaVacia(Lista l);

void BorrarLista(Lista *);
void MostrarLista(Lista l);

int main() {
   Lista lista = NULL;
   pNodo p;

   Insertar(&lista, 20);
   Insertar(&lista, 10);
   Insertar(&lista, 40);
   Insertar(&lista, 30);

   MostrarLista(lista);

   Borrar(&lista, 10);
   Borrar(&lista, 15);
   Borrar(&lista, 45);
   Borrar(&lista, 30);
   Borrar(&lista, 40);

   MostrarLista(lista);

   BorrarLista(&lista);

   return 0;
}

void Insertar(Lista *lista, int v) {
   pNodo nuevo, anterior;

   /* Crear un nodo nuevo */
   nuevo = (pNodo)malloc(sizeof(tipoNodo));
   nuevo->valor = v;

   /* Si la lista está vacía */
   if(ListaVacia(*lista) || (*lista)->valor > v) {
      /* Añadimos la lista a continuación del nuevo nodo */
      nuevo->siguiente = *lista;
      /* Ahora, el comienzo de nuestra lista es en nuevo nodo */
      *lista = nuevo;
   } else {
      /* Buscar el nodo de valor menor a v */
      anterior = *lista;
      /* Avanzamos hasta el último elemento o hasta que el siguiente tenga
         un valor mayor que v */
      while(anterior->siguiente && anterior->siguiente->valor <= v)
         anterior = anterior->siguiente;
      /* Insertamos el nuevo nodo después del nodo anterior */
      nuevo->siguiente = anterior->siguiente;
      anterior->siguiente = nuevo;
   }
}

void Borrar(Lista *lista, int v) {
   pNodo anterior, nodo;

   nodo = *lista;
   anterior = NULL;
   while(nodo && nodo->valor < v) {
      anterior = nodo;
      nodo = nodo->siguiente;
   }
   if(!nodo || nodo->valor != v) return;
   else { /* Borrar el nodo */
      if(!anterior) /* Primer elemento */
         *lista = nodo->siguiente;
      else  /* un elemento cualquiera */
         anterior->siguiente = nodo->siguiente;
      free(nodo);
   }
}

int ListaVacia(Lista lista) {
   return (lista == NULL);
}

void BorrarLista(Lista *lista) {
   pNodo nodo;

   while(*lista) {
      nodo = *lista;
      *lista = nodo->siguiente;
      free(nodo);
   }
}

void MostrarLista(Lista lista) {
   pNodo nodo = lista;

   if(ListaVacia(lista)) printf("Lista vacía\n");
   else {
      while(nodo) {
         printf("%d -> ", nodo->valor);
         nodo = nodo->siguiente;
     }
     print("\n");
   }
}

Fichero con el código fuente.

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo de listas en C lista_c.zip 2001-09-15 965 bytes 1308

1.10 Ejemplo de lista abierta en C++ usando clases

Usando clases el programa cambia bastante, aunque los algoritmos son los mismos.

Para empezar, necesitaremos dos clases, una para nodo y otra para lista. Además la clase para nodo debe ser amiga de la clase lista, ya que ésta debe acceder a los miembros privados de nodo.

class nodo {
   public:
    nodo(int v, nodo *sig = NULL) {
       valor = v;
       siguiente = sig;
    }

   private:
    int valor;
    nodo *siguiente;

   friend class lista;
};

typedef nodo *pnodo;

class lista {
   public:
    lista() { primero = actual = NULL; }
    ~lista();

    void Insertar(int v);
    void Borrar(int v);
    bool ListaVacia() { return primero == NULL; }
    void Mostrar();
    void Siguiente();
    void Primero();
    void Ultimo();
    bool Actual() { return actual != NULL; }
    int ValorActual() { return actual->valor; }

   private:
    pnodo primero;
    pnodo actual;
};

Hemos hecho que la clase para lista sea algo más completa que la equivalente en C, aprovechando las prestaciones de las clases. En concreto, hemos añadido funciones para mantener un puntero a un elemento de la lista y para poder moverse a través de ella.

Los algoritmos para insertar y borrar elementos son los mismos que expusimos para el ejemplo C, tan sólo cambia el modo de crear y destruir nodos.

Código del ejemplo completo

#include <iostream>
using namespace std;

class nodo {
   public:
    nodo(int v, nodo *sig = NULL)
    {
       valor = v;
       siguiente = sig;
    }

   private:
    int valor;
    nodo *siguiente;

   friend class lista;
};

typedef nodo *pnodo;

class lista {
   public:
    lista() { primero = actual = NULL; }
    ~lista();

    void Insertar(int v);
    void Borrar(int v);
    bool ListaVacia() { return primero == NULL; }
    void Mostrar();
    void Siguiente() { if(actual) actual = actual->siguiente; }
    void Primero() { actual = primero; }
    void Ultimo() { Primero(); if(!ListaVacia()) while(actual->siguiente) Siguiente(); }
    bool Actual() { return actual != NULL; }
    int ValorActual() { return actual->valor; }

   private:
    pnodo primero;
    pnodo actual;
};

lista::~lista() {
   pnodo aux;

   while(primero) {
      aux = primero;
      primero = primero->siguiente;
      delete aux;
   }
   actual = NULL;
}

void lista::Insertar(int v) {
   pnodo anterior;

   // Si la lista está vacía
   if(ListaVacia() || primero->valor > v) {
      // Asignamos a lista un nuevo nodo de valor v y
      // cuyo siguiente elemento es la lista actual
      primero = new nodo(v, primero);
   } else {
      // Buscar el nodo de valor menor a v
      anterior = primero;
      // Avanzamos hasta el último elemento o hasta que el siguiente tenga
      // un valor mayor que v
      while(anterior->siguiente && anterior->siguiente->valor <= v)
         anterior = anterior->siguiente;
      // Creamos un nuevo nodo después del nodo anterior, y cuyo siguiente
      // es el siguiente del anterior
      anterior->siguiente = new nodo(v, anterior->siguiente);
   }
}

void lista::Borrar(int v) {
   pnodo anterior, nodo;

   nodo = primero;
   anterior = NULL;
   while(nodo && nodo->valor < v) {
      anterior = nodo;
      nodo = nodo->siguiente;
   }
   if(!nodo || nodo->valor != v) return;
   else { // Borrar el nodo
      if(!anterior) // Primer elemento
         primero = nodo->siguiente;
      else  // un elemento cualquiera
         anterior->siguiente = nodo->siguiente;
      delete nodo;
   }
}

void lista::Mostrar() {
   nodo *aux;

   aux = primero;
   while(aux) {
      cout << aux->valor << "-> ";
      aux = aux->siguiente;
   }
   cout << endl;
}

int main() {
   lista Lista;

   Lista.Insertar(20);
   Lista.Insertar(10);
   Lista.Insertar(40);
   Lista.Insertar(30);

   Lista.Mostrar();

   cout << "Lista de elementos:" << endl;
   Lista.Primero();
   while(Lista.Actual()) {
      cout << Lista.ValorActual() << endl;
      Lista.Siguiente();
   }
   Lista.Primero();
   cout << "Primero: " << Lista.ValorActual() << endl;

   Lista.Ultimo();
   cout << "Ultimo: " << Lista.ValorActual() << endl;

   Lista.Borrar(10);
   Lista.Borrar(15);
   Lista.Borrar(45);
   Lista.Borrar(30);
   Lista.Borrar(40);

   Lista.Mostrar();

   return 0;
}

Fichero con el código fuente.

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo de listas en C++ lista_cpp.zip 2001-09-15 1082 bytes 1230

1.11 Ejemplo de lista abierta en C++ usando plantillas

El siguiente paso es usar plantillas (templates) para definir listas genéricas, cuyos nodos pueden ser objetos de cualquier tipo definido en nuestro programa.

El código es algo más complicado, al menos a primera vista. Pero comprobaremos que es mucho más flexible y versatil.

Seguimos necesitando dos clases, una para nodo y otra para lista. Pero ahora podremos usar esas clases para construir listas de cualquier tipo de datos. Además, definiremos la clase para nodo como local dentro de la clase para lista, de ese modo nos evitamos definir amistades entre clases. A fin de cuentas, la clase nodo sólo la usaremos en esta lista.

Código del un ejemplo completo

Es preferible declarar cada plantilla clase en un fichero independiente, de este modo podremos usarlas en otros programas fácilmente. Empezamos con una plantilla para lista:

// ListaAb.h: Plantilla para lista abierta ordenada
// C con Clase. (C) Marzo de 2002
// Plantilla para lista abierta
// Posibles mejoras:
// * Implementar constructor copia.

#ifndef _LISTAABIERTA_
#define _LISTAABIERTA_

template<class DATO>
class Lista {
  private:
   //// Clase local de Lista para Nodo de Lista:
   template<class DATON>
   class Nodo {
     public:
      // Constructor:
      Nodo(const DATON dat, Nodo<DATON> *sig) : dato(dat), siguiente(sig) {}
      // Miembros:
      DATON dato;
      Nodo<DATON> *siguiente;
   };

   // Punteros de la lista, para cabeza y nodo actual:
   Nodo<DATO> *primero;
   Nodo<DATO> *actual;

  public:
   // Constructor y destructor básicos:
   Lista() : primero(NULL), actual(NULL) {}
   ~Lista();
   // Funciones de inserción:
   void InsertarFinal(const DATO dat);
   void InsertarPrincipio(const DATO dat);
   bool InsertarActual(const DATO dat);
   void Insertar(const DATO dat);
   // Funciones de borrado:
   void BorrarActual();
   bool BorrarPrimerValor(const DATO dat);
   // Función de búsqueda:
   bool BuscarPrimerValor(const DATO dat);
   // Comprobar si la lista está vacía:
   bool Vacia() { return primero==NULL; }
   // Devolver referencia al dato del nodo actual:
   DATO &ValorActual() { return actual->dato; }
   // Hacer que el nodo actual sea el primero:
   void Primero() { actual = primero; }
   // Comprobar si el nodo actual es válido:
   bool Actual() { return actual != NULL; }
   // Moverse al siguiente nodo de la lista:
   void Siguiente() { if(actual) actual = actual->siguiente; }
   // Sobrecargar operator++ en forma sufija para los mismo:
   void operator++(int) { Siguiente(); }
   // Aplicar una función a cada elemento de la lista:
   void ParaCada(void (*func)(DATO&));
};

//////// Implementación:

// Destructor
template<class DATO>
Lista<DATO>::~Lista() {
   while(!Vacia()) {
      actual = primero;
      primero = primero->siguiente;
      delete actual;
   }
}

template<class DATO>
void Lista<DATO>::InsertarFinal(const DATO dat) {
   Nodo<DATO> *ultimo;

   // Si la lista está vacía, insertar al principio:
   if(Vacia()) InsertarPrincipio(dat);
   else { // Si no lo está:
      // Buscar el último nodo:
      ultimo = primero;
      while(ultimo->siguiente) ultimo = ultimo->siguiente;
      // Insertar a continuación:
      ultimo->siguiente = new Nodo<DATO>(dat, NULL);
   }
}

template<class DATO>
void Lista<DATO>::InsertarPrincipio(const DATO dat) {
   primero = new Nodo<DATO>(dat, primero);
}

template<class DATO>
bool Lista<DATO>::InsertarActual(const DATO dat) {
   // Sólo si la lista no está vacía y actual es válido:
   if(!Vacia() && actual) {
      actual->siguiente = new Nodo<DATO>(dat, actual->siguiente);
      return true;
   }
   // Si no se puede insertar, retornar con false:
   return false;
}

// Insertar ordenadamente:
template<class DATO>
void Lista<DATO>::Insertar(const DATO dat) {
   Nodo<DATO> *temp = primero;
   Nodo<DATO> *anterior = NULL;

   // Si la lista está vacía, insertar al principio:
   if(Vacia()) InsertarPrincipio(dat);
   else {
      // Buscar el nodo anterior al primer nodo con un dato mayor qur 'dat'
      while(temp && temp->dato < dat) {
         anterior = temp;
         temp = temp->siguiente;
      }
      // Si no hay anterior, insertar al principio,
      // nuestro valor es el menor de la lista:
      if(!anterior)
         InsertarPrincipio(dat);
      else // Insertar:
         anterior->siguiente = new Nodo<DATO>(dat, temp);
   }
}

template<class DATO>
void Lista<DATO>::BorrarActual() {
   Nodo<DATO> *anterior;

   // Si el nodo actual es el primero:
   if(actual && actual == primero) {
      // El primer nodo será ahora el segundo:
      // Sacar el nodo actual de la lista:
      primero = actual->siguiente;
      // Borrarlo:
      delete actual;
      actual = NULL;
   } else
   if(actual && !Vacia()) {
      // Buscar el nodo anterior al actual:
      anterior = primero;
      while(anterior && anterior->siguiente != actual)
         anterior = anterior->siguiente;
      // Sacar el nodo actual de la lista:
      anterior->siguiente = actual->siguiente;
      // Borrarlo:
      delete actual;
      actual = NULL;
   }
}

// Borrar el primer nodo cuyo dato sea igual a 'dat':
template<class DATO>
bool Lista<DATO>::BorrarPrimerValor(const DATO dat) {
   Nodo<DATO> *anterior = NULL;
   Nodo<DATO> *temp = primero;

   if(!Vacia()) {
      // Si la lista no está vacía, buscar el nodo a borrar (temp)
      // y el nodo anterior a ese (anterior):
      while(temp && temp->dato != dat) {
         anterior = temp;
         temp = temp->siguiente;
      }
      // Si el valor está en la lista:
      if(temp) {
         // Si anterior es válido, no es el primer valor de la lista
         if(anterior) // Sacar nodo temp de la lista
            anterior->siguiente = temp->siguiente;
         else         // Ahora el primero es el segundo:
           primero = temp->siguiente;  // Borrar primer elemento
         // Borrar nodo:
         delete temp;
         return true; // Se ha encontrado y borrado dat
      }
   }
   return false; // valor no encontrado
}

// Busca el primer nodo con valor 'dat':
template<class DATO>
bool Lista<DATO>::BuscarPrimerValor(const DATO dat) {
   actual = primero;

   // Si la lista no está vacía:
   if(!Vacia()) {
      while(actual && actual->dato != dat) {
         actual = actual->siguiente;
      }
   }
   // Si el nodo es válido, se ha encontrado el valor:
   return actual != NULL;
}

// Aplicar una función a cada nodo de la lista:
template<class DATO>
void Lista<DATO>::ParaCada(void (*func)(DATO&)) {
   Nodo<DATO> *temp = primero;

   // Recorrer la lista:
   while(temp) {
      // Aplicar la función:
      func(temp->dato);
      temp = temp->siguiente;
   }
}

// La función "func" debe ser una plantilla de una función
// que no retorne valor y que admita un parámetro del mismo
// tipo que la lista:
// template <class DATO>
// void <funcion>(DATO d);

#endif

Hemos introducido algunos refinamientos en nuestra clase que la harán más fácil de usar. Por ejemplo:

  • Cuatro versiones distintas para insertar nodos, una para insertar al principio, otra al final, otra a continuación del nodo actual y una cuarta para insertar por orden. Ésta última nos permite crear listas ordenadas.
  • Dos funciones para borrar valores, una borra el elemento actual y la otra el primer nodo que contenga el valor especificado.
  • Sobrecarga del operador de postincremento, que nos permite movernos a lo largo de la lista de un modo más intuitivo.
  • Función "ParaCada", que aplica una función a cada elemento de la lista. Veremos cómo podemos usar esta función para hacer cosas como mostrar la lista completa o incrementar todos los elementos.

En el tema de plantillas del curso de C++ ya hemos visto que existen algunas limitaciones para los tipos que se pueden emplear en plantillas. Por ejemplo, no podemos crear una lista de cadenas usando Lista<char *>, ya que de ese modo sólo creamos una lista de punteros.

Para poder crear listas de cadenas hemos implementado una clase especial para cadenas, en la que además de encapsular las cadenas hemos definido los operadadores =, ==, !=, >, <, >=, <=, algunos de los cuales son necesarios para poder crear listas con esta clase.

Clase para manejar cadenas:

// CCadena.h: Fichero de cabecera de definición de cadenas
// C con Clase: Marzo de 2002

#ifndef CCADENA
#define CCADENA
#include <cstring>
using namespace std;

class Cadena {
  public:
   Cadena(char *cad) {
      cadena = new char[strlen(cad)+1];
      strcpy(cadena, cad);
   }
   Cadena() : cadena(NULL) {}
   Cadena(const Cadena &c) : cadena(NULL) {*this = c;}
   ~Cadena() { if(cadena) delete[] cadena; }
   Cadena &operator=(const Cadena &c) {
      if(this != &c) {
         if(cadena) delete[] cadena;
         if(c.cadena) {
            cadena = new char[strlen(c.cadena)+1];
            strcpy(cadena, c.cadena);
         }
         else cadena = NULL;
      }
      return *this;
   }
   bool operator==(const Cadena &c) const {
      return !strcmp(cadena, c.cadena);
   }
   bool operator!=(const Cadena &c) const {
      return strcmp(cadena, c.cadena);
   }
   bool operator<(const Cadena &c) const {
      return strcmp(cadena, c.cadena) < 0;
   }
   bool operator>(const Cadena &c) const {
      return strcmp(cadena, c.cadena) > 0;
   }
   bool operator<=(const Cadena &c) const {
      return strcmp(cadena, c.cadena) <= 0;
   }
   bool operator>=(const Cadena &c) const {
      return strcmp(cadena, c.cadena) >= 0;
   }

   const char* Lee() const {return cadena;}

  private:
   char *cadena;
};

ostream& operator<<(ostream &os, const Cadena& cad) {
   os << cad.Lee();
   return os;
}
#endif

Ahora ya podemos poner algunos ejemplos de listas creadas con plantillas:

// ListaAb.cpp: Ejemplo de uso de plantilla para listas abiertas
// C con Clase: Marzo de 2002

#include <iostream>
#include "CCadena.h"
#include "ListaAb.h"
using namespace std;

// Plantilla de función que incrementa el valor del objeto
// dado como parámetro aplicando el operador ++
template<class DATO>
void Incrementar(DATO &d) {
   d++;
}

// Plantilla de función que mustra el valor del objeto
// dado como parámetro formando una lista separada con comas
template<class DATO>
void Mostrar(DATO &d) {
   cout << d << ", ";
}

int main() {
   // Declaración de una lista de enteros:
   Lista<int> ListaInt;

   // Inserción de algunos valores:
   ListaInt.InsertarFinal(43);
   ListaInt.InsertarFinal(65);
   ListaInt.InsertarFinal(33);
   ListaInt.InsertarFinal(64);
   ListaInt.InsertarFinal(22);
   ListaInt.InsertarFinal(11);

   // Mostrar lista:
   cout << "---listado---" << endl;
   ListaInt.ParaCada(Mostrar); // Aplicamos la función Mostrar a cada elemento
   cout << endl << "-------------" << endl;

   // Incrementamos los valores de todos los elementos de la lista
   // aplicando a cada uno la función "Incrementar":
   cout << "---Incrementar todos---" << endl;
   ListaInt.ParaCada(Incrementar);

   // Mostrar lista:
   cout << "---listado---" << endl;
   ListaInt.ParaCada(Mostrar);
   cout << endl << "-------------" << endl;
   cin.get();

   // Borrar el primer elemento de valor 34:
   cout << "borrar 34" << endl;
   ListaInt.BorrarPrimerValor(34);

   // Mostrar lista:
   cout << "---listado---" << endl;
   ListaInt.ParaCada(Mostrar);
   cout << endl << "-------------" << endl;

   // Declaración de una lista de floats:
   Lista<float> ListaFloat;

   // Inserción de algunos valores:
   ListaFloat.InsertarFinal(43.2);
   ListaFloat.InsertarFinal(65.3);
   ListaFloat.InsertarFinal(33.1);
   ListaFloat.InsertarFinal(64.8);
   ListaFloat.InsertarFinal(22.32);
   ListaFloat.InsertarFinal(11.003);

   // Mostrar lista:
   cout << "---listado---" << endl;
   ListaFloat.ParaCada(Mostrar);
   cout << endl << "-------------" << endl;

   // Incrementamos todos:
   cout << "---Incrementar todos---" << endl;
   ListaFloat.ParaCada(Incrementar);

   // Mostrar lista:
   cout << "---listado---" << endl;
   ListaFloat.ParaCada(Mostrar);
   cout << endl << "-------------" << endl;

   cin.get();

   // Declaración de una lista de cadenas:
   Lista<Cadena> ListaCad;

   // Inserción de algunos valores, creando una lista ordenada:
   ListaCad.Insertar("alfa");
   ListaCad.Insertar("delta");
   ListaCad.Insertar("beta");
   ListaCad.Insertar("gamma");
   ListaCad.Insertar("delta");
   ListaCad.Insertar("epsilon");
   ListaCad.Insertar("sigma");
   ListaCad.Insertar("delta");

   // Mostrar lista:
   cout << "---listado---" << endl;
   ListaCad.ParaCada(Mostrar);
   cout << endl << "-------------" << endl;
   cin.get();

   // Borramos todos los elementos de valor "delta":
   while(ListaCad.BorrarPrimerValor("delta")) {
      cout << "borrar 'delta'" << endl;
      // Mostrar lista:
      cout << "---listado---" << endl;
      ListaCad.ParaCada(Mostrar);
      cout << endl << "-------------" << endl;
      cin.get();
   }

   // Buscar el primer elemento de valor "gamma":
   cout << "buscar 'gamma'" << endl;
   if(ListaCad.BuscarPrimerValor("gamma"))
      cout << ListaCad.ValorActual() << endl;
   else
      cout << "No encontrado" << endl;

   // Declaración de una lista de enteros:
   Lista<int> ListaOrden;

   // Inserción de algunos valores, creando una lista ordenada:
   cout << "Lista ordenada de enteros" << endl;
   ListaOrden.Insertar(43);
   ListaOrden.Insertar(65);
   ListaOrden.Insertar(33);
   ListaOrden.Insertar(64);
   ListaOrden.Insertar(4);
   ListaOrden.Insertar(22);
   ListaOrden.Insertar(1);
   ListaOrden.Insertar(11);
   ListaOrden.Insertar(164);

   // Mostrar lista:
   cout << "---listado---" << endl;
   ListaOrden.ParaCada(Mostrar);
   cout << endl << "-------------" << endl;

   cin.get();
   return 0;
}

Hemos creado dos plantillas de funciones para demostrar el uso de la función "ParaCada", una de ellas incrementa cada valor de la lista, la otra lo muestra por pantalla. La primera no puede aplicarse a listas de cadenas, porque el operador ++ no está definido en la clase Cadena.

El resto creo que no necesita mucha explicación.

Fichero con el código fuente.

Nombre Fichero Fecha Tamaño Contador Descarga
Ejemplo de listas con plantillas lista_templ.zip 2002-03-31 3358 bytes 1101