Rust

Rust

Tutorial Rust: Colecciones estándar

Aprende a usar colecciones estándar en Rust como Vec, HashMap y HashSet para gestionar datos dinámicos y eficientes.

Aprende Rust y certifícate

Vec<T>

El vector es una de las estructuras de datos más utilizadas en Rust. Un Vec<T> es una colección dinámica que almacena elementos del mismo tipo T de forma contigua en memoria, similar a los arrays pero con la capacidad de crecer o reducir su tamaño durante la ejecución del programa.

A diferencia de los arrays que tienen un tamaño fijo definido en tiempo de compilación, los vectores permiten añadir o eliminar elementos según sea necesario. Esta flexibilidad los convierte en una herramienta fundamental para cuando no conocemos de antemano cuántos elementos necesitaremos almacenar.

Creación de vectores

Existen varias formas de crear un nuevo vector:

// Vector vacío
let mut numeros: Vec<i32> = Vec::new();

// Vector con capacidad inicial reservada
let mut nombres: Vec<String> = Vec::with_capacity(10);

// Vector con valores iniciales usando macro vec!
let colores = vec!["rojo", "verde", "azul"];

El macro vec! proporciona una sintaxis concisa para crear vectores con valores iniciales. Rust inferirá automáticamente el tipo de los elementos basándose en los valores proporcionados.

Añadir elementos

Para añadir elementos a un vector, necesitamos declararlo como mutable usando mut:

let mut frutas = Vec::new();

// Añadir elementos al final del vector
frutas.push("manzana");
frutas.push("naranja");
frutas.push("plátano");

println!("Tenemos {} frutas", frutas.len()); // Imprime: Tenemos 3 frutas

También podemos añadir múltiples elementos de una vez:

let mut numeros = vec![1, 2];
numeros.extend([3, 4, 5].iter());
println!("{:?}", numeros); // Imprime: [1, 2, 3, 4, 5]

Acceso a elementos

Podemos acceder a los elementos de un vector de dos formas principales:

  • Mediante indexación con corchetes []
  • Usando el método get
let vocales = vec!['a', 'e', 'i', 'o', 'u'];

// Acceso por índice (puede causar pánico si el índice está fuera de rango)
let primera = vocales[0]; // 'a'

// Acceso seguro con get (devuelve Option<&T>)
match vocales.get(1) {
    Some(valor) => println!("El segundo elemento es: {}", valor),
    None => println!("No existe ese elemento"),
}

// También podemos usar if let para un código más conciso
if let Some(valor) = vocales.get(10) {
    println!("Elemento encontrado: {}", valor);
} else {
    println!("Índice fuera de rango");
}

La diferencia clave entre estos métodos es que la indexación directa causará que el programa termine abruptamente (panic) si intentamos acceder a un índice fuera de rango, mientras que el método get devuelve un Option<&T> que podemos manejar de forma segura.

Eliminar elementos

Rust ofrece varios métodos para eliminar elementos de un vector:

let mut numeros = vec![10, 20, 30, 40, 50];

// Eliminar y devolver el último elemento
let ultimo = numeros.pop(); // Some(50)
println!("Último elemento: {:?}", ultimo);
println!("Vector después de pop: {:?}", numeros); // [10, 20, 30, 40]

// Eliminar elemento en una posición específica
let elemento_eliminado = numeros.remove(1); // 20
println!("Elemento eliminado: {}", elemento_eliminado);
println!("Vector después de remove: {:?}", numeros); // [10, 30, 40]

El método drain_filter permite eliminar elementos que cumplen cierta condición:

let mut numeros = vec![1, 2, 3, 4, 5, 6];

// Eliminar números pares
let pares: Vec<_> = numeros.drain_filter(|x| x % 2 == 0).collect();

println!("Números pares eliminados: {:?}", pares); // [2, 4, 6]
println!("Vector resultante: {:?}", numeros); // [1, 3, 5]

Iteración sobre vectores

Podemos recorrer los elementos de un vector de varias formas:

let animales = vec!["perro", "gato", "conejo"];

// Iteración básica por referencia
for animal in &animales {
    println!("{}", animal);
}

// Iteración con índices
for (indice, animal) in animales.iter().enumerate() {
    println!("Animal {}: {}", indice, animal);
}

