Ir al contenido principal

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.