Pilas y colas
Las pilas y colas son estructuras de datos fundamentales que siguen reglas específicas para el acceso y manipulación de elementos. A diferencia de los arrays o listas donde puedes acceder a cualquier elemento directamente, estas estructuras imponen un orden particular que resulta extremadamente útil en numerosos algoritmos y aplicaciones del mundo real. Su comportamiento predecible y sus operaciones eficientes las convierten en herramientas esenciales para resolver problemas complejos de programación.
Estas estructuras son la base de muchos sistemas informáticos: desde el manejo de la memoria en los programas hasta la gestión de tareas en sistemas operativos, pasando por algoritmos de navegación web y procesamiento de datos. Comprender su funcionamiento te permitirá abordar problemas de programación de manera más elegante y eficiente.
Pilas (Stack)
Una pila es una estructura de datos que sigue el principio LIFO (Last In, First Out), lo que significa que el último elemento que se añade es el primero que se retira. Imagina una pila de platos: solo puedes añadir o quitar platos de la parte superior.
Características de las pilas
Característica | Descripción |
---|---|
Principio LIFO | El último elemento añadido es el primero en salir |
Punto de acceso único | Solo se puede acceder al elemento superior |
Operaciones principales | Push (apilar), Pop (desapilar), Peek (mirar) |
Acceso restringido | No se puede acceder a elementos intermedios directamente |
Uso de memoria eficiente | Operaciones en tiempo constante O(1) |
Operaciones principales de una pila
Operación | Descripción | Complejidad |
---|---|---|
Push(item) |
Añade un elemento al tope de la pila | O(1) |
Pop() |
Retira y devuelve el elemento del tope | O(1) |
Peek() o Top() |
Devuelve el elemento del tope sin retirarlo | O(1) |
Count |
Devuelve el número de elementos | O(1) |
Contains(item) |
Verifica si un elemento está en la pila | O(n) |
Implementación básica de pilas en C#
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Crear una pila de enteros
Stack<int> numeros = new Stack<int>();
Console.WriteLine("=== Operaciones básicas con pilas ===");
// Push: añadir elementos
numeros.Push(10);
numeros.Push(20);
numeros.Push(30);
numeros.Push(40);
Console.WriteLine($"Elementos en la pila: {numeros.Count}");
// Peek: ver el elemento superior sin retirarlo
Console.WriteLine($"Elemento en el tope: {numeros.Peek()}");
Console.WriteLine($"Elementos después de Peek: {numeros.Count}");
// Pop: retirar elementos
Console.WriteLine($"Elemento retirado: {numeros.Pop()}");
Console.WriteLine($"Nuevo elemento en el tope: {numeros.Peek()}");
Console.WriteLine($"Elementos restantes: {numeros.Count}");
// Verificar si la pila está vacía
Console.WriteLine($"¿Está vacía? {numeros.Count == 0}");
}
}
Recorrido y manipulación de pilas
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Stack<string> palabras = new Stack<string>();
// Añadir palabras
palabras.Push("Hola");
palabras.Push("mundo");
palabras.Push("desde");
palabras.Push("C#");
Console.WriteLine("=== Recorrido de la pila ===");
Console.WriteLine("Orden de salida (LIFO):");
// Método 1: Recorrido sin modificar la pila
foreach (string palabra in palabras)
{
Console.WriteLine($"- {palabra}");
}
Console.WriteLine($"Elementos después del recorrido: {palabras.Count}");
Console.WriteLine("\n=== Vaciado de la pila ===");
// Método 2: Vaciar la pila usando Pop
while (palabras.Count > 0)
{
string palabraRetirada = palabras.Pop();
Console.WriteLine($"Retirada: {palabraRetirada}");
}
Console.WriteLine($"Elementos finales: {palabras.Count}");
}
}
Ejemplo práctico: validación de paréntesis
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
string[] expresiones = {
"(a + b) * c",
"((a + b) * (c - d))",
"(a + b * c",
"a + b) * c",
"[{()}]",
"[{(})]"
};
Console.WriteLine("=== Validación de paréntesis ===");
foreach (string expresion in expresiones)
{
bool esValida = ValidarParentesis(expresion);
Console.WriteLine($"{expresion,-20} -> {(esValida ? "Válida" : "Inválida")}");
}
}
static bool ValidarParentesis(string expresion)
{
Stack<char> pila = new Stack<char>();
foreach (char caracter in expresion)
{
// Si es un paréntesis de apertura, lo apilamos
if (caracter == '(' || caracter == '[' || caracter == '{')
{
pila.Push(caracter);
}
// Si es un paréntesis de cierre, verificamos
else if (caracter == ')' || caracter == ']' || caracter == '}')
{
// Si la pila está vacía, hay un cierre sin apertura
if (pila.Count == 0)
return false;
char apertura = pila.Pop();
// Verificar que coincidan los tipos
if ((caracter == ')' && apertura != '(') ||
(caracter == ']' && apertura != '[') ||
(caracter == '}' && apertura != '{'))
{
return false;
}
}
}
// La expresión es válida si la pila está vacía al final
return pila.Count == 0;
}
}
Colas (Queue)
Una cola es una estructura de datos que sigue el principio FIFO (First In, First Out), lo que significa que el primer elemento que se añade es el primero que se retira. Es como una cola de personas: quien llega primero, es atendido primero.
Características de las colas
Característica | Descripción |
---|---|
Principio FIFO | El primer elemento añadido es el primero en salir |
Dos puntos de acceso | Entrada por un extremo, salida por el otro |
Operaciones principales | Enqueue (encolar), Dequeue (desencolar), Peek |
Orden preservado | Mantiene el orden de llegada de los elementos |
Procesamiento secuencial | Ideal para procesar elementos en orden |
Operaciones principales de una cola
Operación | Descripción | Complejidad |
---|---|---|
Enqueue(item) |
Añade un elemento al final de la cola | O(1) |
Dequeue() |
Retira y devuelve el primer elemento | O(1) |
Peek() |
Devuelve el primer elemento sin retirarlo | O(1) |
Count |
Devuelve el número de elementos | O(1) |
Contains(item) |
Verifica si un elemento está en la cola | O(n) |
Implementación básica de colas en C#
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Crear una cola de cadenas
Queue<string> cola = new Queue<string>();
Console.WriteLine("=== Operaciones básicas con colas ===");
// Enqueue: añadir elementos
cola.Enqueue("Primer cliente");
cola.Enqueue("Segundo cliente");
cola.Enqueue("Tercer cliente");
cola.Enqueue("Cuarto cliente");
Console.WriteLine($"Clientes en la cola: {cola.Count}");
// Peek: ver el primer elemento sin retirarlo
Console.WriteLine($"Próximo cliente: {cola.Peek()}");
Console.WriteLine($"Clientes después de Peek: {cola.Count}");
// Dequeue: atender (retirar) clientes
Console.WriteLine($"Atendiendo a: {cola.Dequeue()}");
Console.WriteLine($"Siguiente cliente: {cola.Peek()}");
Console.WriteLine($"Clientes restantes: {cola.Count}");
// Verificar si la cola está vacía
Console.WriteLine($"¿Cola vacía? {cola.Count == 0}");
}
}
Recorrido y manipulación de colas
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Queue<int> numeros = new Queue<int>();
// Añadir números
for (int i = 1; i <= 5; i++)
{
numeros.Enqueue(i * 10);
}
Console.WriteLine("=== Recorrido de la cola ===");
Console.WriteLine("Orden de salida (FIFO):");
// Método 1: Recorrido sin modificar la cola
foreach (int numero in numeros)
{
Console.WriteLine($"- {numero}");
}
Console.WriteLine($"Elementos después del recorrido: {numeros.Count}");
Console.WriteLine("\n=== Procesamiento de la cola ===");
// Método 2: Procesar todos los elementos
while (numeros.Count > 0)
{
int numeroAtendido = numeros.Dequeue();
Console.WriteLine($"Procesando: {numeroAtendido}");
}
Console.WriteLine($"Elementos finales: {numeros.Count}");
}
}
Ejemplo práctico: simulador de atención al cliente
using System;
using System.Collections.Generic;
class Cliente
{
public string Nombre { get; set; }
public string Tipo { get; set; }
public DateTime HoraLlegada { get; set; }
public Cliente(string nombre, string tipo)
{
Nombre = nombre;
Tipo = tipo;
HoraLlegada = DateTime.Now;
}
public override string ToString()
{
return $"{Nombre} ({Tipo}) - {HoraLlegada:HH:mm:ss}";
}
}
class Program
{
static void Main()
{
Queue<Cliente> colaAtencion = new Queue<Cliente>();
Console.WriteLine("=== Simulador de Atención al Cliente ===\n");
// Llegada de clientes
colaAtencion.Enqueue(new Cliente("Ana García", "Consulta"));
colaAtencion.Enqueue(new Cliente("Luis Pérez", "Reclamo"));
colaAtencion.Enqueue(new Cliente("María López", "Consulta"));
colaAtencion.Enqueue(new Cliente("Carlos Ruiz", "Trámite"));
Console.WriteLine("Clientes en espera:");
int posicion = 1;
foreach (Cliente cliente in colaAtencion)
{
Console.WriteLine($"{posicion}. {cliente}");
posicion++;
}
Console.WriteLine($"\nTotal de clientes en cola: {colaAtencion.Count}");
// Simular atención de clientes
Console.WriteLine("\n=== Inicio de atención ===");
int numeroAtencion = 1;
while (colaAtencion.Count > 0)
{
Cliente clienteAtendido = colaAtencion.Dequeue();
TimeSpan tiempoEspera = DateTime.Now - clienteAtendido.HoraLlegada;
Console.WriteLine($"Atención #{numeroAtencion}:");
Console.WriteLine($" Cliente: {clienteAtendido.Nombre}");
Console.WriteLine($" Tipo: {clienteAtendido.Tipo}");
Console.WriteLine($" Tiempo de espera: {tiempoEspera.TotalSeconds:F1} segundos");
Console.WriteLine($" Clientes restantes: {colaAtencion.Count}");
Console.WriteLine();
numeroAtencion++;
// Simular tiempo de atención
System.Threading.Thread.Sleep(1000);
}
Console.WriteLine("Todos los clientes han sido atendidos.");
}
}
Comparación entre pilas y colas
Diferencias principales
Aspecto | Pila (Stack) | Cola (Queue) |
---|---|---|
Principio | LIFO (Last In, First Out) | FIFO (First In, First Out) |
Punto de acceso | Solo desde el tope | Entrada por atrás, salida por delante |
Analogía | Pila de platos | Cola de personas |
Uso típico | Deshacer acciones, recursión | Procesamiento por orden, planificación |
Ejemplo comparativo
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Console.WriteLine("=== Comparación Pila vs Cola ===\n");
// Datos de prueba
string[] elementos = {"A", "B", "C", "D", "E"};
// Usar pila
Stack<string> pila = new Stack<string>();
Console.WriteLine("Añadiendo elementos a la pila:");
foreach (string elemento in elementos)
{
pila.Push(elemento);
Console.WriteLine($"Push: {elemento}");
}
Console.WriteLine("\nRetirando elementos de la pila (LIFO):");
while (pila.Count > 0)
{
Console.WriteLine($"Pop: {pila.Pop()}");
}
Console.WriteLine("\n" + new string('-', 40) + "\n");
// Usar cola
Queue<string> cola = new Queue<string>();
Console.WriteLine("Añadiendo elementos a la cola:");
foreach (string elemento in elementos)
{
cola.Enqueue(elemento);
Console.WriteLine($"Enqueue: {elemento}");
}
Console.WriteLine("\nRetirando elementos de la cola (FIFO):");
while (cola.Count > 0)
{
Console.WriteLine($"Dequeue: {cola.Dequeue()}");
}
}
}
Casos de uso comunes
Cuándo usar pilas:
- Deshacer operaciones (Ctrl+Z en editores)
- Navegación web (botón "Atrás" del navegador)
- Evaluación de expresiones matemáticas
- Manejo de llamadas a funciones (call stack)
- Algoritmos de recorrido en profundidad
Cuándo usar colas:
- Procesamiento de tareas en orden de llegada
- Sistemas de impresión (cola de documentos)
- Algoritmos de recorrido en anchura
- Simulaciones de sistemas de atención
- Buffers de comunicación entre procesos
Implementación personalizada simple
using System;
using System.Collections.Generic;
// Implementación simple de una pila usando List<T>
class MiPila<T>
{
private List<T> elementos = new List<T>();
public void Push(T item)
{
elementos.Add(item);
}
public T Pop()
{
if (elementos.Count == 0)
throw new InvalidOperationException("La pila está vacía");
T item = elementos[elementos.Count - 1];
elementos.RemoveAt(elementos.Count - 1);
return item;
}
public T Peek()
{
if (elementos.Count == 0)
throw new InvalidOperationException("La pila está vacía");
return elementos[elementos.Count - 1];
}
public int Count => elementos.Count;
}
// Implementación simple de una cola usando List<T>
class MiCola<T>
{
private List<T> elementos = new List<T>();
public void Enqueue(T item)
{
elementos.Add(item);
}
public T Dequeue()
{
if (elementos.Count == 0)
throw new InvalidOperationException("La cola está vacía");
T item = elementos[0];
elementos.RemoveAt(0);
return item;
}
public T Peek()
{
if (elementos.Count == 0)
throw new InvalidOperationException("La cola está vacía");
return elementos[0];
}
public int Count => elementos.Count;
}
class Program
{
static void Main()
{
Console.WriteLine("=== Implementaciones personalizadas ===");
// Probar pila personalizada
MiPila<int> miPila = new MiPila<int>();
miPila.Push(1);
miPila.Push(2);
miPila.Push(3);
Console.WriteLine($"Pila - Tope: {miPila.Peek()}");
Console.WriteLine($"Pila - Pop: {miPila.Pop()}");
Console.WriteLine($"Pila - Elementos: {miPila.Count}");
// Probar cola personalizada
MiCola<string> miCola = new MiCola<string>();
miCola.Enqueue("Primero");
miCola.Enqueue("Segundo");
miCola.Enqueue("Tercero");
Console.WriteLine($"Cola - Frente: {miCola.Peek()}");
Console.WriteLine($"Cola - Dequeue: {miCola.Dequeue()}");
Console.WriteLine($"Cola - Elementos: {miCola.Count}");
}
}
Resumen
Las pilas y colas son estructuras de datos fundamentales que proporcionan formas específicas y eficientes de gestionar colecciones de elementos. Las pilas, con su comportamiento LIFO, son perfectas para operaciones que requieren acceso al elemento más reciente, como sistemas de deshacer o manejo de llamadas a funciones. Las colas, con su comportamiento FIFO, son ideales para procesar elementos en el orden en que llegaron, como sistemas de atención o procesamiento de tareas.
Estas estructuras son la base de muchos algoritmos avanzados y sistemas informáticos. Su comprensión te permitirá diseñar soluciones más elegantes y eficientes para problemas que requieren un control específico sobre el orden de procesamiento de datos. En el siguiente tema exploraremos las estructuras definidas por el usuario con struct, que te permitirán crear tipos de datos personalizados para representar información de manera más organizada.