Listas y colecciones dinámicas
Las listas y colecciones dinámicas representan un paso evolutivo fundamental respecto a los arrays tradicionales, proporcionando flexibilidad para manejar conjuntos de datos cuyo tamaño puede cambiar durante la ejecución del programa. Mientras que los arrays tienen un tamaño fijo establecido en el momento de su creación, las colecciones dinámicas pueden crecer y decrecer según las necesidades de la aplicación.
En C#, el namespace System.Collections.Generic
ofrece una amplia variedad de colecciones optimizadas para diferentes escenarios de uso. Entre estas, List<T>
destaca como la colección más utilizada debido a su versatilidad y eficiencia para la mayoría de operaciones comunes. Estas colecciones mantienen la seguridad de tipos gracias a los genéricos, permitiendo especificar exactamente qué tipo de elementos pueden contener.
En este artículo exploraremos las principales colecciones dinámicas disponibles en C#, sus características distintivas, y cuándo utilizar cada una según las necesidades específicas de nuestros programas.
Introducción a las colecciones genéricas
Las colecciones genéricas son estructuras de datos que pueden almacenar elementos de un tipo específico, ofreciendo operaciones optimizadas para agregar, eliminar, buscar y modificar elementos. A diferencia de los arrays, estas colecciones pueden cambiar de tamaño dinámicamente y proporcionan métodos especializados para manipular los datos.
Ventajas de las colecciones dinámicas
Característica | Descripción |
---|---|
Tamaño variable | Pueden crecer y decrecer automáticamente |
Seguridad de tipos | Los genéricos previenen errores de tipo en tiempo de compilación |
Métodos especializados | Incluyen operaciones optimizadas como búsqueda, ordenación y filtrado |
Rendimiento optimizado | Cada colección está optimizada para escenarios específicos |
Flexibilidad | Permiten inserción y eliminación en cualquier posición |
Principales colecciones en System.Collections.Generic
Colección | Características principales | Uso recomendado |
---|---|---|
List<T> |
Acceso por índice, tamaño dinámico | Uso general, acceso secuencial y por índice |
Stack<T> |
LIFO (Last In, First Out) | Operaciones de pila, recursión |
Queue<T> |
FIFO (First In, First Out) | Colas de procesamiento, buffer |
LinkedList<T> |
Lista enlazada, inserción/eliminación eficiente | Inserción/eliminación frecuente en el medio |
HashSet<T> |
Elementos únicos, búsqueda rápida | Conjuntos matemáticos, eliminación de duplicados |
List<T>: La colección más versátil
List<T>
es la colección dinámica más utilizada en C#, proporcionando la funcionalidad de un array redimensionable con métodos adicionales para manipulación de datos.
Declaración e inicialización
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Declaración básica
List<int> numeros = new List<int>();
// Inicialización con valores
List<string> nombres = new List<string> { "Ana", "Luis", "María", "Carlos" };
// Inicialización con capacidad inicial (optimización)
List<double> precios = new List<double>(100);
// Usando var para inferencia de tipos
var ciudades = new List<string> { "Madrid", "Barcelona", "Valencia", "Sevilla" };
Console.WriteLine($"Lista de nombres tiene {nombres.Count} elementos");
Console.WriteLine($"Lista de precios tiene capacidad inicial para {precios.Capacity} elementos");
}
}
Operaciones básicas con List<T>
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> tareas = new List<string>();
// Agregar elementos
tareas.Add("Revisar correo");
tareas.Add("Hacer la compra");
tareas.Add("Llamar al médico");
// Agregar múltiples elementos
tareas.AddRange(new[] { "Estudiar C#", "Hacer ejercicio" });
// Insertar en posición específica
tareas.Insert(1, "Desayunar"); // Insertar en índice 1
Console.WriteLine("Lista de tareas:");
MostrarLista(tareas);
// Acceso por índice
Console.WriteLine($"\nPrimera tarea: {tareas[0]}");
Console.WriteLine($"Última tarea: {tareas[tareas.Count - 1]}");
// Modificar elemento
tareas[2] = "Hacer la compra en el supermercado";
// Buscar elemento
int indice = tareas.IndexOf("Llamar al médico");
if (indice != -1)
{
Console.WriteLine($"'Llamar al médico' está en el índice: {indice}");
}
// Verificar si contiene elemento
bool contieneEstudio = tareas.Contains("Estudiar C#");
Console.WriteLine($"¿Contiene 'Estudiar C#'? {contieneEstudio}");
// Eliminar elementos
tareas.Remove("Hacer ejercicio"); // Elimina la primera ocurrencia
tareas.RemoveAt(0); // Elimina por índice
Console.WriteLine("\nLista después de eliminaciones:");
MostrarLista(tareas);
// Limpiar toda la lista
// tareas.Clear();
}
static void MostrarLista(List<string> lista)
{
for (int i = 0; i < lista.Count; i++)
{
Console.WriteLine($"{i + 1}. {lista[i]}");
}
}
}
Métodos avanzados de List<T>
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> numeros = new List<int> { 64, 34, 25, 12, 22, 11, 90, 5, 77, 30 };
Console.WriteLine("Lista original:");
Console.WriteLine(string.Join(", ", numeros));
// Ordenación
List<int> numerosOrdenados = new List<int>(numeros);
numerosOrdenados.Sort();
Console.WriteLine("\nLista ordenada ascendente:");
Console.WriteLine(string.Join(", ", numerosOrdenados));
// Ordenación descendente
List<int> numerosDesc = new List<int>(numeros);
numerosDesc.Sort((a, b) => b.CompareTo(a));
Console.WriteLine("\nLista ordenada descendente:");
Console.WriteLine(string.Join(", ", numerosDesc));
// Búsqueda binaria (requiere lista ordenada)
int valorBuscado = 25;
int indice = numerosOrdenados.BinarySearch(valorBuscado);
Console.WriteLine($"\nÍndice de {valorBuscado} en lista ordenada: {indice}");
// Filtrado con FindAll
List<int> mayoresQue30 = numeros.FindAll(n => n > 30);
Console.WriteLine($"\nNúmeros mayores que 30: {string.Join(", ", mayoresQue30)}");
// Encontrar primer elemento que cumple condición
int primerMayorQue50 = numeros.Find(n => n > 50);
Console.WriteLine($"Primer número mayor que 50: {primerMayorQue50}");
// Verificar si algún/todos los elementos cumplen condición
bool hayMayoresQue80 = numeros.Exists(n => n > 80);
bool todosMayoresQue0 = numeros.TrueForAll(n => n > 0);
Console.WriteLine($"¿Hay números mayores que 80? {hayMayoresQue80}");
Console.WriteLine($"¿Todos los números son mayores que 0? {todosMayoresQue0}");
// Convertir a array
int[] arrayNumeros = numeros.ToArray();
Console.WriteLine($"\nConvertido a array: {arrayNumeros.GetType().Name}");
}
}
Stack<T>: Colección tipo pila
Stack<T>
implementa una estructura de datos LIFO (Last In, First Out), donde el último elemento añadido es el primero en ser removido.
Operaciones principales
Operación | Método | Descripción |
---|---|---|
Apilar | Push(item) |
Añade elemento al tope de la pila |
Desapilar | Pop() |
Elimina y retorna el elemento del tope |
Observar | Peek() |
Retorna el elemento del tope sin eliminarlo |
Verificar | Count |
Número de elementos en la pila |
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
Stack<string> pilaLibros = new Stack<string>();
// Apilar libros
pilaLibros.Push("Fundamentos de programación");
pilaLibros.Push("Algoritmos y estructuras de datos");
pilaLibros.Push("Programación orientada a objetos");
pilaLibros.Push("Patrones de diseño");
Console.WriteLine($"Libros en la pila: {pilaLibros.Count}");
// Ver el libro del tope sin quitarlo
string libroSuperior = pilaLibros.Peek();
Console.WriteLine($"Libro en el tope: {libroSuperior}");
// Procesar todos los libros (LIFO)
Console.WriteLine("\nLeyendo libros en orden LIFO:");
while (pilaLibros.Count > 0)
{
string libro = pilaLibros.Pop();
Console.WriteLine($"Leyendo: {libro}");
}
Console.WriteLine($"\nLibros restantes en la pila: {pilaLibros.Count}");
// Ejemplo práctico: validador de paréntesis
ValidarParentesis("((()))"); // Válido
ValidarParentesis("(()"); // Inválido
ValidarParentesis("())"); // Inválido
}
static void ValidarParentesis(string expresion)
{
Stack<char> pila = new Stack<char>();
bool esValida = true;
foreach (char caracter in expresion)
{
if (caracter == '(')
{
pila.Push(caracter);
}
else if (caracter == ')')
{
if (pila.Count == 0)
{
esValida = false;
break;
}
pila.Pop();
}
}
// Debe estar vacía al final para ser válida
esValida = esValida && (pila.Count == 0);
Console.WriteLine($"'{expresion}' es {(esValida ? "válida" : "inválida")}");
}
}
Queue<T>: Colección tipo cola
Queue<T>
implementa una estructura FIFO (First In, First Out), donde el primer elemento añadido es el primero en ser removido.
Operaciones principales
Operación | Método | Descripción |
---|---|---|
Encolar | Enqueue(item) |
Añade elemento al final de la cola |
Desencolar | Dequeue() |
Elimina y retorna el primer elemento |
Observar | Peek() |
Retorna el primer elemento sin eliminarlo |
Verificar | Count |
Número de elementos en la cola |
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Simulador de cola de atención al cliente
Queue<string> colaAtencion = new Queue<string>();
// Llegan clientes
colaAtencion.Enqueue("Cliente 1 - Ana García");
colaAtencion.Enqueue("Cliente 2 - Luis Martín");
colaAtencion.Enqueue("Cliente 3 - María López");
colaAtencion.Enqueue("Cliente 4 - Carlos Ruiz");
Console.WriteLine($"Clientes en cola: {colaAtencion.Count}");
// Ver quién es el siguiente sin atenderlo
string siguienteCliente = colaAtencion.Peek();
Console.WriteLine($"Siguiente en la cola: {siguienteCliente}");
// Atender clientes en orden FIFO
Console.WriteLine("\nAtendiendo clientes:");
while (colaAtencion.Count > 0)
{
string cliente = colaAtencion.Dequeue();
Console.WriteLine($"Atendiendo a: {cliente}");
// Simular tiempo de atención
System.Threading.Thread.Sleep(1000);
}
Console.WriteLine($"\nClientes restantes en cola: {colaAtencion.Count}");
// Ejemplo práctico: sistema de impresión
SistemaImpresion();
}
static void SistemaImpresion()
{
Console.WriteLine("\n--- Sistema de Cola de Impresión ---");
Queue<string> colaImpresion = new Queue<string>();
// Agregar trabajos de impresión
colaImpresion.Enqueue("Documento1.pdf - 5 páginas");
colaImpresion.Enqueue("Presentación.pptx - 20 páginas");
colaImpresion.Enqueue("Informe.docx - 10 páginas");
colaImpresion.Enqueue("Factura.pdf - 2 páginas");
Console.WriteLine($"Trabajos en cola de impresión: {colaImpresion.Count}");
// Procesar trabajos de impresión
int numeroTrabajo = 1;
while (colaImpresion.Count > 0)
{
string trabajo = colaImpresion.Dequeue();
Console.WriteLine($"Imprimiendo trabajo {numeroTrabajo}: {trabajo}");
numeroTrabajo++;
}
Console.WriteLine("Todos los trabajos han sido procesados.");
}
}
LinkedList<T>: Lista enlazada
LinkedList<T>
es una lista doblemente enlazada que permite inserción y eliminación eficiente en cualquier posición, especialmente útil cuando se realizan muchas operaciones en el medio de la colección.
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
LinkedList<string> playlist = new LinkedList<string>();
// Agregar canciones
playlist.AddLast("Canción 1 - Intro");
playlist.AddLast("Canción 2 - Rock clásico");
playlist.AddLast("Canción 3 - Balada");
playlist.AddLast("Canción 4 - Final épico");
// Agregar al principio
playlist.AddFirst("Canción 0 - Preludio");
Console.WriteLine("Playlist actual:");
MostrarPlaylist(playlist);
// Encontrar un nodo específico y agregar después
LinkedListNode<string> nodoRock = playlist.Find("Canción 2 - Rock clásico");
if (nodoRock != null)
{
playlist.AddAfter(nodoRock, "Canción 2.5 - Solo de guitarra");
}
// Agregar antes de un nodo
LinkedListNode<string> nodoFinal = playlist.Find("Canción 4 - Final épico");
if (nodoFinal != null)
{
playlist.AddBefore(nodoFinal, "Canción 3.5 - Interludio");
}
Console.WriteLine("\nPlaylist después de inserciones:");
MostrarPlaylist(playlist);
// Eliminar canciones específicas
playlist.Remove("Canción 2.5 - Solo de guitarra");
playlist.RemoveFirst(); // Eliminar primera
playlist.RemoveLast(); // Eliminar última
Console.WriteLine("\nPlaylist final:");
MostrarPlaylist(playlist);
// Navegación bidireccional
Console.WriteLine("\nNavegación hacia adelante y atrás:");
LinkedListNode<string> nodoActual = playlist.First;
while (nodoActual != null)
{
Console.WriteLine($"Reproduciendo: {nodoActual.Value}");
nodoActual = nodoActual.Next;
}
Console.WriteLine("\nReproducción en reversa:");
nodoActual = playlist.Last;
while (nodoActual != null)
{
Console.WriteLine($"Reproduciendo: {nodoActual.Value}");
nodoActual = nodoActual.Previous;
}
}
static void MostrarPlaylist(LinkedList<string> playlist)
{
int numero = 1;
foreach (string cancion in playlist)
{
Console.WriteLine($"{numero}. {cancion}");
numero++;
}
Console.WriteLine($"Total de canciones: {playlist.Count}");
}
}
HashSet<T>: Conjunto de elementos únicos
HashSet<T>
mantiene una colección de elementos únicos con operaciones de búsqueda muy eficientes (O(1) en promedio).
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Crear conjunto de números únicos
HashSet<int> numerosUnicos = new HashSet<int>();
// Agregar elementos (duplicados se ignoran)
numerosUnicos.Add(1);
numerosUnicos.Add(2);
numerosUnicos.Add(3);
numerosUnicos.Add(2); // Este se ignora
numerosUnicos.Add(4);
numerosUnicos.Add(1); // Este se ignora
Console.WriteLine($"Números únicos: {string.Join(", ", numerosUnicos)}");
Console.WriteLine($"Cantidad de elementos: {numerosUnicos.Count}");
// Verificar existencia (muy rápido)
bool contiene3 = numerosUnicos.Contains(3);
Console.WriteLine($"¿Contiene el número 3? {contiene3}");
// Eliminar duplicados de una lista
List<string> listaConDuplicados = new List<string>
{
"manzana", "banana", "manzana", "naranja", "banana", "uva", "manzana"
};
Console.WriteLine($"\nLista original: {string.Join(", ", listaConDuplicados)}");
HashSet<string> frutasUnicas = new HashSet<string>(listaConDuplicados);
Console.WriteLine($"Frutas únicas: {string.Join(", ", frutasUnicas)}");
// Operaciones de conjuntos matemáticos
OperacionesConjuntos();
}
static void OperacionesConjuntos()
{
Console.WriteLine("\n--- Operaciones de Conjuntos ---");
HashSet<int> conjunto1 = new HashSet<int> { 1, 2, 3, 4, 5 };
HashSet<int> conjunto2 = new HashSet<int> { 4, 5, 6, 7, 8 };
Console.WriteLine($"Conjunto 1: {string.Join(", ", conjunto1)}");
Console.WriteLine($"Conjunto 2: {string.Join(", ", conjunto2)}");
// Unión
HashSet<int> union = new HashSet<int>(conjunto1);
union.UnionWith(conjunto2);
Console.WriteLine($"Unión: {string.Join(", ", union)}");
// Intersección
HashSet<int> interseccion = new HashSet<int>(conjunto1);
interseccion.IntersectWith(conjunto2);
Console.WriteLine($"Intersección: {string.Join(", ", interseccion)}");
// Diferencia
HashSet<int> diferencia = new HashSet<int>(conjunto1);
diferencia.ExceptWith(conjunto2);
Console.WriteLine($"Diferencia (1 - 2): {string.Join(", ", diferencia)}");
// Diferencia simétrica
HashSet<int> diferenciaSimetrica = new HashSet<int>(conjunto1);
diferenciaSimetrica.SymmetricExceptWith(conjunto2);
Console.WriteLine($"Diferencia simétrica: {string.Join(", ", diferenciaSimetrica)}");
// Verificaciones de relaciones entre conjuntos
bool esSubconjunto = conjunto1.IsSubsetOf(union);
bool esSuperconjunto = union.IsSupersetOf(conjunto1);
bool sonDisjuntos = conjunto1.Overlaps(conjunto2);
Console.WriteLine($"¿Conjunto1 es subconjunto de la unión? {esSubconjunto}");
Console.WriteLine($"¿La unión es superconjunto de conjunto1? {esSuperconjunto}");
Console.WriteLine($"¿Los conjuntos se superponen? {sonDisjuntos}");
}
}
Comparación de rendimiento y cuándo usar cada colección
Tabla comparativa de operaciones
Operación | List<T> | Stack<T> | Queue<T> | LinkedList<T> | HashSet<T> |
---|---|---|---|---|---|
Acceso por índice | O(1) | N/A | N/A | O(n) | N/A |
Búsqueda | O(n) | O(n) | O(n) | O(n) | O(1) promedio |
Inserción al final | O(1)* | O(1) | O(1) | O(1) | O(1) promedio |
Inserción al principio | O(n) | N/A | N/A | O(1) | O(1) promedio |
Inserción en el medio | O(n) | N/A | N/A | O(1)** | N/A |
Eliminación al final | O(1) | O(1) | N/A | O(1) | O(1) promedio |
Eliminación al principio | O(n) | N/A | O(1) | O(1) | O(1) promedio |
*O(1) amortizado (puede ser O(n) cuando se redimensiona) **O(1) si ya tienes la referencia al nodo
Guía de selección
using System;
using System.Collections.Generic;
class GuiaSeleccion
{
static void Main()
{
Console.WriteLine("=== Guía de Selección de Colecciones ===\n");
EjemploUsoList();
EjemploUsoStack();
EjemploUsoQueue();
EjemploUsoLinkedList();
EjemploUsoHashSet();
}
static void EjemploUsoList()
{
Console.WriteLine("--- Usar List<T> cuando: ---");
Console.WriteLine("• Necesites acceso por índice frecuente");
Console.WriteLine("• Realices muchas operaciones al final de la colección");
Console.WriteLine("• Requieras funcionalidad general (uso más común)");
Console.WriteLine("• Necesites ordenar los elementos");
// Ejemplo: gestión de puntuaciones de juego
List<int> puntuaciones = new List<int> { 1500, 2300, 1800, 2500 };
puntuaciones.Sort();
Console.WriteLine($"Puntuaciones ordenadas: {string.Join(", ", puntuaciones)}\n");
}
static void EjemploUsoStack()
{
Console.WriteLine("--- Usar Stack<T> cuando: ---");
Console.WriteLine("• Necesites comportamiento LIFO");
Console.WriteLine("• Implementes funcionalidad de deshacer/rehacer");
Console.WriteLine("• Manejes llamadas recursivas o expresiones anidadas");
// Ejemplo: historial de navegación
Stack<string> historial = new Stack<string>();
historial.Push("Página inicio");
historial.Push("Página productos");
historial.Push("Página detalles");
Console.WriteLine($"Volver a: {historial.Pop()}\n");
}
static void EjemploUsoQueue()
{
Console.WriteLine("--- Usar Queue<T> cuando: ---");
Console.WriteLine("• Necesites comportamiento FIFO");
Console.WriteLine("• Implementes sistemas de procesamiento por orden de llegada");
Console.WriteLine("• Manejes buffers de datos");
// Ejemplo: cola de tareas
Queue<string> tareas = new Queue<string>();
tareas.Enqueue("Procesar pedido 1");
tareas.Enqueue("Procesar pedido 2");
Console.WriteLine($"Procesando: {tareas.Dequeue()}\n");
}
static void EjemploUsoLinkedList()
{
Console.WriteLine("--- Usar LinkedList<T> cuando: ---");
Console.WriteLine("• Realices muchas inserciones/eliminaciones en el medio");
Console.WriteLine("• No necesites acceso por índice");
Console.WriteLine("• Implementes algoritmos que requieren navegación bidireccional");
// Ejemplo: editor de texto con cursor
LinkedList<char> texto = new LinkedList<char>();
texto.AddLast('H');
texto.AddLast('o');
texto.AddLast('l');
texto.AddLast('a');
Console.WriteLine($"Texto: {string.Join("", texto)}\n");
}
static void EjemploUsoHashSet()
{
Console.WriteLine("--- Usar HashSet<T> cuando: ---");
Console.WriteLine("• Necesites elementos únicos automáticamente");
Console.WriteLine("• Realices búsquedas muy frecuentes");
Console.WriteLine("• Implementes operaciones matemáticas de conjuntos");
// Ejemplo: tags únicos
HashSet<string> tags = new HashSet<string> { "programación", "C#", "tutorial", "programación" };
Console.WriteLine($"Tags únicos: {string.Join(", ", tags)}\n");
}
}
Resumen
Las listas y colecciones dinámicas en C# ofrecen flexibilidad y eficiencia para manejar conjuntos de datos que pueden cambiar durante la ejecución del programa. Cada tipo de colección está optimizada para escenarios específicos: List<T>
para uso general con acceso por índice, Stack<T>
para comportamiento LIFO, Queue<T>
para procesamiento FIFO, LinkedList<T>
para inserción eficiente en cualquier posición, y HashSet<T>
para mantener elementos únicos con búsqueda rápida.
La elección de la colección adecuada depende principalmente de las operaciones más frecuentes que realizaremos: acceso aleatorio, inserción/eliminación en posiciones específicas, mantenimiento de orden, o búsqueda de elementos. En el próximo artículo exploraremos los diccionarios y conjuntos, estructuras que nos permitirán organizar datos mediante relaciones clave-valor y realizar operaciones matemáticas avanzadas con conjuntos.