// Iteración con mutabilidad
let mut puntuaciones = vec![85, 92, 78];
for puntuacion in &mut puntuaciones {
    *puntuacion += 5; // Añadir 5 puntos a cada puntuación
}
println!("Puntuaciones ajustadas: {:?}", puntuaciones); // [90, 97, 83]

Métodos útiles

Los vectores en Rust incluyen numerosos métodos que facilitan operaciones comunes:

let mut valores = vec![3, 1, 4, 1, 5, 9, 2, 6];

// Ordenar el vector
valores.sort();
println!("Ordenado: {:?}", valores); // [1, 1, 2, 3, 4, 5, 6, 9]

// Eliminar duplicados (primero debe estar ordenado)
valores.dedup();
println!("Sin duplicados: {:?}", valores); // [1, 2, 3, 4, 5, 6, 9]

// Verificar si contiene un valor
let contiene_cinco = valores.contains(&5);
println!("¿Contiene el 5? {}", contiene_cinco); // true

// Encontrar la posición de un elemento
if let Some(pos) = valores.iter().position(|&x| x == 3) {
    println!("El 3 está en la posición {}", pos);
}

// Dividir un vector
let mut numeros = vec![1, 2, 3, 4, 5];
let parte = numeros.split_off(3); // Divide desde el índice 3
println!("Primera parte: {:?}", numeros); // [1, 2, 3]
println!("Segunda parte: {:?}", parte); // [4, 5]

Capacidad y rendimiento

Los vectores gestionan automáticamente su memoria, pero podemos optimizar su rendimiento:

// Crear un vector con capacidad inicial
let mut datos = Vec::with_capacity(1000);

// Añadir elementos no causará realocaciones hasta superar la capacidad
for i in 0..100 {
    datos.push(i);
}

println!("Longitud: {}", datos.len()); // 100
println!("Capacidad: {}", datos.capacity()); // 1000

// Reducir la capacidad para ajustarla al tamaño actual
datos.shrink_to_fit();
println!("Capacidad después de shrink_to_fit: {}", datos.capacity()); // 100

La capacidad es el espacio reservado en memoria, mientras que la longitud es el número de elementos. Cuando añadimos elementos y superamos la capacidad, Rust automáticamente reserva más memoria (típicamente duplicando la capacidad actual).

Conversiones con arrays y slices

Los vectores se pueden convertir fácilmente desde y hacia arrays y slices:

// De array a vector
let array = [1, 2, 3, 4];
let vector: Vec<i32> = array.to_vec();
// O alternativamente:
let vector = Vec::from(array);

// De slice a vector
let slice = &[5, 6, 7];
let vector = slice.to_vec();

// De vector a slice
let vector = vec![8, 9, 10];
let slice = &vector[..]; // Slice que abarca todo el vector
let slice_parcial = &vector[1..]; // Slice desde el índice 1 hasta el final

Es importante recordar que insertar elementos en colecciones puede transferir su propiedad, algo que estudiaremos en el módulo de ownership.

HashMap y BTreeMap

Los mapas son estructuras de datos que almacenan pares clave-valor, permitiendo recuperar valores rápidamente a partir de sus claves. En Rust, las implementaciones más comunes son HashMap<K, V> y BTreeMap<K, V>, donde K representa el tipo de la clave y V el tipo del valor.

Estas colecciones son fundamentales cuando necesitamos asociar datos relacionados y acceder a ellos de forma eficiente. Por ejemplo, podríamos usar un mapa para almacenar puntuaciones de jugadores, donde el nombre del jugador es la clave y su puntuación el valor.

HashMap: mapas basados en hash

HashMap<K, V> implementa un mapa utilizando una tabla hash interna. Esta estructura ofrece operaciones de búsqueda, inserción y eliminación con un rendimiento promedio de O(1) (tiempo constante), lo que la hace extremadamente eficiente para la mayoría de los casos de uso.

Para usar HashMap, primero debemos importarlo:

use std::collections::HashMap;

// Crear un HashMap vacío que asocia cadenas con enteros
let mut puntuaciones: HashMap<String, i32> = HashMap::new();

Inserción de elementos

Podemos añadir pares clave-valor usando el método insert:

let mut inventario = HashMap::new();

// Añadir elementos (producto -> cantidad)
inventario.insert("manzanas", 5);
inventario.insert("plátanos", 8);
inventario.insert("naranjas", 12);

