HashMap, HashSet e iteradores básicos en Rust — Capítulo 12

Exploración de colecciones asociativas y recorridos elementales


En el contexto de un libro sobre programación en Rust, este capítulo se centra en las estructuras de datos asociativas proporcionadas por la biblioteca estándar, específicamente HashMap y HashSet, junto con los mecanismos básicos para iterar sobre ellas. Estas herramientas son fundamentales para manejar datos no secuenciales de manera eficiente, permitiendo operaciones rápidas de inserción, búsqueda y eliminación en escenarios donde la asociación clave-valor o la unicidad de elementos resultan esenciales. Su comprensión habilita el desarrollo de aplicaciones más robustas, optimizando el rendimiento en tareas como el almacenamiento de configuraciones o el seguimiento de conjuntos únicos.

La Estructura HashMap

La estructura HashMap<K, V> en Rust representa una tabla hash que asocia claves de tipo K con valores de tipo V, donde tanto K como V deben implementar los traits necesarios para el hashing y la comparación. Esta colección, parte del módulo std::collections, utiliza un algoritmo de hashing para distribuir las entradas en buckets internos, lo que garantiza un tiempo de acceso promedio de O(1) para operaciones comunes, asumiendo una distribución uniforme de hashes. Es importante destacar que HashMap no preserva el orden de inserción; para eso, se recurre a BTreeMap en capítulos posteriores.

La inserción de elementos se realiza mediante el método insert, que toma una clave y un valor, devolviendo el valor anterior si la clave ya existía. Por ejemplo:

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

En este fragmento, se crea una instancia vacía con new y se insertan pares clave-valor. Si la clave ya existe, insert sobrescribe el valor anterior, lo que es un detalle sutil en escenarios de actualización concurrente. Para inserciones condicionales, se emplea entry con patrones como or_insert, aunque este capítulo se limita a las operaciones básicas.

La búsqueda se efectúa con get, que devuelve una Option<&V> referenciando el valor asociado, o None si la clave no existe. Un caso borde surge cuando la clave es un tipo que implementa Hash pero no Eq correctamente, lo que podría llevar a colisiones no resueltas; Rust exige que K implemente ambos traits para evitar tales inconsistencias.

let team_name = String::from("Blue");
if let Some(score) = scores.get(&team_name) {
    // Uso del score
}

La eliminación se logra con remove, que elimina la entrada y devuelve el valor asociado si existía. Un detalle importante es que remove no altera la capacidad interna de la tabla hash, lo que podría implicar reasignaciones en inserciones posteriores si se excede el factor de carga.

scores.remove(&String::from("Yellow"));

En comparación con diccionarios en Python o mapas en C++, HashMap en Rust enfatiza la propiedad (ownership): las claves y valores se mueven al mapa, requiriendo clones o referencias si se necesitan en otros contextos.

La Estructura HashSet

La estructura HashSet<T> es una colección de elementos únicos de tipo T, implementada internamente como un HashMap<T, ()> donde las claves son los elementos y los valores son unidades vacías. Al igual que HashMap, reside en std::collections y ofrece tiempos de operación promedio O(1) gracias al hashing. No mantiene orden, y su utilidad radica en escenarios donde se requiere verificar membresía o eliminar duplicados eficientemente.

La inserción se realiza con insert, que devuelve true si el elemento no existía previamente y fue agregado, o false si ya estaba presente. Esto contrasta con conjuntos en lenguajes como Java, donde add podría no indicar duplicados explícitamente.

use std::collections::HashSet;

let mut books = HashSet::new();
books.insert("Rust in Action");
books.insert("The Rust Programming Language");
let added = books.insert("Rust in Action"); // false

Un caso borde surge con tipos que implementan Hash de manera deficiente, potencialmente causando colisiones y degradando el rendimiento a O(n) en el peor caso. Rust mitiga esto mediante un hasher seguro por defecto, pero se recomienda implementar Hash cuidadosamente para tipos personalizados.

La búsqueda se efectúa con contains, que devuelve un booleano indicando la presencia del elemento.

if books.contains("Rust in Action") {
    // Elemento presente
}

La eliminación usa remove, devolviendo true si el elemento existía y fue eliminado. Similar a HashMap, no reduce la capacidad automáticamente, lo que es relevante para optimizaciones de memoria.

books.remove("The Rust Programming Language");

En términos comparativos, HashSet se asemeja a los sets en Python, pero en Rust, la propiedad de los elementos insertados impone consideraciones adicionales, como el uso de Rc o Arc para compartir referencias en contextos multi-hilo.

Iteradores Básicos

Las colecciones como HashMap y HashSet proporcionan métodos para generar iteradores que permiten recorrer sus elementos de manera eficiente, sin exponer la estructura interna. Estos iteradores básicos se centran en iteriter_mut y el uso en bucles for, facilitando el acceso secuencial a datos no ordenados. Es esencial notar que, dada la naturaleza no ordenada de estas colecciones, el orden de iteración no está garantizado y puede variar entre ejecuciones debido a la reorganización de la tabla hash.

