Servicio de atención al lector: [email protected]
En este capítulo veremos cómo implementar una lista
enlazada, una lista doblemente enlazada, una pila
dinámica y una cola dinámica. Finalmente, aprenderemos
a reimplementar todas estas estructuras, pero de modo
independiente del tipo de dato que manejen, por medio
de las plantillas.
Estructuras de datos dinámicas y plantillas
▼ Listas enlazadas .........................2
¿Qué es una lista enlazada? ............... 2
Implementación de una lista enlazada 2
La clase Nodo .................................... 3
La clase Lista .................................... 6
▼ Listas doblemente enlazadas ...20
Pila .................................................. 35
Cola ................................................. 36
▼ Plantillas ..................................37
Listas doblemente enlazadas
con plantillas .................................... 47
Pila .................................................. 54
Cola ................................................. 55
▼ Los iteradores ..........................55
▼ Resumen ...................................59
▼ Actividades ...............................60
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS2
www.redusers.com
Listas enlazadasLa lista enlazada es una estructura versátil y popular. No existe
curso de C/C++ que no trate el tema, y este libro no será la excepción.
¿Qué es una lista enlazada?Una lista enlazada es una estructura de datos compuesta por nodos;
cada uno de ellos se encuentra enlazado a otro por medio de un
puntero. El motivo de su popularidad reside en que la lista puede
crecer y decrecer en forma dinámica.
Figura 1. Lista enlazada.
Cada nodo está compuesto por dos elementos: datos y un puntero
a otro nodo. Por lo general, la lista que gestiona los nodos posee
un puntero a próximo nodo en NULL, lo que indica que estamos
en presencia del último nodo de la lista. Existen implementaciones de
listas que no solo poseen un puntero al próximo nodo, sino al anterior,
variedad conocida como “lista doblemente enlazada”.
Figura 2. Lista doblemente enlazada.
Implementación de lista enlazadaImplementaremos una lista enlazada por medio de dos clases:
la primera representará el nodo (datos + puntero a próximo a nodo), y
la segunda representará la lista que gestionará un conjunto de nodos.
próximodatos
primer nodo
NULLpróximodatos
próximodatos
próximodatos
primer nodo
primer nodo
NULLpróximodatos
próximoanterior anterior anterior
datos
NULL
C++ 3
www.redusers.com
La lista no tendrá un array de nodos, sino un puntero al primer nodo
de la lista. Por esta razón, en el diagrama de clases observamos que la
relación existente entre la entidad Lista y la entidad Nodo es 1 a 1. Luego
un nodo poseerá un puntero al próximo nodo.
Figura 3. El sencillo diagrama de clases de una lista enlazada.
La clase NodoComencemos a ver cómo estará conformada nuestra clase Nodo:
class Nodo
{
int m_dato;
Nodo* m_pProx;
// …
};
Las dos propiedades que tendrá nuestra clase serán:
• El dato: en este caso, solo un número entero.
• El puntero al próximo nodo: una propiedad del tipo Nodo*.
Como estas propiedades serán privadas, deberemos colocar
los métodos correspondientes para poder acceder a ellas. Veamos:
class Nodo
En algunas publicaciones, es posible encontrar que la lista doblemente enlazada se denomina “lista
enlazada”, mientras que la lista con solo un puntero al próximo nodo, “lista simplemente enlazada”. En
otros casos, se denomina genéricamente “listas enlazadas”, sin hacer referencia específi ca a ninguna de
las dos implementaciones. Estas son simples convenciones.
DENOMINACIÓN DE TIPO DE LISTAS
1
1
1 1Lista Nodo
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS4
www.redusers.com
{
// …
public:
// Seteo los valores del nodo
void FijarValor(int dato) ñ m_dato = dato; }
// Seteo el próximo nodo
void FijarProximo(Nodo * pProx) { m_pProx = pProx; }
// Leo la propiedad m_dato
int LeerDato() const { return m_dato; }
// Tomo la propiedad al próximo nodo
Nodo * LeerProximo() const { return m_pProx; }
};
Finalmente, se encuentran los constructores y el destructor
de la clase:
class Nodo
{
// …
public:
// Constructor estándar
Nodo() : m_pProx(NULL) {}
// Constructor de copia
Nodo(const Nodo& copiaDe) : m_pProx(NILL) { m_dato = copiaDe.m_dato; }
// Constructor con parámetros iniciales
Nodos(int dato) : m_dato(dato), m_pProx(NULL) {}
// Destructor
~Nodo()
{
if (m_pProx)
delete this->m_pProx;
C++ 5
www.redusers.com
}
// …
};
Como podemos notar, ofrecemos tres constructores distintos:
1) El constructor por defecto, que fi ja el puntero próximo a NULL.
2) El constructor de copia, que fi ja el puntero próximo a NULL,
y copia el dato pasado como parámetro al nodo.
3) Un constructor con un parámetro entero, para crear un objeto
nodo y asignarle un valor al dato en un solo paso (simplemente
por comodidad).
Respecto del destructor, sería interesante analizarlo. Posee dos
líneas de código que dicen, resumidamente: “Si el nodo próximo es
distinto de NULL, eliminarlo”. De este modo, si destruimos el primer
nodo de la lista, se irán destruyendo todos en cascada hasta que
no quede ninguno, ya que el destructor del primer nodo eliminará
al segundo, disparando, a su vez, a su destructor, que eliminará al
próximo nodo, que es el tercero, y así sucesivamente.
Figura 4. La destrucción del primer nodo acarrea la destrucción de todos los nodos próximos, hasta encontrar un NULL en el puntero a nodo próximo.
En caso de que no queramos destruir todos los nodos en cascada,
sino solo un nodo determinado, deberemos tener la precaución de fi jar
el puntero de próximo nodo a NULL en el nodo a destruir.
Nodo
Nodo
Nodo NULL
Primer nodo
3
2
1
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS6
www.redusers.com
La clase ListaLa clase Lista será la que gestione la lista enlazada propiamente
dicha, por lo que hará el trabajo más duro, que consiste en la inserción
y extracción de nodos.
class Lista
{
// Puntero al primer nodo de la lista (NULL si la lista está vacía)
Nodo * m_pPrimerNodo;
// Cantidad de nodos en lista
int m_iCantidad;
//
};
Las propiedades de la clase serán las siguientes:
• Puntero al primer nodo: la lista solo tendrá información de cuál
es el primer nodo; para acceder al resto, siempre deberemos pasar
por todos los anteriores. Inicialmente, el valor de este puntero es
NULL, ya que la lista se encuentra vacía.
• Cantidad de nodos: aunque es posible contar la cantidad de nodos
cada que vez que se requiera esa información, mejor será poseer
una propiedad en la que llevemos cuenta de esta cantidad en todo
momento. En un principio, la cantidad de nodos será cero.
• El constructor: cuando construyamos la lista, esta fi jará el puntero
a primer nodo en NULL y la cantidad de nodos en cero, como
habíamos mencionado.
En algunas implementaciones, el nodo no es una clase con métodos –como sí lo será en la nuestra–
sino simplemente una estructura. Esto acarrea algunas líneas de código de más en la clase lista, ya que,
por ejemplo, no existirá un constructor que fi je los valores por defecto a las propiedades.
EL NODO COMO ESTRUCTURA
C++ 7
www.redusers.com
Veamos su implementación en el siguiente código:
Lista::Lista(void)
{
m_pPrimerNodo = NULL;
m_iCantidad = 0;
}
• El destructor: si alguien destruye una lista, deberemos tener
la prudencia de eliminar todos los nodos relacionados con ella,
para que no queden zonas de memoria reservadas pero sin usar
(conocidas como memory leaks).
Lista::~Lista(void)
{
if (m_pPrimerNodo)
delete m_pPimerNodo;
}
Para ellos, verifi camos que la lista no se encuentra vacía y
eliminamos el primer modo. Como habíamos visto en la clase Nodo,
esta acción conlleva la eliminación de todos los nodos de la lista.
• El método Limpiar: es posible que, en un determinado momento,
queramos vaciar codifi cado del siguiente modo:
void Lista::Limpiar()
{
if (m_pPreimerNodo)
delete m_pPrimerNodo;
m_pPrimerNodo = NULL;
m_iCantidad = 0;
}
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS8
www.redusers.com
Similar al destructor en cuanto al modo de destruir los nodos de
la lista, fi jamos las propiedades puntero al primer nodo en NULL, y la
cantidad de nodos en cero, porque el objeto sigue vivo y seguirá siendo
utilizado (en el destructor no molestaba, porque el objeto era destruido).
Métodos de inserción y remoción de nodos: llegamos al corazón
del problema. Todo lo visto hasta el momento fue el esqueleto de la
lista. Para entender cómo funciona una lista enlazada, más allá del
modo de implementación, es fundamental entender de qué manera
debemos proceder en cada una de las operaciones básicas de la lista:
• Inserción de un nodo por delante.
• Inserción de un nodo por detrás.
• Extracción de un nodo por delante.
• Extracción de un nodo por detrás.
Inserción de un nodo por delante: el método InsertarDelante
llevará a cabo esta operación, que consiste en crear un nuevo nodo
y agregarlo delante de todos los demás. Veamos los pasos que son
necesarios para realizar este procedimiento.
1. La lista en su estado, antes de realizar la inserción.
primer nodo
NULL
2. Creamos un nuevo nodo (comienza apuntando a NULL debido al constructor de la clase Nodo).
primer nodo
NULL
primer nodo
NULL
3. Modificamos el puntero próximo del nodo recién creado para que apunte al primer nodo de la lista.
primer nodo
primer nodo
NULL
C++ 9
www.redusers.com
4. Modificamos el puntero al primer nodo de la lista de modo que apunte al nodo recién creado.
primer nodo
primer nodo
NULL
Figura 5. La inserción por delante.
Ahora, codifi quemos los pasos descritos en el diagrama anterior
empleando el método InsertarDelante, según se muestra en el
siguiente ejemplo:
bool Lista::InsertarDelante(const int & dato)
{
Nodo * pNodo = new Nodo(dato);
if (pNodo)
{ pNodo->FijarProximo(m_pPrimerNodo);
m_pPrimerNodo = pNodo;
m_iCantidad++;
return tue;
}
else
return false;
}
Analicemos el código con más detalle:
Nodo * pNodo = new Nodo(dato);
Debemos crear un nuevo nodo, y para eso utilizaremos el operador new.
Como percibimos el dato como parámetro, aprovechamos que existe un
constructor apropiado para crear el nodo y darle un valor en un solo paso.
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS10
www.redusers.com
if (pNodo)
Es importante verifi car que el nodo haya sido creado correctamente.
Las listas enlazadas pueden ser muy extensas y, eventualmente, el
sistema puede quedarse sin la memoria sufi ciente como para crear
un nuevo nodo.
pNodo->FijarProximo(m_pPrimerNodo);
Aquí fi jamos, por medio del método FijarProximo de la clase Nodo, cuál
será el próximo al nodo recién creado. Como nuestro nodo será el primero
de la lista, el que se ubique después será el que antes estaba primero.
Por esta razón, utilizamos la propiedad de la clase lista
m_pPrimerNodo para fi jar el próximo nodo al nodo nuevo:
m_pPrimerNodo = pNodo;
Ahora estamos diciendo que el nuevo primer nodo de la lista es el
recién creado; de este modo, dejamos la lista debidamente enlazada.
m_iCantidad++;
A continuación, aumentamos el contador de nodos, ya que existe
uno nuevo enlazado en la lista.
Tengamos siempre en cuenta un caso especial: cuando la lista esté
vacía. En dicha instancia, nada cambiará, pero el método FijarProximo
sería invocado con un parámetro NULL, porque el puntero al
primer nodo posee este valor; por consiguiente, se volverá a fi jar
el puntero a próximo nodo al mismo valor que le había dejado el
constructor: NULL. Es muy importante tener en claro que, si bien
estamos haciendo el new sobre una variable local que será destruida
al fi nalizar la ejecución del método, la dirección de memoria que ella
conservaba será inserta en la propiedad de un nodo y, por lo tanto,
esta dirección no se extravía.
C++ 11
www.redusers.com
Inserción de un nodo por detrás: veamos cuáles son los pasos
para insertar un nodo detrás de todos los demás.
Figura 6. La inserción por detrás.
1. La lista en su estado, antes de realizar la inserción.
primer nodo
NULL
2. Creamos un nuevo nodo (comienza apuntando a NULL debido al constructor de la clase Nodo).
primer nodo
NULL
primer nodo
NULL
primer nodo
NULL
primer nodo
NULL
pUltimoNodo
3. Encontramos el último nodo.
primer nodo
NULL
pUltimoNodo
4. Finalmente, cambiamos el valor del puntero a próximo nodo del último para que apunte al nodo recientemente creado.
primer nodo
Los métodos de las operaciones retornan un valor booleano, que indica si la operación se ha llevado a
cabo con éxito o no. En el caso de la inserción, el método podrá fallar sólo si no existe memoria para
crear el nuevo nodo. En cambio, en el caso de la extracción de nodos, el método podrá fallar cuando no
existan más nodos que extraer, es decir, cuando la lista esté vacía.
EL VALOR DE RETORNO DE LAS OPERACIONES
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS12
www.redusers.com
La difi cultad en esta operación es hallar el último nodo. Hallar
el primero es una tarea muy sencilla, pues existe una propiedad
(m_pPrimerNodo) que mantiene dicho valor. Pero si se quiere encontrar
el último nodo en una lista simplemente enlazada, se deben recorrer
todos los nodos.
Para realizar esa labor, crearemos un método privado llamado
LeerUltimo, que nos retorna un puntero al último nodo o NULL, si es
que la lista está vacía.
Veamos:
Nodo = Lista::LeerUltimo(void)
{
Nodo * pNodo = m_pPrimerNodo;
Nodo * pNodoAnt = pNodo;
while(pNodo)
{ pNodoAnt = pNodo;
pNodo = pNodo->LeerProximo();
}
return pNodoAnt;
}
La idea subyacente del método LeerUltimo es recorrer la lista por
medio de una sentencia while hasta encontrar el último nodo.
1. Comenzamos fijando los puntero pNodoAnt y pNodo apuntando al primer nodo.
primer nodo
NULL
2. Dentro de la sentencia while, los punteros avanzarán sobre la lista
pNodopNodoAnt
primer nodo
NULL
pNodoAnt pNodo
C++ 13
www.redusers.com
3. Cuando pNodo quede apuntando a NULL, pNodoAnt atará apuntando al último nodo.
primer nodo
NULL
pNodoAnt pNodo
Figura 7. Pasos para buscar el último nodo.
Veamos cómo codifi camos InsertarDetras haciendo uso de este método:
bool Lista::InsertarDetras(const int & dato)
{
Nodo * pNodo = new Nodo(dato);
if (pNodo)
{
Nodo * pUltimoNodo = LeerUltimo();
if (pUltimoNodo)
pUltimoNodo->FijarProximo(pNodo);
else
// Si no existe ningún “ultimoNodo” signifi ca que la lista
estaba vacía.
m_pPrimerNodo = pNodo;
m_iCantidad++;
return true;
}
else
return false;
}
Nodo * pNodo = new Nodo(dato);
En primera instancia, debemos crear el nuevo nodo a insertar,
al igual que en el método InsertarDelante.
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS14
www.redusers.com
Nodo = pUltimoNodo = LeerUltimo();
Luego, en una variable local, obtenemos el último nodo de la lista.
if (pUltimoNodo)
Lo que hacemos aquí es dividir el fl ujo de ejecución en función
de si la lista se encuentra vacía o no.
pUltimoNodo->FijarProximo(pNodo);
Si la lista no se encuentra vacía, entonces cambiamos el puntero
a próximo del último nodo para que apunte al nuevo nodo creado.
m_pPrimerNodo = pNodo;
Si la lista está vacía, entonces el nuevo nodo será el último y también
el primero. Por lo tanto, debemos modifi car el puntero m_pPrimerNodo.
m_iCantidad++;
Finalmente, aumentamos el contador de nodo de la lista.
• Extracción de un nodo por delante: la extracción de un nodo
por delante de la lista no reviste mayor difi cultad.
Como se puede observar en la implementación del método de inserción por detrás, se debe recorrer
la lista entera para hallar el último nodo. Esto no es un detalle; la lista enlazada podría ser muy extensa.
Como veremos enseguida, la lista doblemente enlazada no presenta este problema.
SIMPLEMENTE VS. DOBLEMENTE ENLAZADA
C++ 15
www.redusers.com
Veamos el algoritmo en la siguiente fi gura:
Figura 8. Extracción por delante.
Ahora, observemos el código del método ExtraerDelante:
bool Lista::ExtraerDelante(int & dato)
{
1. La lista en su estado antes de realizar la remoción.
primer nodo
NULL
2. Creo un puntero pNodoAExtraer que apunte al primer nodo que es el nodo a extraer.
NULL
3. Modificamos el puntero m_pPrimerNodo para que apunte al nodo próximo, que antes era el primero.
4. Modificamos el puntero a próximo del nodo a extraer para que apunte a NULL.
5. Eliminamos el nodo a extraer.
primer nodo
NULL
pNodoAExtraer
pNodoAExtraer
pNodoAExtraer
primer nodo
NULL
primer nodo
NULL
NULL
pNodoAExtraer
primer nodo
NULL
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS16
www.redusers.com
Nodo * pNodoExtraer;
if (m_pPrimerNodo)
{
// Existe al menos un nodo en la lista
pNodoExtraer = m_pPrimerNodo;
m_pPrimerNodo = m_pPrimerNodo->LeerProximo();
// Fijo el puntero a próximo del nodo a eliminar a NULL
pNodoAExtraer->FijaProximo(NULL);
dato = pNodoAExtraer->LeerDato();
// Elimino el nodo
delete pNodoAExtraer;
// Decremento la cantidad de nodos
m_iCantidad-;
return true;
}
else
return false;
}
Analicemos el código del listado anterior:
if (m_pPrimerNodo)
En primer lugar, verifi camos que la lista no se encuentre vacía.
pNodoAExtraer = m_pPrimerNodo;
Obtenemos el nodo a extraer, que, en este caso, será el primero.
pNodoAExtraer->FijarProximo(NULL);
C++ 17
www.redusers.com
Modifi camos el puntero al próximo nodo del nodo a extraer. Esto es
para evitar que el destructor del nodo comience a eliminar a todos los
demás que se encuentren enlazados a él.
dato = pNodoAExtraer->LeerDato();
Luego obtenemos el dato del nodo que debe retornar el método.
delete pNodoAExtraer;
Totalmente aislado, eliminamos el nodo a extraer.
m_iCantidad-;
Por último, decrementamos el contador que mantiene la cantidad
de nodos en la lista.
• Extracción de un nodo por detrás: la difi cultad en la extracción
por detrás es que encontramos no solo el último nodo sino
el anterior a él. Veamos el diagrama del algoritmo.
1. La lista en su estado antes de realizar la remoción.
primer nodo
NULL
2. Creamos un puntero al último (nodo a extraer) y otro que apunte al anteúltimo nodo.
3. Modificamos el puntero a próximo del anteúltimo nodo para que apunte a NULL.
primer nodo
NULL
pNodoAExtraerpNodoAnteriorAExtraer
primer nodo
NULL
pNodoAExtraerpNodoAnteriorAExtraerNULL
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS18
www.redusers.com
4. Aislado el nodo a extraer, lo eliminamos.
primer nodo
NULL
pNodoAExtraerpNodoAnteriorAExtraerNULL
Figura 9. Extracción por detrás.
El código será el más largo de los cuatro:
bool Lista::ExtraerDetras(int & dato)
{
Nodo * pNodo = m_pPrimerNodo;
Nodo * pNodoAextraer = pNodo;
Nodo * pNodoAnteriorAExtraer = NULL;
while (pNodo)
{ pNodoAnteriorAExtraer = pNodoAExtraer;
pNodoAExtraer = pNodo;
pNodo = pNodo-ZLeerProximo();
}
if (pNodoAEXtraer)
{
// Existe un nodo a eliminar -> La lista no está vacía
if (pNodoAnteriorAExtraer == pNodoAExtraer)
// Actualizo el puntero al primer nodo
m_PrimerNodo = NULL;
else
// La lista posee más de un nodo
pNodoAnteriorAExtraer->FijarProximo(NULL);
// La lista tiene sólo un nodo -> Elimino
El único nodo existente
pNodoAExtraer->FijarProximo(NULL);
dato = pNodoAExtraer;->LeerDato();
C++ 19
www.redusers.com
// Elimino el nodo a extraer
delete pNodoAExtraer;
// Decremento la cantidad de nodos
m_iCantidad-;
return true;
}
else
return false;
}
Analicemos el código anterior:
while (pNodo)
{ pNodoAnteriorAExtraer = pNodoAExtraer;
pNodoAExtraer = pNodo;
pNodo = pNodo->LeerProximo();
}
Con esta sentencia while, deseamos encontrar el último nodo
de la lista y el anterior a él. Procedemos de un modo muy similar
al método LeerUltimo.
if (pNodoAExtraer)
Así, verifi camos que la lista no se encuentra vacía.
if (pNodoAnteriorAExtraer == pNodoAExtraer)
Luego verifi camos si existe solo un nodo a extraer.
m_pPrimerNodo = NULL;
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS20
www.redusers.com
Si se comprueba la sentencia condicional anterior, debemos
modifi car el puntero al primer nodo para que apunte a NULL,
ya que la lista quedará vacía.
pNodoAnteriorAExtraer->FijarProximo(NULL);
Si existe más de un nodo en la lista, estaremos ante el caso más
común. Fijaremos el puntero a próximo del nodo anteúltimo a NULL,
para dejar aislado el nodo a extraer.
Dato = pNodoAExtraer->LeerDaro();
Tomamos el dato que deberemos retornar.
delete pNodoAExtraer;
Eliminamos el nodo a extraer.
m_iCantidad-;
Como último paso, decrementamos el contador de la cantidad
de nodos que posee la lista. Ahora veamos cómo trabajan las listas
doblemente enlazadas.
Listas doblemente enlazadasLa lista doblemente enlazada, hemos dicho, posee dos punteros
por nodo: uno que mira hacia delante y otro que mira hacia atrás.
De este modo, las operaciones de inserción y extracción son totalmente
simétricas (algo que no ocurría con las listas simplemente enlazadas).
Aunque tendremos más trabajo, al actualizar más nodos se facilitará
la navegación por lista, ya que podremos hacerlo en ambos sentidos.
C++ 21
www.redusers.com
1
2
2 1Lista Nodo
Figura 10. Diagrama de clases de una lista doblemente enlazada.
Veamos de qué forma se modifi ca la clase Nodo ahora que le
agregamos un puntero más a un nodo anterior:
class Nodo{ int m_dato; Nodo * m_Prox; Nodo * m_pPrev;public: // constructor estándar Nodo() : m_pProx(NULL), m_pPrev(NULL) {} // Constructor de copiaNodo(const Nodo& copiaDe) : m_pProx(NULL), m_pPrev(NULL){ m_dato = copiaDe.m_dato; }// Constructor con parámetros iniciales Nodo(int dato) _ m_dato(dato), m_pProx(NULL), m_pPrev(NULL) {}
// Destructor
~Nodo()
{
if (m_pProx)
delete this->m_pProx;
}
En cuanto a la lista doblemente enlazada, otra diferencia que encontramos (y que se puede ver en los
ejemplos) es que ocupará un poco más de memoria de nuestra lista. Este es un factor muy importante
a tener en cuenta cuando deseemos seleccionar el tipo de estructura de datos correcto para resolver
nuestros problemas.
ESPACIO OCUPADO EN MEMORIA
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS22
www.redusers.com
//Seteo los valores del nodo
void FijarValor(intdato) { m_dato ) dato; }
// Seteo el nodo próximo
void FijarProximo(Nodo * pProx) { m_pProx = pProx; }
// Seteo el nodo anterior
void FijarAnterior(Nodo * pPrev) { m_pPrev = pPrev; }
// Leo la propiedad m_dato
int LeerDato() const { return m_dato; }
// Tomo la propiedad próximo nodo
Nodo * LeerProximo() const { return m_pProx; }
// Tomo la propiedad nodo anterior
Nodo * LeerAnterior() cons { return m_pPrev; }
};
Es bastante similar; se destaca la presencia del nuevo puntero y los
métodos correspondientes a su lectura y modifi cación:
Nodo * m_pPrev;
La clase Lista ahora tendrá no solo un puntero al primer nodo de la
lista, sino también un puntero al último de la lista:
class Lista
{
// Puntero al primer nodo de la lista (NULL si la lista está vacía)
Nodo * m_pPrimerNodo;
// Puntero al último nodo de la lista (NULL si la lista está vacía)
Nodo * m_pUltimoNodo;
// Cantidad de nodos en lista
int m_iCantidad;
public:
C++ 23
www.redusers.com
Lista();
~Lista();N
// Elimino todos los nodos de la lista
void Limpiar();
// Inserto un nodo por delante de la lista
bool InsertarDelante(const int & dato);
// Inserto un nodo por detrás de la lista
bool InsertarDetras (const int & dato);
// Extrae un nodo por delante de la lista
bool ExtraerDelante(int & dato);
// Extrae un nodo por detrás de la lista
bool ExtraerDetras(int & dato);
// Devuelve la cantidad de nodos que posee la lista
int LeerCantidad() { return m_iCantidad; };
};
Como se puede apreciar, solamente agregamos el puntero al último
nodo, llamado m_pUltimoNodo.
A continuación, veamos en qué cambia cada una de las cuatro
operaciones básicas:
• Inserción de un nodo por delante: en las operaciones
de inserciones y remoción de una lista doblemente enlazada,
deberemos actualizar más punteros, como observamos en el
siguiente diagrama.
1. La lista, antes de realizarse alguna modificación.
primer nodoNULL último
nodo
NULL
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS24
www.redusers.com
2. Creamos el nuevo nodo que insertaremos en la lista.
primer nodoNULL
NULL
último nodo
NULL
NULL
3. Modificamos el puntero a próximo del nuevo nodo, para que apunte al que por el momento es el primer nodo.
primer nodoNULL
NULL
último nodo
NULL
4. Modificamos el puntero a interior del que por el momento es el primer nodo, para que apunte al nuevo nodo.
primer nodoNULL
NULL
último nodo
NULL
5. Modificamos el puntero al primer nodo, para que apunte al nodo a insertar.
primer nodoNULL
NULL
último nodo
NULL
Figura 11. Inserción por delante.
Ahora veamos el código del método InsertarDelante:
bool Lista::InsertarDelante(const in & dato)
{
// 1. Creo nodo a insertar
Nodo * pNodo = new Nodo(dato);
if (pNodo)
{ // 2. Apunto el nodo nuevo adonde apuntaba m_pPrimerNodo
pNodo->FijarProximo(m_pPrimerNodo);
// 3. El primer nodo debe cambiar su “nodo previo” al
C++ 25
www.redusers.com
// nuevo insertado
if (m_pPrimerNodo)
m_iPrimerNodo->FijarAnterior(pNodo);
// 4. El primer nodo pasa a apuntar hacia el nodo insertado
m_pPrimerNodo = pNodo)
// 5. Si la lista estaba vacía (m_pUltimoNodo == NULL), apunto if (!m_pUltimoNodo)
m_pUltimoNodo = pNodo;
// Incremento la cantidad de nodos
m_iCantidad++; return true; } else return false;
}
Analicemos el listado anterior:
Nodo * pNodo = new Nodo(dato);
Primero, creamos el nuevo nodo.
if (pNodo)
Luego verifi camos que el nodo haya sido creado.
pNodo->FijarProximo(m_pPrimerNodo);
A continuación, cambiamos el puntero a próximo del nodo nuevo.
if (m_pPrimerNodo)
m_pPrimerNodo->FijarAnterior(pNodo);
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS26
www.redusers.com
Así, cambiamos el puntero a nodo anterior del nodo que, por el
momento, se encuentra primero en la lista, para que apunte al nodo
nuevo. Esta acción se realiza solo si la lista no se encuentra vacía.
m_pPrimerNodo = pNodo;
El primer nodo de la lista es el que insertamos ahora.
if (!m_pUltimoNodo)
m_pUltimoNodo = pNodo;
Si la lista estaba vacía, entonces el nodo insertado es el primero
y también el último.
Por lo tanto, debemos modifi car el puntero a último nodo para
que apunte al nodo nuevo.
m_iCantidad++;
Aumentamos el contador de cantidad de nodos en lista.
• Inserción de un nodo por detrás: la inserción por detrás será muy
similar a la inserción por delate, ya que la lista ahora es simétrica y
tenemos un puntero al último nodo.
Veamos la Figura 12.
1. La lista, antes de realizarse alguna modificación.
primer nodoNULL último
nodo
NULL
2. Creamos el nuevo nodo que insertaremos en la lista.
primer nodoNULL
NULL
último nodo
NULL
NULL
C++ 27
www.redusers.com
NULL
3. Modificamos el puntero a próximo del nuevo nodo, para que apunte al que por el momento es el último nodo de la lista.
primer nodoNULL último
nodo
NULL
NULL
4. Modificamos el puntero a próximo del actual último nodo de la lista, para que apunte al nodo nuevo.
primer nodoNULL
primer nodoNULL
último nodo
5. Modificamos el puntero último de la lista, para que apunte al nodo nuevo.
último nodo
NULL
Figura 12. Inserción por detrás.
bool Lista::InsertarDetras(const int & dato)
{
// 1. Creo nodo a insertar
Nodo * pNodo new Nodo(dato);
if (pNodo)
{ // 2. Apunto el nodo nuevo adonde apuntaba m_pUltimoNodo
pNodo->FijarAnterior(m_pUlimoNodo);
// 3. El que antes era el último nodo, ahora debe apuntar al nodo
insertado
if (m_pUltimoNodo)
m_pUltimoNodo->FijarProximo(pNodo);
// 4. El primer nodo pasa a apuntar hacia el nodo insertado
m_pUltimoNodo = pNodo;
// 5. Si la lista estaba vacía (m_pUltimoNodo == NULL), apunto
// el último nodo hacia el nodo insertado
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS28
www.redusers.com
if (!m_pPrimerNodo)
m_pPrimerNodo = pNodo;
// Incremento la cantidad de nodos
m_iCantidad++;
return true;
}
else
return false;
}
Analizando el listado, veremos que es muy similar al método
InsertarDelante, cambiando el uso de los punteros. Veamos:
Nodo * pNodo = new Nodo(dato);
Para comenzar, creamos el nuevo nodo.
pNodo->FijarAnterior(m_pUltimoBodo);
Modifi camos el puntero a nodo anterior del nodo nuevo a insertar,
de modo que apunte al que, por el momento, es el último nodo.
if (m_pUltimoNodo)
m_pUltimoNodo->FijarProximo(pNodo);
Si existe un último nodo, es decir, si la lista no está vacía, entonces le
modifi camos el puntero a nodo próximo para que apunte al nuevo nodo.
m_pUltimoNodo = pNodo;
Modifi camos el puntero al último nodo para que apunte al nodo nuevo.
C++ 29
www.redusers.com
if (!m_pPrimerNodo)
m_pPrimerNodo = pNodo;
Si no existe un primer nodo (si la lista está vacía), el último nodo
también será primero. Por ende, modifi camos el puntero al primer nodo.
• Extracción por delante: la extracción por delante es relativamente
sencilla, como veremos a continuación (Figura 13):
Figura 13. Extracción por delante.
1. La lista, antes de realizarse alguna modificación.
primer nodoNULL último
nodo
NULL
2. Creamos un puntero que apunte al primer nodo de la lista, que es el nodo a extraer.
primer nodoNULL último
nodo
NULL
pNodoAExtraer
3. Modificamos el puntero al primer nodo de la lista que apunte al próximo nodo a extraer.
NULL último nodo
NULL
4. Modificamos el puntero del nuevo primero nodo, para que apunte a NULL.
5. Modificamos el puntero a próximo nodo del nodo a extraer, para que apunte NULL.
pNodoAExtraer primer nodo
NULL
NULL
último nodo
NULL
pNodoAExtraer primer nodo
NULL
NULL
NULL
último nodo
NULL
pNodoAExtraer primer nodo
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS30
www.redusers.com
bool Lista::ExtraerDelante(int & dato)
{
// Creo puntero NodoAExtraer
Nodo * pNodoAExtraer;
// ¿La lista está vacía?
if (m_pPrimerNodo)
{
// 1. Apunto el NodoAExtraer al primer nodo
pNodoAExtraer = m_pPrimerNodo;
// 2. Modifi co el primer Nodo para que apunte al próximo nodo
m_pPrimerNodo = m_pPrimerNodo->LeerProximo();
// 3. Si existe dicho nodo, modifi co su propiedad m_prev para que
apunte a NULL
if (m_pPrimerNodo)
m_pPrimerNodo->FijarAnterior(NULL);
else
// Si la lista quedó vacía, apunto el último nodo a NULL
m_pUltimoNodo = NULL;
// 4. Fijo el puntero a próximo del nodo a eliminar a NULL
pNodoAExtraer->FijarProximo(NULL);
// Copio el dato y lo paso como parámetro
dato = pNodoAExtraer->LeerDato();
// Elimino el nodo
delete pNodoAExtraer;
// Decremento la cantidad de nodos
m_iCantidad-;
return true;
}
else
return false;
}
C++ 31
www.redusers.com
Analicemos el código del listado anterior.
Nodo * pNodoAExtraer;
En primer lugar, creamos una variable local puntero a un nodo.
if (m_pPrimerNodo)
A continuación, verifi camos que la lista no esté vacía.
pNodoAExtraer = m_pPrimerNodo;
En esta línea de código, fi jamos entonces el valor del puntero local
para que apunte al primer nodo, que es el nodo a extraer.
m_pPrimerNodo = m_pPrimerNodo->LeerProximo();
Luego modifi camos el puntero a primer nodo para que apunte al
nodo próximo al nodo a extraer, que será, a partir de ahora, el primer
nodo de la lista.
if (m_pPrimerNodo)
m_pPrimerNodo->FijarAnterior(NULL);
Verifi camos que el nuevo primer nodo realmente exista (cuando
solo queda un nodo, el nodo próximo al nodo a extraer es NULL), para
modifi car su puntero a nodo anterior, que, como será el primer nodo
de la lista, deberá ser NULL.
else
m_pUltimoNodo = NULL;
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS32
www.redusers.com
Si no existe el nuevo primer nodo, entonces la lista quedará vacía,
y deberemos modifi car el valor del puntero al último nodo.
pNodoAExtreaer->FijarProximo(NULL);
Modifi camos el puntero a próximo nodo del nodo a extraer, ya que,
como lo eliminaremos, queremos evitar que su destructor destruya
otros nodos.
dato = pNodoAExtraer->LeerDato();
Rescatamos el dato que debemos retornar por método.
delete pNodoAExtraer;
Y así destruimos el nodo.
m_iCantidad-;
Por último, decrementamos el contador de cantidad de nodos
en lista.
• Extracción por detrás: la extracción por detrás también es
muy similar a la extracción por delante debido a la simetría
de la lista.
1. La lista, antes de realizarse alguna modificación.
primer nodoNULL último
nodo
NULL
2. Creamos un puntero que apunte al último nodo de la lista, que es el nodo a extraer.
primer nodoNULL último
nodo
NULL
pNodoAExtraer
C++ 33
www.redusers.com
primer nodo
3. Modificamos el puntero a último nodo de la lista, para que apunte al nodo anterior del nodo a extraer.
primer nodo
NULLNULL
4. Modificamos el puntero a próximo del nuevo último nodo, para que apunte a NULL.
5. Modificamos el puntero nodo anterior, del nodo a extraerpara que apunte a NULL.
pNodoAExtraerúltimo nodo
pNodoAExtraerúltimo nodo
pNodoAExtraerúltimo nodo
NULL
NULL
NULL
primer nodoNULL
NULL
NULL
Figura 14. Extracción por detrás.
bool Lista::ExtraerDetras(int & dato)
{
// Creo puntero NodoExtraer
Nodo * pNodoAExtraer;
// ¿La lista está vacía?
if (m_pUltimoNodo=)
{
// 1. Apunto el NodoAExtraer al primer nodo
pNodoAExtraer = m_ pUltimoNodo;
// 2. Modifi co el pimer Nodo para que apunte al próximo nodo
m_pUltimoNodo = m_pUltimoNodo->LeerAnterior();
// 3. Si existe dicho nodo, modifi co su propiedad m_prev para
// que apunte a NULL
if (m_pUltimoNodo)
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS34
www.redusers.com
m_pUltimoNodo->FijarProximo(NULL);
else
// La lista quedó vacía, cambio el apuntador
al PrimerNodo
m_pPrimerNodo = NULL;
// Copio el dato y lo paso como parámetro
dato = pNodoAExtraer->LeerDato();
// Elimino el nodo
delete AExtraer;
// Decremento la cantidad de nodos
m_iCantidad-;
return true;
}
else
return false;
}
Nodo * pNodoAExtraer;
En la primera línea, creamos una variable local puntero a un nodo.
if (m_pUltimoNodo)
Verifi camos que la lista no está vacía.
pNodoAExtraer = m_pUltimoNodo;
Fijamos el valor del puntero local para que apunte al último nodo,
que es el nodo a extraer, y que será, a partir de ahora, el último nodo
de la lista.
C++ 35
www.redusers.com
if (m_pUltimoNodo)
m_pUltimoNodo->FijarProximo(NULL);
Verifi camos que el nuevo último nodo realmente exista (cuando
solo queda un nodo, el nodo anterior al nodo a extraer es NULL), para
modifi car su puntero a nodo próximo, que, como será el último nodo
de la lista, deberá ser NULL.
else
m_pPrimerNodo = NULL;
Si no existe el nuevo último nodo, entonces la lista quedará vacía,
y deberemos modifi car el valor del puntero al primer nodo.
dato = pNodoAExtraer->LeerDato();
Rescatamos el dato que debemos retornar por el método.
delete pNodoAExtraer;
Luego destruimos el nodo.
m_iCantidad-;
Y decrementamos el contador de cantidad de nodos en lista.
PilaCrear una pila, tomando como base alguna de las listas que hemos
creado, es realmente muy sencillo. Veamos:
class Pila
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS36
www.redusers.com
{
Lista m_lista;
public:
bool Insertar(int & dato) { return m_lista.InsertarDelante(dato); }
bool Extraer(int & dato) { return m_lista.ExtraerDelante(dato); }
bool EstaVacia(void) { return (m_lista.LeerCantidad() == 0); }
};
El método Insertar realizará una inserción por delante de la lista.
Por lo tanto, bastará con que invoquemos el método InsertarDelante
de la lista que posee la pila como propiedad.
El método Extraer hará una extracción por delante, debido a que en
una pila se inserta y extrae por el mismo extremo. Entonces, realizaremos
la invocación del método ExtraerDelante de la lista que posee la pila como
propiedad. El método EstaVacia simplemente retorna el resultado de la
expresión que verifi ca si la cantidad de elementos de la lista es cero.
ColaCrear una cola también es muy sencillo haciendo uso de nuestra lista.
class Cola
{
Lista m_lista;
public:
Cola(void);
~Cola(void);
Recordemos que en el Capítulo 6 habíamos hecho una introducción acerca de cómo funcionaban
estas estructuras de datos, y también habíamos realizado una implementación sobre una clase
que encapsulaba un array. Si tenemos dudas en este punto, podemos consultarlo antes de continuar.
PILA Y COLA
C++ 37
www.redusers.com
bool Insertar(int & dato) { return m_lista.InsertarDelante(dato); }
bool Extraer(int & dato) { return m_lista.ExtraerDelante(dato); }
bool EstaVacia(void) { return (m_lista.LeerCantidad() == 0); }
};
Esto es muy similar a la pila, solamente que, en este caso, Extraer
invoca el método ExtraerDetras, ya que en una cola se inserta por un
extremo y se extrae por el otro.
De las implementaciones de listas enlazadas y doblemente enlazadas
que vimos, basta decir que poseen una limitación enorme: trabajan con
un tipo de dato predeterminado (que en nuestro ejemplo es un número
entero).
Veremos ahora que C++ dispone de un recurso muy poderoso con el
cual es posible evitar esa limitación de raíz.
PlantillasHasta ahora, hemos implementado una lista doblemente enlazada
de números enteros, pero ¿qué ocurriría si necesitáramos una lista
enlazada de números fl otantes? Desgraciadamente, deberíamos
recodifi car todo de nuevo.
Para evitar eso, C++ ofrece un mecanismo llamado plantillas, que
permite crear estructuras de datos independientes del tipo. Cuando
introdujimos el tema de funciones, vimos que por medio de ellas era
posible realizar una operación basándonos en datos parametrizados
que podían variar invocación tras invocación.
Las plantillas, de cierto modo, tienen algo en común a esto: pueden
recibir uno o varios parámetros en el momento de su creación. Pero,
en lugar de referirnos a una variable, nos estamos refi riendo a un tipo
de variable.
De esta manera, nuestras listas no trabajarán con enteros o con
fl otantes, ni con ningún tipo de dato preestablecido, sino que lo harán
con cualquiera que le especifi quemos al realizar la instancia del
objeto en cuestión.
Veamos un ejemplo:
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS38
www.redusers.com
class array
{
// Cantidad de elementos actual del array
int m_iCantElementos;
// Puntero a array de números enteros
int * m_piArray;
public:
Array(int iCantidadElementos);
~Array(void);
// Fija el tamaño del array
bool FijaTam(int iCantidadElementos);
// Lee el tamaño del array
int LeerTam() const;
// Fija el valor en un elemento determinado del array
bool FijarValor(inti Elemento, inti Valor);
// Retorna el valor en un elemento determinado del array
bool LeerValor(intiElemento, int & iValor) const;
// Limpia el array con el valor pasado como parámetro
void Limpiar(inti Valor);
};
La clase Array que construimos algunos capítulos atrás solo podía
manejar enteros. Veamos ahora cómo convertir dicha clase en una
plantilla y, de ese modo, poder manejar diferentes tipos de datos.
En primera instancia, cambiaremos la declaración de la clase
agregando la palabra reservada template antes del clásico class.
Revisemos cómo escribíamos antes:
class Array
C++ 39
www.redusers.com
{
// declaración de propiedades y métodos
};
Utilizando template sería:
template <class T>
class Array
{
};
¿Qué fue lo que hicimos aquí? Convertimos nuestra clase en una
plantilla. En este caso, nuestra plantilla podrá recibir un tipo de
dato, representado por la letra T, en el momento de la instanciación
(podría haber sido cualquier letra o palabra que siguiera la regla de
cualquier otro identifi cador).
Veamos cómo sería la instanciación de un objeto tipo Array<T>.
Antes escribíamos:
int main()
{
// Antes, creábamos un objeto del tipo Array del siguiente modo:
Array unObjArray;
// …
}
Una plantilla podría recibir más de un tipo de dato; simplemente debemos realizar una separación
por medio de comas dentro de las llaves. Por ejemplo:
template >class T1, class T2>
class …
CANTIDAD DE TIPOS DE UNA PLANTILLA
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS40
www.redusers.com
Ahora podemos escribir:
int main()
{
// Ahora, como Array es una plantilla lo haremos así:
Array<int> unObjArray;
// …
}
La novedad es haber colocado un tipo de dato entre llaves
inmediatamente después del nombre de la plantilla. No existe otro
modo de realizar la instanciación, ya que es necesario pasarle el tipo
de dato con el cual trabajará la plantilla.
En nuestro ejemplo, decidimos hacerlo con el tipo de dato entero,
pero otras instanciaciones válidas también habrían sido las siguientes:
Array<int> arr1;
Array<fl oat> arr2;
Array<char> arr3;
Array<unTipoDeObjetoCualquiera> arr4;
Del listado anterior, observamos rápidamente que todos estos
nuevos objetos tipo Array manejan diferentes tipos de datos y, sin
embargo, no tuvimos la necesidad de modifi car nuestra plantilla.
Sin duda, las plantillas son un recurso poderoso del lenguaje.
¿Qué es lo que hace realmente C++ en estos casos? Cuando
codifi camos una plantilla, si esta no es utilizada, no formará
parte nuestro programa. Dicho de otro modo: la plantilla solo es
compilada cuando el compilador advierte que es utilizada; en ese
caso, construye una clase con el tipo de dato correspondiente, y
es compilada. Por esta razón, es posible que no se adviertan errores
en la defi nición de los métodos de una plantilla hasta que se invoque
el método en cuestión por medio de una instancia de dicha plantilla.
El compilador creará las clases que correspondan por cada tipo de
dato con el cual se utilice la plantilla.
C++ 41
www.redusers.com
Veamos ahora cómo sería la declaración completa de nuestra plantilla
Array en el código de ejemplo que se muestra en la página siguiente:
template <class T>
class Array
{
// Cantidad de elementos actual del array
int m_iCantElementos;
// Puntero a array
T * m_pArray;
public:
Array(int iCantidadElementos);
~Array(void);
// Fija el tamaño del array
bool FijaTam(int iCantidadElementos);
// Lee el tamaño del array
int LeerTam() const;
// Fija el valor en un elemento determinado del array
bool FijarValor(inti Elemento, T valor);
// Retorna el valor en un elemento determinado del array
bool LeerValor(inti Elemento, T & valor) const;
// Limpia el array con el valor pasado como parámetro
void Limpiar(T valor);
};
Observemos que hemos reemplazado todos los tipos de datos
int (que antes hacían referencia al tipo de dato utilizado por la clase)
por el parámetro T, que representa el tipo de dato que es pasado al
momento de instanciar el objeto.
T * m_pArray;
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS42
www.redusers.com
Según esta línea de código, la propiedad en la que se almacenaba
el puntero a un array de números enteros es ahora una propiedad en
la que se almacena un puntero a un array de tipo de datos T.
bool FijarValor(int iElemento, T valor);
El método que utilizábamos para fi jar un valor a un elemento del array,
ahora, en su segundo parámetro, no recibe un entero sino un tipo de dato
T (que podrá ser fi nalmente un entero, un fl otante, un carácter, etcétera).
bool LeerValor(int iElemento, T & valor) const;
void Limpiar(T valor);
Lo mismo ocurre con el método LeerValor y Limpiar, y todo método
que reciba o entregue datos relacionados con el tipo del array.
Entonces, ¿qué cambia en la defi nición de estos métodos? En primer
lugar, las defi niciones de los métodos de las plantillas no se colocan en
los archivos cpp, sino en el mismo archivo cabecera en el que se realiza
su declaración. Es decir que la defi nición de los métodos que teníamos
en el archivo array.cpp se moverá al archivo array.h justo después de la
declaración de la plantilla.
Figura 15. En las plantillas, la defi nición de todos los métodos debe colocarse en el archivo cabecera.
Además, existe un cambio en el modo de defi nir el método.
Anteriormente, al método Limpiar lo defi níamos de esta forma:
clase convencional plantilla
cabecera(.h)
(declaración de la clase)
cuerpo (.cpp)
(definición de los métodos)
cabecera(.h)
(declaración de la plantilla)
+(definición de los métodos)
cuerpo (.cpp)
C++ 43
www.redusers.com
int Array::LeerTam() const
{
return m_iCantElementos;
}
Ahora, deberemos anteponer template<class T> ante cada método y,
además, deberemos agregar un <T> después del nombre de la clase.
Veamos:
template <class T>
int Array<T>::LeerTam() const
{
return m_iCantElementos;
}
Claro que siempre tendremos la posibilidad de colocar la defi nición
de los métodos dentro del cuerpo de la plantilla. En dicho caso, no existe
salvedad alguna; si procediéramos de esa manera, como con el método
Limpiar, quedaría codifi cado de acuerdo a como se muestra a continuación:
template <class T>
class Array
{
// …
public:
// …
int LeerTam() const
{
return m_iCantElementos;
}
// …
};
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS44
www.redusers.com
Ahora, veamos la declaración completa de todos los métodos de la clase.
El constructor:
template <class T>
Array<T>::Array(int iCantidadElementos)
{
// Asigno el valor
m_iCantElementos = iCantidadElementos;
// Creo un array de forma dinámica en función de la cantidad de elementos fi jada
m_pArray = new T[iCantidadElementos];
}
El destructor:
template <class T>
Array<T>::~Array(void)
{
// Libero la memoria solicitada
if (m_pArray)
delete [] m_pArray;
}
El método FijaTam:
template <class T>
bool Array<T>::FijaTam(int iCantidadElementos)
{
m_iCantElementos = iCantidadElementos;:
// Libero la memoria solicitada
C++ 45
www.redusers.com
if (m_pArray)
delete [] m_pArray;
// Vuelvo a solicitar memoria con el nuevo valor
m_pArray = new T[m_iCantElementos];
if [m_pArray)
return true;
else
return false;
}
El método FijarValor:
template <class T>
bool Array<T>::FijarValor(int iElemento, T valor)
{
// Verifi co que el elemento propuesto sea válido
if (iElemento >= 0 && iElemento < m_iCantElementos)
{ m_pArray[iElemento];
return true;
}
else
return false;
}
El método LeerValor:
template <class T>
bool Array<T>::LeerValor(inti Elemento, T & valor) const
{
if (iElemento >= 0 && iElemento < m_iCanElementos)
{ valor = m_pArray[iElemento];
return true;
}
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS46
www.redusers.com
else
return false;
}
El método Limpiar:
template <class T>>
void Array<T>::Limpiar(T valor)
{
for (int i=0; i<m_iCantElementos; i++)
m_pArray[i] = valor;
}
El método LeerTam:
template <class T>
int Array<T>::LeerTam() const
{
return m_iCantElementos;
}
Si realizamos operaciones aritméticas con variables del tipo T,
en el momento de realizar la instanciación con ciertos tipos de datos,
es posible que exista un error de compilación. Veamos un ejemplo:
template <class T>
void Foo<T>::Inc(T valor)
{
Valor++;
}
En el listado anterior, es posible que el operador de postincremento
no aplique al tipo de dato T para alguna instancia. Lo mismo podría
ocurrir en el momento de realizar cualquier tipo de comparación lógica.
C++ 47
www.redusers.com
Listas doblemente enlazadas con plantillas
Ahora modifi quemos nuestra lista doblemente enlazada para que
sea una plantilla y, de este modo, pueda trabajar con cualquier tipo
de dato, no solo con números enteros. ¡Es muy fácil!
template <class T>
class Nodo
{
T m_dato;
Nodo<T> * m_pProx;
Nodo<T> * m_pPrev;
Public;
// Constructor estándar
Nodo() : m_pProx(NULL), m_pPrev(NULL) {}
// Constructor de copia
Nodo(const Nodo& copiaDe) : m_pProx(NULL), m_pPrev(NULL)
{ m_dato = copiaDe.m_dato; }
// Constructor con parámetros iniciales
Nodo(Tdato) : m_dato(dato), m_pProx(NULL), m_pPrev(NULL) {}
// Destructor
~Nodo()
{
if (m_pProx)
delete this->m_pProx;
}
// Seteo los valores del nodo
void FijarValor(T dato) { m_dato = dato; }
// Seteo el nodo próximo
void FijarProximo(Nodo<T> * pProx) { m_pProx = pProx; }
// Seteo el nodo anterior
void FijarAnterior(Nodo<T> * pPrev) { m_pPrev = pPrev; }
// Leo la propiedad m_dato
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS48
www.redusers.com
T LeerDato() const { return m_dato; }
// Tomo la propiedad próximo nodo
Nodo<T> * LeerProximo() const { return m_pProx; }
// Tomo la propiedad nodo anterior
Nodo<T> * LeerAnterior() const { return m_pPrev; }
};
En pocas palabras, cambiamos todos los int relacionados con el dato
encapsulado dentro de la clase Nodo por el T del tipo de dato de la plantilla.
template <class T>class Lista{ // Puntero al primer nodo de la lista (NULL si la lista está vacía) Nodo<T> * m_pPrimerNodo; // Puntero al último nodo de la lista (NULL si la lista está vacía)Nodo<T> * m_pUltimoNodo;
// Cantidad de nodos en la listaint m_iCantidad;
public:Lista();Virtual ~Lista(); // Inserto un nodo por delante de la listabool InsertarDelante(const T & dato);
// Inserto un nodo por detrás de la listabool InsertarDetras(const T & dato);
// Extrae un nodo por delante de la listabool ExtraerDelante(T & dato);// Extrae un nodo por detrás de la listabool ExtraerDetras(T & dato);
// Devuelve la cantidad de nodos que posee la lista
int LeerCantidad() { return m_iCantidad; };
};
C++ 49
www.redusers.com
Procedemos de manera análoga con la clase Lista. Notemos que
esta clase hace uso de propiedades tipo Nodo, que ahora son del tipo
Nodo<T> debido a que le hacemos el pase del tipo de dato con el cual
se instancia la lista. La defi nición de los métodos también deberá ser
modifi cada, ya que donde exista:
Nodo * pNodo = new Nodo(dato);
tendremos que colocar:
Nodo<T> * pNodo = new Nodo<T>(dato);
Además, deberemos pasar todo este código a la cabecera (al archivo
Lista.h), porque, como habíamos mencionado, las plantillas se codifi can
enteramente allí.
Por lo tanto, el código de los métodos quedará del siguiente modo.
El constructor:
template <class T>
Lista<T>::Lista(void)
{
m_pPrimerNodo = m_pUltimoNodo = NULL;
m_iCantidad= 0;
}
El destructor:
template <class T>
Lista<T>::~Lista(void)
{
if (m_pPrimerNodo)
delete m_pPrimerNodo;
}
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS50
www.redusers.com
El método Limpiar:
template <class T>
void Lista<T>::limpiar()
{
if (m_pPrimerNodo)
delete m_pPrimerNodo;
m_pPrimerNodo = m_pUltimoNodo = NULL;
m_iCantidad = 0;
}
El método InsertarDelante:
template <class T>
bool Lists<T>::InsertarDelante(const T & dato)
{
// 1. Creo nodo a insertar
Nodo<T> * pNodo = new Nodo<T>(dato);
if (pNodo)
{
// 2. Apunto el nodo nuevo a donde apuntaba m_pPrimerNodo
pNodo->FijarProximo(m_pPrimerNodo);
// 3. El primer nodo debe cambiar su “nodo previo” al nuevo insertado
if (m_pPrimerNodo)
m_pPrimerNodo->FijarAnterior(pNodo);
// 4. El primer nodo pasa a apuntar hacia el nodo insertado
m_pPrimerNodo = pNodo;
// 5. Si la lista estaba vacía (m_pUltimoNodo == NULL), apunto el
último nodo hacia el nodo insertado
if (!m_pUltimoNodo)
C++ 51
www.redusers.com
m_pUltimoNodo = PnODO;
// Incremento la cantidad de nodos
m_iCantidad++;
return true;
}
else return false;
}
El método InsertarDetras:
template <class T>
bool Lista<T>::InsertarDetras(const T & dato)
{
// 1. Creo nodo a insertar
Nodo<T> * pNodo = new Nodo<T>(dato);
if (pNodo)
{
// 2. Apunto el nodo nuevo adonde apuntaba m_pUltimoNodo
pNodo->FijarAnterior(m_pUltimoNodo);
// 3. El que antes era el último nodo, ahora debe apuntar
al nodo insertado
if (m_pUltimo nod)
m_pUltimoNodo->FijarProximo(pNodo);
// 4. El primer nodo pasa a apuntar hacia el nodo insertado
m_pUltimoNodo = pNodo;
// 5. Si la lista estaba vacía (m_pUltimoNodo == NULL), apunto
// el último nodo hacia el nodo insertado
if (!m_pPrimerNodo)
m_pPrimerNodo = pNodo;
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS52
www.redusers.com
// Incremento la cantidad de nodos
m_iCantidad++;
return true;
}
else
return false;
}
El método ExtraerDelante:
template <class T>
bool Lista<T>::ExtraerDelante(T & dato)
{
// Creo puntero NodoAExtraer
Nodo<T> * pNodoAExtraer
// ¿La lista está vacía?
if (m_pPrimerNodo)
{
// 1. Apunto el NodoAExtraer al primer nodo
pNodoAExtraer = m_pPrimerNodo;
// 2. Modifi co el pimer nodo para que apunte al próximo nodo
m_pPrimerNodo = m_pPrimerNodo->LeerProximo();
// Si existe dicho nodo, modifi co su propiedad m_prev para
// que apunte a NULL
if (m_pPrimerNodo)
m_pPrimerNodo->FijarAnterior(NULL);
else
// Si la lista quedó vacía, apunto el último nodo
a NULL
m_pUltimoNodo = NULL;
// 4. Fijo el puntero a próximo del nodo a eliminar a NULL
C++ 53
www.redusers.com
pNodoAExtraer->FijarProximo(NULL);
// Copio el dato y lo paso como parámetro
dato = pNodoAExtraer->LeerDato();
// Elimino el nodo
delete pNodoAExtraer;
// Decremento la cantidad de nodos
m_iCantidad-;
return true;
}
else
return false;
}
El método ExtraerDetras:
template <class T>
bool Lista<T>::ExtraerDetras(T & dato)
{
// Creo puntero NodoAExtraer
Nodo<T> * pNodoAExtraer;
// ¿La lista está vacía?
if (m_pUltimoNodo)
{
// 1. Apunto el NodoAExtraer al primer nodo
pNodoAExtraer = m_pUltimoNodo;
// 2. Modifi co el pimer nodo para que apunte al próximo nodo
m_pUltimoNodo = m_pUltimoNodo->LeerAnterior();
// 3. Si existe dicho nodo, modifi co su propiedad m_prew para
// que apunte a NULL
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS54
www.redusers.com
if m_pUltimoNodo)
m_pUltimoNodo->FijarProximo(NULL);
else
// La lista quedó vacía, cambio el apuntador
Al primernodo
m_pPrimerNodo = NULL;
// Copio el dato y lo paso como parámetro
dato = pNodoAExtraer->LeerDato();
// Elimino el nodo
delete pNodoAExtraer;
// Decremento la cantidad de nodos
m_iCantidad-;
return true;
}
else
return false;
}
No realizaremos un análisis método por método porque,
lógicamente, estos son iguales a los utilizados en la lista que manejaba
números enteros; solamente cambiaron los tipos int relacionados
con el dato tipo T. Por lo tanto, ¡ya tenemos nuestra lista doblemente
enlazada, que puede manejar cualquier tipo de dato!
Pila¿Cómo hacer uso de nuestra fl amante plantilla Lista para crear una
Pila? Veamos:
template <class T>
class Pila
C++ 55
www.redusers.com
{
Lista<T> m_lista;
public:
bool Insertar(T & dato) { return m_lista.InsertarDelante(dato); }
bool Extraer(T & dato) { return m_lista.ExtraerDelante(dato); }
bool EstaVacia(void) { return (m_lista.LeerCantidad() == 0); }
};
ColaLa implementación de la cola es tan sencilla como la de la pila:
template <class T>
class Cola
{
Lista<T> m_lista;
Public;
bool Insertar(T & dato) { return m_lista.InsertarDelante(dato); }
bool Extraer(T & dato) { return m_lista.ExtraerDelante(dato); }
bool EstaVacia(void) { return (m_lista.LeerCantidad() == 0); }
};
Los iteradoresUtilizando la plantilla de la lista doblemente enlazada, la pila y la cola
han quedado muy bien implementadas. Sin embargo, respecto de la lista
en sí nos falta algo bastante importante: un modo de recorrer los nodos.
Supongamos que una aplicación desea hacer uso de nuestra plantilla
Lista. Veamos:
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS56
www.redusers.com
fl oat fValor;
Lista<fl oat> lis;
fValor = 1.0f;
lis.InsertarDelante(fValor);
fValor = 2.0f;
lis.InsertarDelante(fValor);
fValor = 3.0f;
lis.InsertarDelante(fValor);
Bien, hemos insertado tres números en nuestra lista de números
fl otantes. ¿Cómo podríamos hacer ahora si quisiéramos recorrer la lista
de punta a punta sin extraer nodos?
Una posible solución sería retornando el puntero al primer nodo y,
desde fuera, hacer una llamada a nodo próximo (es decir, algo parecido al
modo en que la recortamos desde dentro). Pero, de esta forma, estaríamos
exponiendo el funcionamiento interno de la lista a quien quiera usarla.
Una solución más elegante es el uso de iteradores. Un iterador es
un objeto que almacena en su interior un puntero a un nodo de la lista
y ofrece primitivas para moverse por ella. Analicemos el siguiente código:
template <class>class Iterador{ Nodo<T> * m_pNodo;public: // Constructor Iterador(void) { m_pNodo = NULL; } // Constructor de copia Iterador(const Iterador<T> & it) { m_pNodo = it.m_pNodo; } // Destructor ~Iterador(void) {}
// Fija valor al iterador
void FijarValor(Nodo<T> * pNodo) { m_pNodo = pNodo; }
C++ 57
www.redusers.com
// Mueve el curso al próximo nodo
void MoverProximo();
// Mueve el cursor al nodo anterior
// Retorna el dato asociado al nodo apuntado
bool LeerDato(T & dato);
// Indica si el iterador se encuentra apuntando a un nodo válido
bool Final() { return (m_pNodo == NULL); }
};
Como se puede apreciar, la plantilla Iterador posee en su interior
una propiedad puntero a nodo; este nodo no será el primero o el último
necesariamente, sino que será el nodo apuntado por el iterador en un
momento determinado.
Luego existirán varias primitivas con las cuales podremos operar
con el iterador:
• MoverProximo: se moverá hacia el próximo nodo de la lista.
• MoverAnterior: se moverá al nodo anterior de la lista.
• LeerDato: retornará el dato asociado con el nodo actual.
• Final: indica si el iterador se encuentra apuntando a un nodo válido
de la lista.
El método MoverProximo:
template <class T>
void Iterator<T>::MoverProximo()
{
if (m_pNodo)
m_pNodo = m_pNodo->LeerPróximo();
}
Simplemente verifi camos que el nodo apuntado sea válido y luego
nos movemos al próximo haciendo uso del método LeerProximo de la
plantilla Nodo.
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS58
www.redusers.com
El método LeerDato:
template <class T>
bool Iterador<T>::LeerDato(T & dato)
{
if (m_pNodo)
{
Dato = m_pNodo->LeerDato();
}
else
return false;
}
También es muy sencillo: verifi camos el puntero y después
invocamos el método LeerDato. Para la implementación de iteradores,
tendremos que hacer un agregado a la plantilla Lista.
Crearemos un nuevo método llamado Comienzo, que retornará
un iterador apuntando al primer nodo de la lista (podríamos crear
otro método que retorne un iterador al último nodo). Veamos cómo
debería estar defi nido:
template <classT>
Iterador<T> Lista<T>::Comienzo()
{
Iterador<T> it;
It.FijarValor(m_pPrimerNodo);
return it;
}
El uso de iteradores facilita la navegación por una estructura de
datos sin necesidad de saber cómo está compuesta o de qué manera
funciona dicha estructura.
Existe la posibilidad de tener muchos iteradores para la misma
lista, solo debemos tener la precaución de reiniciarlos una vez que
se haya modifi cado la lista, ya que podrían quedar apuntando a un
nodo ya eliminado. Sería muy útil sobrecargar algunos operadores
C++ 59
www.redusers.com
para que el uso del iterador sea más sencillo e intuitivo. Por
ejemplo, podríamos sobrecargar el operador de postincremento
para movernos al próximo nodo.
En este capítulo, descubrimos algunos conceptos fundamentales de la programación en C++.
Estudiamos cómo crear una estructura de datos dinámica a través de las listas enlazadas y de qué forma,
a través de las plantillas, podemos modifi car todas las estructuras.
RESUMEN
APÉNDICE B. ESTRUCTURAS DE DATOS DINÁMICAS Y PLANTILLAS60
www.redusers.com
Actividades
TEST DE AUTOEVALUACIÓN
1 ¿Qué ventaja tiene una lista doblemente enlazada sobre una simplemente enlazada?
2 ¿Cuál es la ventaja de una lista simplemente enlazada sobre una doblemente enlazada?
3 Qué es una plantilla y para qué se utiliza?
4 ¿Qué son los iteradores?
EJERCICIOS PRÁCTICOS
1 Para una lista simplemente enlazada de números enteros, agregue un método llamado InsertarDespuesDe que reciba un puntero a un nodo como parámetro y un dato. Este método deberá insertar un nodo después del nodo especifi cado.
2 Para una lista simplemente enlazada de números enteros, agregue un método llamado InsertarEnOrden que inserte el número entero pasado como paráme-tro dentro de la lista, manteniendo un orden ascendente.
Si tiene alguna consulta técnica relacionada con el contenido, puede contactarse con nuestros expertos: [email protected]
PROFESOR EN LÍNEA