// Sobrescribir un valor existente
inventario.insert("manzanas", 7); // Ahora tenemos 7 manzanas, no 5

También podemos insertar un valor solo si la clave no existe:

// Insertar solo si la clave no existe
inventario.entry("peras").or_insert(3);
// Si la clave ya existe, podemos modificar su valor
*inventario.entry("plátanos").or_insert(0) += 2; // Añade 2 plátanos más

Acceso a valores

Hay varias formas de acceder a los valores de un HashMap:

let mut capitales = HashMap::new();
capitales.insert("España", "Madrid");
capitales.insert("Francia", "París");
capitales.insert("Italia", "Roma");

// Acceso seguro con get (devuelve Option<&V>)
match capitales.get("España") {
    Some(capital) => println!("La capital de España es {}", capital),
    None => println!("No conozco la capital de ese país"),
}

// Acceso con valor por defecto
let capital_portugal = capitales.get("Portugal").unwrap_or(&"Desconocida");
println!("La capital de Portugal es {}", capital_portugal);

// Verificar si existe una clave
if capitales.contains_key("Italia") {
    println!("Tenemos información sobre Italia");
}

Eliminación de elementos

Podemos eliminar entradas de un HashMap de varias maneras:

let mut edades = HashMap::new();
edades.insert("Ana", 28);
edades.insert("Carlos", 32);
edades.insert("Elena", 25);

// Eliminar una entrada específica
let edad_eliminada = edades.remove("Carlos");
println!("Edad eliminada: {:?}", edad_eliminada); // Some(32)

// Eliminar entradas que cumplan cierta condición
let mayores_eliminados: HashMap<_, _> = edades
    .drain_filter(|_, edad| *edad > 26)
    .collect();

println!("Personas mayores de 26 eliminadas: {:?}", mayores_eliminados);
println!("Edades restantes: {:?}", edades);

Iteración sobre un HashMap

Podemos recorrer todas las entradas, claves o valores:

let mut población = HashMap::new();
población.insert("Madrid", 3_223_000);
población.insert("Barcelona", 1_620_000);
población.insert("Valencia", 791_000);

// Iterar sobre pares clave-valor
for (ciudad, habitantes) in &población {
    println!("{} tiene {} habitantes", ciudad, habitantes);
}

// Iterar solo sobre las claves
for ciudad in población.keys() {
    println!("Ciudad: {}", ciudad);
}

// Iterar solo sobre los valores
let total: i32 = población.values().sum();
println!("Población total: {}", total);

BTreeMap: mapas ordenados

BTreeMap<K, V> implementa un mapa utilizando un árbol B como estructura interna. A diferencia de HashMap, mantiene las claves ordenadas según su orden natural, lo que permite operaciones adicionales como obtener rangos de claves.

Las operaciones en un BTreeMap tienen un rendimiento de O(log n), algo más lento que HashMap pero aún muy eficiente.

use std::collections::BTreeMap;

let mut calificaciones = BTreeMap::new();
calificaciones.insert("Matemáticas", 85);
calificaciones.insert("Historia", 92);
calificaciones.insert("Ciencias", 78);

La principal ventaja de BTreeMap es que las claves siempre están ordenadas:

// Las claves se imprimen en orden alfabético: Ciencias, Historia, Matemáticas
for (asignatura, nota) in &calificaciones {
    println!("{}: {}", asignatura, nota);
}

Operaciones específicas de BTreeMap

BTreeMap ofrece métodos para trabajar con rangos ordenados:

let mut ranking = BTreeMap::new();
ranking.insert(1, "Alicia");
ranking.insert(5, "Carlos");
ranking.insert(2, "Berta");
ranking.insert(4, "David");
ranking.insert(3, "Elena");

// Obtener un rango de entradas
let top3 = ranking.range(1..=3);
println!("Top 3 jugadores:");
for (posición, nombre) in top3 {
    println!("{}. {}", posición, nombre);
}

// Obtener la primera y última entrada
if let Some((primera_pos, primer_nombre)) = ranking.first_key_value() {
    println!("Primer lugar: {}. {}", primera_pos, primer_nombre);
}

if let Some((última_pos, último_nombre)) = ranking.last_key_value() {
    println!("Último lugar: {}. {}", última_pos, último_nombre);
}

Comparación entre HashMap y BTreeMap

Elegir entre HashMap y BTreeMap depende de tus necesidades específicas:

  • HashMap:

  • Ventajas: Operaciones más rápidas en promedio (O(1))

  • Desventajas: No mantiene las claves ordenadas

  • Uso ideal: Cuando necesitas máxima velocidad y no te importa el orden

  • BTreeMap:

  • Ventajas: Claves siempre ordenadas, permite operaciones de rango

  • Desventajas: Operaciones ligeramente más lentas (O(log n))

  • Uso ideal: Cuando necesitas mantener las claves ordenadas o realizar operaciones por rangos

Casos de uso prácticos

Contador de frecuencias con HashMap

let texto = "rust es un lenguaje de programación rust es seguro";
let mut frecuencias = HashMap::new();

for palabra in texto.split_whitespace() {
    *frecuencias.entry(palabra).or_insert(0) += 1;
}

println!("Frecuencia de palabras:");
for (palabra, frecuencia) in &frecuencias {
    println!("{}: {}", palabra, frecuencia);
}

Agenda telefónica ordenada con BTreeMap

let mut agenda = BTreeMap::new();
agenda.insert("Ana García", "612345678");
agenda.insert("Carlos López", "623456789");
agenda.insert("Beatriz Martín", "634567890");

println!("Contactos (ordenados alfabéticamente):");
for (nombre, teléfono) in &agenda {
    println!("{}: {}", nombre, teléfono);
}

Combinando ambos tipos de mapas

A veces, podemos necesitar convertir entre ambos tipos:

use std::collections::{HashMap, BTreeMap};

// Crear un HashMap
let mut datos_desordenados = HashMap::new();
datos_desordenados.insert("c", 3);
datos_desordenados.insert("a", 1);
datos_desordenados.insert("b", 2);

// Convertir a BTreeMap para obtener los datos ordenados
let datos_ordenados: BTreeMap<_, _> = datos_desordenados.into_iter().collect();

println!("Datos ordenados:");
for (clave, valor) in &datos_ordenados {
    println!("{}: {}", clave, valor);
}

Es importante recordar que insertar elementos en colecciones puede transferir su propiedad, algo que estudiaremos en el módulo de ownership.

HashSet y BTreeSet

Los conjuntos (sets) son colecciones que almacenan elementos únicos sin un orden específico. En Rust, disponemos de dos implementaciones principales: HashSet<T> y BTreeSet<T>, donde T representa el tipo de los elementos almacenados.

A diferencia de los vectores que permiten elementos duplicados, los conjuntos garantizan que cada elemento aparezca una sola vez. Esta característica los hace ideales para operaciones como eliminar duplicados, comprobar pertenencia o realizar operaciones matemáticas de conjuntos.

HashSet: conjuntos basados en hash

HashSet<T> implementa un conjunto utilizando una tabla hash interna (similar a HashMap), lo que proporciona operaciones de búsqueda, inserción y eliminación con un rendimiento promedio de O(1).

Para utilizar HashSet, primero debemos importarlo:

use std::collections::HashSet;

// Crear un HashSet vacío de enteros
let mut numeros: HashSet<i32> = HashSet::new();

Creación e inserción de elementos

Podemos crear conjuntos de varias formas:

// Conjunto vacío
let mut frutas = HashSet::new();

// Añadir elementos individuales
frutas.insert("manzana");
frutas.insert("naranja");
frutas.insert("plátano");

// Intentar insertar un elemento duplicado
let insertado = frutas.insert("manzana");
println!("¿Se insertó el duplicado? {}", insertado); // false, ya existía

// Crear un conjunto a partir de un iterador
let vocales: HashSet<char> = "aeiou".chars().collect();
println!("Vocales: {:?}", vocales);

Verificación de elementos

Una de las operaciones más comunes con conjuntos es comprobar si contienen cierto elemento:

let colores = HashSet::from(["rojo", "verde", "azul"]);

// Verificar si un elemento existe
if colores.contains("verde") {
    println!("El verde está en la lista de colores");
}

// Comprobar si está vacío
if !colores.is_empty() {
    println!("Tenemos {} colores", colores.len());
}

Eliminación de elementos

Podemos eliminar elementos de un HashSet de varias maneras:

let mut lenguajes = HashSet::new();
lenguajes.insert("Rust");
lenguajes.insert("Python");
lenguajes.insert("JavaScript");
lenguajes.insert("C++");

// Eliminar un elemento específico
let eliminado = lenguajes.remove("Python");
println!("¿Se eliminó Python? {}", eliminado); // true

// Eliminar elementos que cumplan cierta condición
let eliminados: HashSet<_> = lenguajes
    .drain_filter(|lang| lang.len() > 4)
    .collect();

println!("Lenguajes con más de 4 letras: {:?}", eliminados);
println!("Lenguajes restantes: {:?}", lenguajes);

Operaciones matemáticas de conjuntos

Los conjuntos en Rust permiten realizar operaciones matemáticas como unión, intersección y diferencia:

let grupo_a: HashSet<_> = [1, 2, 3, 4].iter().cloned().collect();
let grupo_b: HashSet<_> = [3, 4, 5, 6].iter().cloned().collect();

// Unión: elementos que están en A o en B (o en ambos)
let union: HashSet<_> = grupo_a.union(&grupo_b).cloned().collect();
println!("Unión: {:?}", union); // {1, 2, 3, 4, 5, 6}

// Intersección: elementos que están tanto en A como en B
let interseccion: HashSet<_> = grupo_a.intersection(&grupo_b).cloned().collect();
println!("Intersección: {:?}", interseccion); // {3, 4}

// Diferencia: elementos en A pero no en B
let diferencia: HashSet<_> = grupo_a.difference(&grupo_b).cloned().collect();
println!("Diferencia A-B: {:?}", diferencia); // {1, 2}

// Diferencia simétrica: elementos en A o B, pero no en ambos
let dif_simetrica: HashSet<_> = grupo_a.symmetric_difference(&grupo_b).cloned().collect();
println!("Diferencia simétrica: {:?}", dif_simetrica); // {1, 2, 5, 6}

Verificación de subconjuntos y superconjuntos

Podemos comprobar relaciones entre conjuntos:

let numeros = HashSet::from([1, 2, 3, 4, 5]);
let pares = HashSet::from([2, 4]);
let impares = HashSet::from([1, 3, 5]);
let primos = HashSet::from([2, 3, 5, 7]);

// Verificar si un conjunto es subconjunto de otro
println!("¿Pares es subconjunto de números? {}", pares.is_subset(&numeros)); // true

// Verificar si un conjunto es superconjunto de otro
println!("¿Números es superconjunto de impares? {}", numeros.is_superset(&impares)); // true

// Verificar si dos conjuntos son disjuntos (no tienen elementos en común)
println!("¿Pares e impares son disjuntos? {}", pares.is_disjoint(&impares)); // true
println!("¿Impares y primos son disjuntos? {}", impares.is_disjoint(&primos)); // false

BTreeSet: conjuntos ordenados

BTreeSet<T> implementa un conjunto utilizando un árbol B como estructura interna. A diferencia de HashSet, mantiene los elementos ordenados según su orden natural, lo que permite operaciones adicionales como obtener rangos de elementos.

use std::collections::BTreeSet;

let mut calificaciones = BTreeSet::new();
calificaciones.insert(85);
calificaciones.insert(92);
calificaciones.insert(78);
calificaciones.insert(95);

La principal ventaja de BTreeSet es que los elementos siempre están ordenados:

// Los elementos se imprimen en orden ascendente: 78, 85, 92, 95
println!("Calificaciones ordenadas:");
for nota in &calificaciones {
    println!("{}", nota);
}

// Obtener la calificación más baja y más alta
if let Some(minima) = calificaciones.first() {
    println!("Calificación más baja: {}", minima);
}

if let Some(maxima) = calificaciones.last() {
    println!("Calificación más alta: {}", maxima);
}

Operaciones específicas de BTreeSet

BTreeSet ofrece métodos para trabajar con rangos ordenados:

let numeros = BTreeSet::from([10, 20, 30, 40, 50, 60, 70]);

// Obtener un rango de elementos
println!("Números entre 25 y 55:");
for n in numeros.range(25..=55) {
    println!("{}", n); // Imprime 30, 40, 50
}

// Obtener elementos mayores o iguales a un valor
println!("Números mayores o iguales a 50:");
for n in numeros.range(50..) {
    println!("{}", n); // Imprime 50, 60, 70
}

// Obtener elementos menores que un valor
println!("Números menores que 40:");
for n in numeros.range(..40) {
    println!("{}", n); // Imprime 10, 20, 30
}

Operaciones de división (split)

BTreeSet permite dividir un conjunto en dos partes:

let mut letras = BTreeSet::from(['a', 'b', 'c', 'd', 'e', 'f']);

// Dividir el conjunto en dos partes: <= 'c' y > 'c'
let segunda_mitad = letras.split_off(&'c');

println!("Primera mitad: {:?}", letras); // {'a', 'b', 'c'}
println!("Segunda mitad: {:?}", segunda_mitad); // {'d', 'e', 'f'}

Comparación entre HashSet y BTreeSet

Elegir entre HashSet y BTreeSet depende de tus necesidades específicas:

  • HashSet:

  • Ventajas: Operaciones más rápidas en promedio (O(1))

  • Desventajas: No mantiene los elementos ordenados

  • Uso ideal: Cuando necesitas máxima velocidad y no te importa el orden

  • BTreeSet:

  • Ventajas: Elementos siempre ordenados, permite operaciones de rango

  • Desventajas: Operaciones ligeramente más lentas (O(log n))

  • Uso ideal: Cuando necesitas mantener los elementos ordenados o realizar operaciones por rangos

Casos de uso prácticos

Eliminación de duplicados con HashSet

let texto = "a b c a d e f c";
let palabras: Vec<&str> = texto.split_whitespace().collect();

// Convertir a HashSet para eliminar duplicados
let palabras_unicas: HashSet<_> = palabras.iter().cloned().collect();
println!("Palabras únicas: {:?}", palabras_unicas);

// Volver a convertir a vector si es necesario
let palabras_sin_duplicados: Vec<_> = palabras_unicas.into_iter().collect();
println!("Vector sin duplicados: {:?}", palabras_sin_duplicados);

Filtrado de elementos únicos

fn obtener_elementos_unicos(lista1: &[i32], lista2: &[i32]) -> Vec<i32> {
    let conjunto1: HashSet<_> = lista1.iter().cloned().collect();
    let conjunto2: HashSet<_> = lista2.iter().cloned().collect();
    
    // Elementos que están en lista1 pero no en lista2
    conjunto1.difference(&conjunto2).cloned().collect()
}

let resultados_grupo_a = [85, 90, 74, 83, 92];
let resultados_grupo_b = [90, 72, 85, 64, 88];

let unicos_grupo_a = obtener_elementos_unicos(&resultados_grupo_a, &resultados_grupo_b);
println!("Resultados únicos del grupo A: {:?}", unicos_grupo_a);

Registro de accesos ordenado con BTreeSet

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Acceso {
    timestamp: u64,
    usuario: String,
}

let mut registro = BTreeSet::new();

// Registrar accesos (no necesariamente en orden)
registro.insert(Acceso { timestamp: 1623456789, usuario: "usuario1".to_string() });
registro.insert(Acceso { timestamp: 1623456123, usuario: "usuario2".to_string() });
registro.insert(Acceso { timestamp: 1623457890, usuario: "usuario3".to_string() });

// Los accesos se mostrarán ordenados por timestamp (y luego por usuario si hay empate)
println!("Registro de accesos ordenado:");
for acceso in &registro {
    println!("{}: {}", acceso.timestamp, acceso.usuario);
}

Es importante recordar que insertar elementos en colecciones puede transferir su propiedad, algo que estudiaremos en el módulo de ownership.

Aprende Rust online

Otras lecciones de Rust

Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Rust y certifícate

Ejercicios de programación de Rust

Evalúa tus conocimientos de esta lección Colecciones estándar con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la estructura y uso de vectores (Vec<T>) en Rust, incluyendo creación, acceso, modificación e iteración.
  • Aprender a utilizar mapas (HashMap y BTreeMap) para almacenar pares clave-valor, con sus diferencias y casos de uso.
  • Conocer los conjuntos (HashSet y BTreeSet), sus propiedades de unicidad y operaciones matemáticas de conjuntos.
  • Diferenciar entre las implementaciones basadas en hash y en árboles B, entendiendo sus ventajas y desventajas.
  • Aplicar las colecciones estándar en ejemplos prácticos para resolver problemas comunes de programación.