16 HashMap
Un HashMap<K, V> es una colección que almacena pares formados por una clave y un valor. K representa el tipo de las claves y V el tipo de los valores. A diferencia de un vector, cuyos elementos se identifican por su posición, en un HashMap cada valor se asocia a una clave única, que se utiliza para insertarlo, buscarlo o modificarlo.
Este tipo de colección resulta especialmente adecuado cuando el acceso a los datos se realiza a partir de un identificador en lugar de una posición. Por ejemplo, una agenda telefónica que asocia nombres con números de teléfono, un diccionario que asocia palabras con sus definiciones o una tabla que asocia direcciones MAC con direcciones IP.
HashMap está implementado mediante una estructura de datos denominada tabla hash. Para cada clave se calcula un número entero mediante una función hash, y ese número se utiliza para determinar la posición donde se almacenará el elemento dentro de una tabla.
Gracias a este mecanismo, cuando se desea buscar un elemento no es necesario recorrer toda la colección. Basta con volver a aplicar la función hash a la clave para localizar la posición donde probablemente se encuentra el valor. Decimos probablemente porque dos claves distintas pueden producir el mismo valor hash y, por tanto, corresponder a la misma posición.
Cuando dos claves distintas producen el mismo valor hash se dice que se ha producido una colisión. Las implementaciones modernas de las tablas hash incorporan mecanismos para resolver estas colisiones de forma eficiente. Como consecuencia, las operaciones de inserción, búsqueda y eliminación tienen, en promedio, una complejidad temporal constante, es decir, (O(1)).
Desde el punto de vista del programador, todos estos detalles son completamente transparentes. Basta con proporcionar una clave para insertar, buscar o eliminar un elemento, sin preocuparse por cómo se almacena internamente.
16.1 Uso básico de un HashMap
El siguiente ejemplo ilustra las operaciones básicas sobre un HashMap:
use std::collections::HashMap;
fn main() {
let mut tabla = HashMap::new();
// Insertamos dos elementos.
tabla.insert("00:11:22:33:44:55", "192.168.1.10");
tabla.insert("AA:BB:CC:DD:EE:FF", "192.168.1.20");
// Mostramos la tabla completa.
println!("{:?}", tabla);
// Mostramos el elemento asociado a una clave
println!("IP: {}", tabla.get("00:11:22:33:44:55").unwrap());
// Reemplazamos el valor de un elemento
tabla.insert("00:11:22:33:44:55", "192.168.1.100");
// Eliminamos un elemento
tabla.remove("AA:BB:CC:DD:EE:FF");
println!("{:?}", tabla);
}La sentencia
use std::collections::HashMap;importa el tipoHashMapde la biblioteca estándar. A diferencia de tipos comoi32,bool,char,StringoVec, es necesario importarHashMapantes de poder utilizarlo.Crea un
HashMapvacío mediante el método asociadoHashMap::new(). La variable se declara comomutporque su contenido va a modificarse.Inserta dos elementos mediante el método
insert(). Obsérvese que no es necesario indicar los tipos de las claves ni de los valores, ya que Rust los deduce a partir de los datos insertados.El método
insert()devuelve unOptioncon el valor anterior asociado a la clave. Si la clave no existía, devuelveNone. Pero en muchos casos, como en este ejemplo, el valor devuelto porinsert()es ignorado.Muestra el contenido completo del
HashMaputilizando el especificador de formato{:?}. Como ocurre con los vectores,HashMapimplementa el traitDebug, lo que permite mostrar su contenido conprintln!(). El orden en que aparecen los elementos no está definido y puede variar entre ejecuciones.Recupera el valor asociado a una clave mediante el método
get(). Este método devuelve unOption, por lo que en este ejemplo se utilizaunwrap()al saber que la clave existe. Tal y como vimos en la Sección 12.4.3.El método
get()devuelve unOption<&V>, es decir, una referencia al valor almacenado. De este modo, el elemento permanece dentro delHashMapy puede seguir utilizándose posteriormente.Reemplaza el valor asociado a una clave existente. Para ello basta con volver a llamar a
insert()utilizando la misma clave y un nuevo valor. El valor anterior se sustituye automáticamente.Elimina un elemento mediante el método
remove(). Si la clave no existe, el método no produce ningún error y simplemente no modifica el contenido delHashMap.
16.2 Acceso mediante el operador []
Además del método get(), también es posible acceder a un valor mediante el operador [].
println!("{}", tabla["00:11:22:33:44:55"]);A diferencia de get(), el operador [] devuelve directamente una referencia al valor asociado a la clave. Si la clave no existe, el programa finaliza produciendo un panic. Por este motivo, en la mayoría de las situaciones se prefiere utilizar get(), que devuelve un Option y permite tratar de forma segura el caso en que la clave no exista.
El operador [] solo puede utilizarse para leer un valor. No es posible modificar un elemento mediante una asignación. Para modificar el valor asociado a una clave debe utilizarse el método insert(), que reemplaza el valor anterior si la clave ya existe.
use std::collections::HashMap;
fn main() {
let mut tabla = HashMap::new();
tabla.insert("00:11:22:33:44:55", "192.168.1.10");
tabla.insert("AA:BB:CC:DD:EE:FF", "192.168.1.20");
// Lectura mediante el operador []
println!(
"IP: {}",
tabla["00:11:22:33:44:55"]
);
// El operador [] no puede utilizarse para modificar un elemento.
tabla["00:11:22:33:44:55"] = "192.168.1.100"; // ¡Error!
}16.3 La API entry()
En ocasiones es necesario realizar una operación distinta según que una clave exista o no en el HashMap. Por ejemplo, puede ser necesario insertar un valor únicamente si la clave no existe, inicializar un contador o modificar el valor asociado a una clave existente.
Una posibilidad sería comprobar primero si la clave existe y, a continuación, actuar en consecuencia. Sin embargo, este enfoque no es el más eficiente, ya que obliga a buscar la misma clave dos veces: una para comprobar si existe y otra para insertar o modificar el valor.
Para resolver este problema, HashMap proporciona la API entry(), que realiza una única búsqueda y permite actuar sobre la entrada correspondiente, tanto si la clave ya existe como si no.
16.3.1 El método or_insert()
El método or_insert() se utiliza junto con entry(). Recibe un valor que se insertará únicamente si la clave indicada en entry() no existe previamente en el HashMap.
Si la clave ya existe, no sucede nada y el valor asociado se conserva.
Si la clave no existe, se inserta un nuevo elemento con dicha clave y el valor indicado.
Este comportamiento resulta útil para inicializar valores sin tener que comprobar previamente si la clave existe.
use std::collections::HashMap;
fn main() {
let mut contador = HashMap::new();
contador.insert("HTTP", 443);
contador.entry("HTTP").or_insert(443);
contador.entry("SSH").or_insert(22);
println!("{contador:?}");
}En este ejemplo, la clave "HTTP" ya existe, por lo que conserva el valor 443. En cambio, la clave "SSH" no existía y se inserta con el valor 22.
16.4 Recorrido de un HashMap
Los elementos de un HashMap pueden recorrerse mediante un bucle for. Dependiendo de la información que interese obtener, existen tres posibilidades:
iter(), que recorre pares formados por la clave y el valor.keys(), que recorre únicamente las claves.values(), que recorre únicamente los valores.
El siguiente ejemplo muestra las tres formas de recorrer un HashMap.
use std::collections::HashMap;
fn main() {
let mut tabla = HashMap::new();
tabla.insert("HTTP", 80);
tabla.insert("HTTPS", 443);
tabla.insert("SSH", 22);
// Recorremos claves y valores.
for (protocolo, puerto) in tabla.iter() {
println!("{protocolo}: {puerto}");
}
println!();
// Recorremos únicamente las claves.
for protocolo in tabla.keys() {
println!("{protocolo}");
}
println!();
// Recorremos únicamente los valores.
for puerto in tabla.values() {
println!("{puerto}");
}
println!();
}
Al igual que ocurre al mostrar un HashMap mediante println!("{:?}"), el orden en que se recorren los elementos no está definido y puede variar entre ejecuciones.
16.5 Otras operaciones habituales
Además de las operaciones vistas hasta ahora, HashMap proporciona otros métodos de uso frecuente.
use std::collections::HashMap;
fn main() {
let mut tabla = HashMap::new();
tabla.insert("HTTP", 80);
tabla.insert("HTTPS", 443);
tabla.insert("SSH", 22);
println!("{:?}", tabla);
println!("¿La tabla está vacía? {}", tabla.is_empty());
println!("Número de elementos: {}", tabla.len());
println!("¿Contiene HTTP? {}", tabla.contains_key("HTTP"));
println!("¿Contiene FTP? {}", tabla.contains_key("FTP"));
println!();
tabla.clear();
println!("{:?}", tabla);
println!("¿La tabla está vacía? {}", tabla.is_empty());
}len()devuelve el número de elementos almacenados en elHashMap.is_empty()devuelvetruesi elHashMapno contiene ningún elemento yfalseen caso contrario.contains_key()comprueba si una determinada clave existe en elHashMap. Devuelvetruesi existe yfalseen caso contrario.clear()elimina todos los elementos delHashMap, dejándolo vacío.
16.6 Resumen
En este capítulo hemos estudiado HashMap<K, V>, la estructura de datos que permite asociar claves con valores.
Las ideas principales son las siguientes:
- Un
HashMapalmacena pares clave-valor y permite acceder a los valores a partir de su clave. - Las claves deben ser únicas; insertar un valor con una clave ya existente sustituye al valor anterior.
- Los elementos pueden insertarse, consultarse, modificarse y eliminarse mediante los métodos proporcionados por la biblioteca estándar.
- El método
get()devuelve unOption<&V>, por lo que es necesario tratar el caso en que la clave no exista. - La API
entry()permite insertar o modificar valores de forma eficiente sin realizar búsquedas repetidas. - Es posible recorrer un
HashMapmediante un buclefor, obteniendo referencias a sus claves y valores.
Los mapas hash son una de las colecciones más útiles de Rust cuando se necesita localizar información de forma rápida a partir de una clave. Junto con los vectores y las cadenas, constituyen una parte fundamental de la biblioteca estándar y aparecen con frecuencia en aplicaciones reales.