El método iter devuelve un iterador inmutable sobre referencias a los elementos. Para HashMap, itera sobre tuplas (&K, &V); para HashSet, sobre &T.

for (key, value) in scores.iter() {
    // Procesamiento de clave y valor
}

En bucles for, el iterador se consume implícitamente, lo que es un patrón idiomático en Rust para evitar copias innecesarias. Un detalle sutil es que iter no consume la colección, permitiendo múltiples iteraciones, a diferencia de iteradores que mueven ownership.

iter_mut proporciona un iterador mutable, permitiendo modificar los elementos en lugar. Para HashMap, itera sobre (&K, &mut V), lo que impide modificar claves para preservar la integridad del hash.

for (_key, value) in scores.iter_mut() {
    *value += 10;
}

Para HashSetiter_mut itera sobre &mut T, habilitando modificaciones directas, aunque con precaución para no alterar el hash del elemento, lo que podría invalidar la estructura.

Los bucles for sobre colecciones invocan implícitamente into_iter si se consume ownership, pero para iteraciones inmutables, se prefiere iter. Un caso borde ocurre cuando la colección se modifica durante la iteración, lo que provoca pánicos en tiempo de ejecución; Rust previene esto mediante borrowing rules, exigiendo que la colección no sea mutable mientras se itera inmutablemente.

En comparación con iteradores en C++ (como begin y end), los de Rust son más seguros gracias al borrow checker, evitando invalidaciones accidentales.

Proyecto: Contador Profesional de Palabras

Para ilustrar la aplicación integrada de HashMap y HashSet con iteradores básicos, se presenta un proyecto completo que implementa un contador de palabras en un texto dado. Este programa lee un archivo de texto, cuenta la frecuencia de cada palabra (ignorando mayúsculas y puntuación básica), utiliza un HashSet para palabras únicas y genera estadísticas como el total de palabras, el número de únicas y un ranking de las 10 más frecuentes ordenadas por frecuencia descendente. El ranking se obtiene iterando sobre el mapa de frecuencias y recolectando en un vector para su ordenación posterior.

La estructura de carpetas es la siguiente:

word_counter/
├── Cargo.toml
├── src/
   └── main.rs
└── input.txt  // Archivo de ejemplo con texto

Contenido de Cargo.toml:

[package]
name = "word_counter"
version = "0.1.0"
edition = "2021"

[dependencies]

Contenido de src/main.rs:

use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{self, BufRead};
use std::env;

fn main() -> io::Result<()> {
    let args: Vec<String> = env::args().collect();
    if args.len() != 2 {
        eprintln!("Uso: word_counter <archivo>");
        std::process::exit(1);
    }

    let file = File::open(&args[1])?;
    let lines = io::BufReader::new(file).lines();

    let mut word_freq: HashMap<String, u32> = HashMap::new();
    let mut unique_words: HashSet<String> = HashSet::new();
    let mut total_words = 0;

    for line in lines {
        let line = line?;
        let words: Vec<String> = line.split_whitespace()
            .map(|s| s.trim_matches(|c: char| c.is_punctuation() || c.is_whitespace())
                      .to_lowercase())
            .filter(|s| !s.is_empty())
            .map(String::from)
            .collect();

        for word in words {
            *word_freq.entry(word.clone()).or_insert(0) += 1;
            unique_words.insert(word);
            total_words += 1;
        }
    }

    // Estadísticas
    println!("Total de palabras: {}", total_words);
    println!("Palabras únicas: {}", unique_words.len());

    // Ranking de las 10 más frecuentes
    let mut freq_vec: Vec<(&String, &u32)> = word_freq.iter().collect();
    freq_vec.sort_by(|a, b| b.1.cmp(a.1));

    println!("Ranking de las 10 palabras más frecuentes:");
    for (i, (word, freq)) in freq_vec.iter().take(10).enumerate() {
        println!("{}. {}: {}", i + 1, word, freq);
    }

    // Iteración sobre conjuntos para ejemplo
    println!("Ejemplo de palabras únicas (primeras 5):");
    let mut unique_iter = unique_words.iter();
    for _ in 0..5 {
        if let Some(word) = unique_iter.next() {
            println!("{}", word);
        }
    }

    Ok(())
}

Este código demuestra inserciones en HashMap y HashSet, búsquedas implícitas via entry, eliminación no usada aquí pero posible para filtrados, e iteradores básicos como iter para recolectar y ordenar. Para ejecutar, coloque texto en input.txt y corra cargo run input.txt. Nota que el orden en iteraciones sobre unique_words no está garantizado.

Habiendo explorado estas colecciones asociativas y sus mecanismos de iteración elementales, el siguiente capítulo profundizará en estructuras ordenadas como BTreeMap y BTreeSet, extendiendo las capacidades para escenarios que requieren ordenamiento implícito.

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio