O HashMap é uma das estruturas de dados mais utilizadas na programação. Ele permite associar chaves a valores com acesso em tempo constante amortizado O(1). Em Rust, o HashMap da biblioteca padrão é implementado sobre a crate hashbrown, que usa o algoritmo Swiss Table do Google – uma das implementações de tabela hash mais eficientes do mundo.
Nesta página, vamos explorar desde o conceito fundamental de hashing até os detalhes da implementação em Rust, incluindo a poderosa Entry API.
Conceito e Teoria
O que é uma Tabela Hash?
Uma tabela hash é uma estrutura que mapeia chaves a valores usando uma função hash. A função hash transforma a chave em um índice no array interno:
Chave "rust" --> hash("rust") --> 42 --> tabela[42] = valor
Anatomia de uma Tabela Hash
Chave Função Hash Índice Buckets (Array)
┌─────────┐ ┌───────────┐ ┌─────────────────┐
│ "rust" │───>│ hash() │──> 2 │ 0: (vazio) │
└─────────┘ └───────────┘ │ 1: (vazio) │
┌─────────┐ ┌───────────┐ │ 2: "rust" -> 10 │ <──
│ "cargo" │───>│ hash() │──> 5 │ 3: (vazio) │
└─────────┘ └───────────┘ │ 4: (vazio) │
┌─────────┐ ┌───────────┐ │ 5: "cargo" -> 7 │ <──
│ "crate" │───>│ hash() │──> 2 COLISÃO! │ 6: (vazio) │
└─────────┘ └───────────┘ │ 7: (vazio) │
└─────────────────┘
Tratamento de Colisões
Quando duas chaves diferentes produzem o mesmo índice, temos uma colisão. Existem duas estratégias principais:
1. Encadeamento (Chaining):
Índice 2: "rust"->10 ──> "crate"->3 ──> NULL
┌────────┐ ┌────────┐
│ rust:10│──────>│crate: 3│──> ∅
└────────┘ └────────┘
2. Endereçamento Aberto (Open Addressing):
Se índice 2 está ocupado, tente o próximo:
Índice: 0 1 2 3 4 5
[ ] [ ] [rust:10] [crate:3] [ ] [cargo:7]
↑ ↑
original reprobing (colisão resolvida)
Swiss Table (hashbrown)
O Rust usa Swiss Table, que é uma forma sofisticada de endereçamento aberto com:
┌──────────────────────────────────────────────────┐
│ Swiss Table Layout │
│ │
│ Metadata (1 byte por slot): │
│ ┌────┬────┬────┬────┬────┬────┬────┬────┐ │
│ │0x82│0xFF│0x45│0xFF│0xFF│0x12│0xFF│0x67│ │
│ └────┴────┴────┴────┴────┴────┴────┴────┘ │
│ ↑ocu ↑vaz ↑ocu ↑vaz ↑vaz ↑ocu ↑vaz ↑ocu│
│ │
│ Dados (slots de chave-valor): │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬───┐ │
│ │ k:v │ │ k:v │ │ │ k:v │...│ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴───┘ │
│ │
│ Busca com SIMD: verifica 16 slots de uma vez! │
└──────────────────────────────────────────────────┘
Vantagens da Swiss Table:
- Busca SIMD: compara 16 bytes de metadata simultaneamente
- Cache-friendly: metadata compacta cabe na cache L1
- Load factor alto: funciona bem até 87.5% de ocupação
- SipHash 1-3: função hash padrão resistente a DoS
Complexidade
| Operação | Caso Médio | Pior Caso | Observação |
|---|---|---|---|
insert(k, v) | O(1)* | O(n) | *Amortizado, pode realocar |
get(&k) | O(1) | O(n) | Pior caso com muitas colisões |
remove(&k) | O(1) | O(n) | Marca slot como “tombstone” |
contains_key(&k) | O(1) | O(n) | Apenas verifica existência |
len() | O(1) | O(1) | Contador mantido internamente |
iter() | O(n) | O(n) | Visita todos os elementos |
| Espaço | O(n) | O(n) | Proporcional ao número de slots |
Nota: O pior caso O(n) é extremamente raro com SipHash. Na prática, o desempenho é consistentemente O(1).
Implementação em Rust
Uso Básico do HashMap
use std::collections::HashMap;
fn main() {
// Criando um HashMap vazio
let mut notas: HashMap<String, f64> = HashMap::new();
// Inserindo pares chave-valor
notas.insert(String::from("Maria"), 9.5);
notas.insert(String::from("João"), 8.0);
notas.insert(String::from("Ana"), 9.8);
// Acessando valores
if let Some(nota) = notas.get("Maria") {
println!("Nota da Maria: {}", nota);
}
// Verificando existência
println!("João existe? {}", notas.contains_key("João"));
// Iterando sobre o HashMap
for (nome, nota) in ¬as {
println!("{}: {:.1}", nome, nota);
}
// Removendo um elemento
notas.remove("João");
println!("Após remoção: {:?}", notas);
}
Criando HashMap com collect()
use std::collections::HashMap;
fn main() {
// A partir de vetores de tuplas
let times = vec![
("Flamengo", 71),
("Palmeiras", 70),
("Atlético-MG", 65),
];
let classificacao: HashMap<&str, i32> = times.into_iter().collect();
println!("{:?}", classificacao);
// A partir de dois vetores com zip
let chaves = vec!["a", "b", "c"];
let valores = vec![1, 2, 3];
let mapa: HashMap<&str, i32> = chaves.into_iter().zip(valores).collect();
println!("{:?}", mapa);
}
Entry API: O Poder do HashMap em Rust
A Entry API é uma das funcionalidades mais elegantes do Rust. Ela evita buscas duplas e torna o código mais expressivo:
use std::collections::HashMap;
fn main() {
let mut contagem: HashMap<&str, i32> = HashMap::new();
// ❌ Sem Entry API: duas buscas (contains_key + insert)
let palavra = "rust";
if contagem.contains_key(palavra) {
*contagem.get_mut(palavra).unwrap() += 1;
} else {
contagem.insert(palavra, 1);
}
// ✅ Com Entry API: uma única busca
*contagem.entry("rust").or_insert(0) += 1;
// or_default() — usa Default::default() (0 para i32)
*contagem.entry("cargo").or_default() += 1;
// or_insert_with() — closure executada somente se a chave não existir
contagem.entry("crate").or_insert_with(|| {
println!("Calculando valor inicial...");
42
});
// and_modify() — modifica valor existente
contagem
.entry("rust")
.and_modify(|v| *v += 10)
.or_insert(1);
println!("{:?}", contagem);
}
Diagrama da Entry API
entry("chave")
│
▼
┌──────────────┐
│ Chave existe?│
└──────┬───────┘
Sim │ │ Não
▼ ▼
┌─────────┐ ┌───────────┐
│Occupied │ │ Vacant │
│ Entry │ │ Entry │
└────┬────┘ └─────┬─────┘
│ │
▼ ▼
.get() .or_insert(val)
.get_mut() .or_default()
.insert(val) .or_insert_with(f)
.remove()
.into_mut()
Métodos comuns a ambos:
.and_modify(f).or_insert(v)
.key()
Hash Customizado com a Trait Hash
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
/// Representa um ponto em coordenadas 2D
#[derive(Debug, Eq, PartialEq)]
struct Ponto {
x: i32,
y: i32,
}
// Implementando Hash manualmente
// REGRA: se a == b, então hash(a) == hash(b)
impl Hash for Ponto {
fn hash<H: Hasher>(&self, state: &mut H) {
self.x.hash(state);
self.y.hash(state);
}
}
fn main() {
let mut mapa_pontos: HashMap<Ponto, String> = HashMap::new();
mapa_pontos.insert(
Ponto { x: 0, y: 0 },
String::from("Origem"),
);
mapa_pontos.insert(
Ponto { x: 3, y: 4 },
String::from("Ponto A"),
);
let busca = Ponto { x: 3, y: 4 };
if let Some(nome) = mapa_pontos.get(&busca) {
println!("Encontrado: {}", nome);
}
}
Usando Hasher Alternativo (FxHash)
// Para cenários onde a proteção contra DoS não é necessária
// (ex: compiladores, ferramentas offline), use FxHash:
//
// Cargo.toml:
// [dependencies]
// rustc-hash = "2"
use rustc_hash::FxHashMap;
fn exemplo_fxhash() {
let mut mapa: FxHashMap<i32, &str> = FxHashMap::default();
mapa.insert(1, "um");
mapa.insert(2, "dois");
// FxHash é ~2-5x mais rápido que SipHash para chaves inteiras
println!("{:?}", mapa);
}
Exemplo Prático: Contador de Frequência de Palavras
Um dos usos mais clássicos do HashMap: contar a frequência de palavras em um texto.
use std::collections::HashMap;
/// Conta a frequência de cada palavra em um texto.
/// Normaliza para minúsculas e remove pontuação.
fn contar_palavras(texto: &str) -> HashMap<String, usize> {
let mut frequencia: HashMap<String, usize> = HashMap::new();
for palavra in texto.split_whitespace() {
// Normaliza: minúsculas e remove pontuação
let limpa: String = palavra
.to_lowercase()
.chars()
.filter(|c| c.is_alphanumeric())
.collect();
if !limpa.is_empty() {
*frequencia.entry(limpa).or_insert(0) += 1;
}
}
frequencia
}
/// Retorna as N palavras mais frequentes, ordenadas por contagem.
fn top_n_palavras(frequencia: &HashMap<String, usize>, n: usize) -> Vec<(&String, &usize)> {
let mut pares: Vec<_> = frequencia.iter().collect();
pares.sort_by(|a, b| b.1.cmp(a.1)); // Ordena por frequência decrescente
pares.into_iter().take(n).collect()
}
/// Sistema de cache simples usando HashMap
struct Cache {
dados: HashMap<String, String>,
capacidade: usize,
}
impl Cache {
fn novo(capacidade: usize) -> Self {
Cache {
dados: HashMap::with_capacity(capacidade),
capacidade,
}
}
/// Busca no cache, retorna None se não encontrar
fn buscar(&self, chave: &str) -> Option<&String> {
self.dados.get(chave)
}
/// Insere no cache. Retorna false se o cache estiver cheio.
fn inserir(&mut self, chave: String, valor: String) -> bool {
if self.dados.len() >= self.capacidade && !self.dados.contains_key(&chave) {
return false; // Cache cheio
}
self.dados.insert(chave, valor);
true
}
/// Busca ou calcula o valor usando a função fornecida
fn buscar_ou_calcular<F>(&mut self, chave: &str, calcular: F) -> &String
where
F: FnOnce() -> String,
{
// Usa Entry API para evitar busca dupla
self.dados
.entry(chave.to_string())
.or_insert_with(calcular)
}
}
fn main() {
// === Contador de Palavras ===
let texto = "Rust é seguro. Rust é rápido. Rust é moderno. \
Programar em Rust é produtivo e divertido. \
A comunidade Rust é acolhedora e ativa.";
let frequencia = contar_palavras(texto);
println!("=== Frequência de Palavras ===");
let top = top_n_palavras(&frequencia, 5);
for (palavra, contagem) in &top {
let barra = "#".repeat(**contagem);
println!(" {:<12} {:>2} {}", palavra, contagem, barra);
}
// === Cache Simples ===
println!("\n=== Cache Simples ===");
let mut cache = Cache::novo(100);
// Simula busca com cache
let resultado = cache.buscar_ou_calcular("fibonacci_40", || {
println!(" Calculando fibonacci(40)... (operação custosa)");
String::from("102334155")
});
println!(" Resultado: {}", resultado);
// Segunda busca: vem do cache (sem recalcular)
let resultado = cache.buscar_ou_calcular("fibonacci_40", || {
println!(" Isso NÃO deve aparecer!");
String::from("erro")
});
println!(" Resultado do cache: {}", resultado);
// === Agrupamento com HashMap ===
println!("\n=== Agrupamento por Inicial ===");
let nomes = vec!["Ana", "Bruno", "Alice", "Bianca", "Carlos", "Amanda"];
let mut por_inicial: HashMap<char, Vec<&str>> = HashMap::new();
for nome in nomes {
let inicial = nome.chars().next().unwrap();
por_inicial.entry(inicial).or_default().push(nome);
}
for (inicial, grupo) in &por_inicial {
println!(" {}: {:?}", inicial, grupo);
}
}
Exercícios
Exercício 1: Detector de Anagramas
Implemente uma função que, dado um vetor de strings, agrupe as strings que são anagramas entre si. Duas palavras são anagramas se contêm as mesmas letras com as mesmas frequências.
// Dica: ordene as letras de cada palavra para criar uma "chave de anagrama"
// "amor" e "roma" ambas geram a chave "amor"
fn agrupar_anagramas(palavras: Vec<String>) -> Vec<Vec<String>> {
// Sua implementação aqui
todo!()
}
// Teste:
// agrupar_anagramas(vec!["amor", "roma", "mora", "carro", "corta", "crota"])
// => [["amor", "roma", "mora"], ["carro"], ["corta", "crota"]]
Exercício 2: Duas Somas (Two Sum)
Dado um vetor de inteiros e um valor alvo, encontre os dois índices cujos valores somam ao alvo. Use um HashMap para resolver em O(n).
fn duas_somas(nums: &[i32], alvo: i32) -> Option<(usize, usize)> {
// Dica: para cada número, verifique se (alvo - num) já foi visto
todo!()
}
// Teste:
// duas_somas(&[2, 7, 11, 15], 9) => Some((0, 1))
// duas_somas(&[3, 2, 4], 6) => Some((1, 2))
Exercício 3: LRU Cache Simplificado
Implemente um cache LRU (Least Recently Used) usando HashMap e Vec para rastrear a ordem de acesso. O cache deve ter capacidade limitada e remover o elemento menos recentemente usado quando estiver cheio.
struct LruSimples<K, V> {
capacidade: usize,
mapa: HashMap<K, V>,
ordem: Vec<K>, // ordem de acesso (mais recente no final)
}
impl<K: Eq + Hash + Clone, V> LruSimples<K, V> {
fn novo(capacidade: usize) -> Self { todo!() }
fn buscar(&mut self, chave: &K) -> Option<&V> { todo!() }
fn inserir(&mut self, chave: K, valor: V) { todo!() }
}
Conclusão
O HashMap é uma ferramenta essencial no arsenal de qualquer programador Rust. Seus pontos principais:
- Swiss Table (hashbrown) torna o HashMap do Rust um dos mais rápidos do mundo
- A Entry API é poderosa e elimina buscas redundantes
- SipHash protege contra ataques de colisão DoS por padrão
- Para máximo desempenho sem preocupação com DoS, use FxHashMap
- Implemente
HasheEqpara usar tipos customizados como chaves
Na próxima página, veremos o HashSet, que é construído sobre o HashMap e oferece operações eficientes de conjuntos.
Conteúdo produzido pela Equipe Rust Brasil para rustlang.com.br