O HashSet é a implementação de um conjunto (set) em Rust, construído internamente sobre o HashMap<T, ()>. Ele armazena valores únicos e oferece operações de conjunto eficientes como união, interseção, diferença e diferença simétrica, todas em tempo quase linear. É a ferramenta ideal quando você precisa garantir unicidade ou realizar comparações entre coleções.
Conceito e Teoria
O que é um Conjunto (Set)?
Um conjunto é uma coleção de elementos sem duplicatas e sem ordem garantida. Diferente de um Vec, onde o mesmo valor pode aparecer múltiplas vezes, um HashSet garante que cada elemento aparece no máximo uma vez.
Vec (permite duplicatas): HashSet (sem duplicatas):
┌───┬───┬───┬───┬───┐ ┌───────────────────────┐
│ 3 │ 1 │ 4 │ 1 │ 3 │ │ { 1, 3, 4 } │
└───┴───┴───┴───┴───┘ └───────────────────────┘
5 elementos 3 elementos únicos
Operações de Conjunto
Conjunto A = {1, 2, 3, 4} Conjunto B = {3, 4, 5, 6}
┌─────────────────────────────────────────────────────┐
│ │
│ UNIÃO (A ∪ B) = {1, 2, 3, 4, 5, 6} │
│ ┌───────────┬───────┬───────────┐ │
│ │ 1 2 │ 3 4 │ 5 6 │ │
│ │ A only │ A ∩ B │ B only │ │
│ └───────────┴───────┴───────────┘ │
│ │
│ INTERSEÇÃO (A ∩ B) = {3, 4} │
│ ┌───────┐ │
│ │ 3 4 │ │
│ └───────┘ │
│ │
│ DIFERENÇA (A - B) = {1, 2} │
│ ┌───────────┐ │
│ │ 1 2 │ │
│ └───────────┘ │
│ │
│ DIFERENÇA SIMÉTRICA (A △ B) = {1, 2, 5, 6} │
│ ┌───────────┐ ┌───────────┐ │
│ │ 1 2 │ │ 5 6 │ │
│ └───────────┘ └───────────┘ │
│ │
└─────────────────────────────────────────────────────┘
Diagrama de Venn
Conjunto A Conjunto B
┌──────────────────────────────────┐
│ ┌───────┐ │
│ ┌───────┤ ├───────┐ │
│ │ │ A ∩ B │ │ │
│ │ A-B │ │ B-A │ │
│ │ │ │ │ │
│ └───────┤ ├───────┘ │
│ └───────┘ │
│ A △ B = (A-B) ∪ (B-A) │
└──────────────────────────────────┘
Internamente: HashMap sem Valor
HashSet<T> é implementado como HashMap<T, ()>
HashSet<&str>:
┌────────────────────────────────────┐
│ HashMap<&str, ()> │
│ │
│ "rust" -> () │
│ "cargo" -> () │
│ "crate" -> () │
│ │
│ O valor () tem tamanho zero! │
│ Não ocupa memória adicional. │
└────────────────────────────────────┘
Complexidade
| Operação | Caso Médio | Pior Caso | Observação |
|---|---|---|---|
insert(v) | O(1)* | O(n) | *Amortizado |
contains(&v) | O(1) | O(n) | Verifica pertinência |
remove(&v) | O(1) | O(n) | Remove elemento |
len() | O(1) | O(1) | Contador interno |
union() | O(n + m) | O(n + m) | n e m = tamanhos dos conjuntos |
intersection() | O(min(n,m)) | O(n * m) | Itera sobre o menor conjunto |
difference() | O(n) | O(n * m) | Itera sobre self |
symmetric_difference() | O(n + m) | O(n + m) | Elementos exclusivos de cada um |
is_subset() | O(n) | O(n * m) | Verifica se A ⊆ B |
Implementação em Rust
Uso Básico
use std::collections::HashSet;
fn main() {
// Criando um HashSet
let mut linguagens: HashSet<String> = HashSet::new();
// Inserindo elementos — retorna true se o elemento é novo
println!("Inseriu Rust? {}", linguagens.insert(String::from("Rust"))); // true
println!("Inseriu Python? {}", linguagens.insert(String::from("Python"))); // true
println!("Inseriu Rust? {}", linguagens.insert(String::from("Rust"))); // false (duplicata!)
// Verificando pertinência
println!("Contém Rust? {}", linguagens.contains("Rust")); // true
println!("Contém Java? {}", linguagens.contains("Java")); // false
// Tamanho
println!("Total: {} linguagens", linguagens.len());
// Removendo
linguagens.remove("Python");
println!("Após remover Python: {:?}", linguagens);
// Iterando
for lang in &linguagens {
println!(" - {}", lang);
}
}
Criando HashSet de Forma Concisa
use std::collections::HashSet;
fn main() {
// A partir de um array usando collect()
let primos: HashSet<i32> = [2, 3, 5, 7, 11, 13].into_iter().collect();
println!("Primos: {:?}", primos);
// A partir de um Vec (remove duplicatas automaticamente!)
let com_duplicatas = vec![1, 2, 3, 2, 1, 4, 3, 5];
let unicos: HashSet<i32> = com_duplicatas.into_iter().collect();
println!("Únicos: {:?}", unicos); // {1, 2, 3, 4, 5}
// Macro auxiliar (padrão comum na comunidade Rust)
macro_rules! hashset {
($($elem:expr),* $(,)?) => {{
let mut set = HashSet::new();
$(set.insert($elem);)*
set
}};
}
let frutas = hashset!{"maçã", "banana", "laranja", "maçã"};
println!("Frutas: {:?}", frutas); // 3 elementos (sem duplicata)
}
Operações de Conjunto
use std::collections::HashSet;
fn main() {
let a: HashSet<i32> = [1, 2, 3, 4].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5, 6].into_iter().collect();
// UNIÃO: todos os elementos de ambos os conjuntos
let uniao: HashSet<&i32> = a.union(&b).collect();
println!("A ∪ B = {:?}", uniao); // {1, 2, 3, 4, 5, 6}
// INTERSEÇÃO: elementos em comum
let intersecao: HashSet<&i32> = a.intersection(&b).collect();
println!("A ∩ B = {:?}", intersecao); // {3, 4}
// DIFERENÇA: elementos em A que não estão em B
let diferenca: HashSet<&i32> = a.difference(&b).collect();
println!("A - B = {:?}", diferenca); // {1, 2}
// DIFERENÇA SIMÉTRICA: elementos exclusivos de cada conjunto
let simetrica: HashSet<&i32> = a.symmetric_difference(&b).collect();
println!("A △ B = {:?}", simetrica); // {1, 2, 5, 6}
// Operadores (usando referências, produz novo HashSet owned)
let uniao_op: HashSet<i32> = &a | &b; // União
let inter_op: HashSet<i32> = &a & &b; // Interseção
let diff_op: HashSet<i32> = &a - &b; // Diferença
let sym_op: HashSet<i32> = &a ^ &b; // Diferença simétrica
println!("\nCom operadores:");
println!("A | B = {:?}", uniao_op);
println!("A & B = {:?}", inter_op);
println!("A - B = {:?}", diff_op);
println!("A ^ B = {:?}", sym_op);
}
Relações entre Conjuntos
use std::collections::HashSet;
fn main() {
let todos: HashSet<i32> = [1, 2, 3, 4, 5].into_iter().collect();
let sub: HashSet<i32> = [2, 3].into_iter().collect();
let outro: HashSet<i32> = [6, 7].into_iter().collect();
// Subconjunto: todos os elementos de sub estão em todos?
println!("sub ⊆ todos? {}", sub.is_subset(&todos)); // true
println!("todos ⊆ sub? {}", todos.is_subset(&sub)); // false
// Superconjunto: todos contém todos os elementos de sub?
println!("todos ⊇ sub? {}", todos.is_superset(&sub)); // true
// Disjuntos: sem elementos em comum?
println!("todos ∩ outro = ∅? {}", todos.is_disjoint(&outro)); // true
println!("todos ∩ sub = ∅? {}", todos.is_disjoint(&sub)); // false
}
Padrões de Deduplicação
use std::collections::HashSet;
fn main() {
// Padrão 1: Deduplicar mantendo a ordem original
let dados = vec![5, 3, 1, 3, 5, 7, 1, 9, 3];
let mut visto = HashSet::new();
let unicos_ordenados: Vec<i32> = dados
.into_iter()
.filter(|x| visto.insert(*x)) // insert retorna true se é novo
.collect();
println!("Ordem preservada: {:?}", unicos_ordenados); // [5, 3, 1, 7, 9]
// Padrão 2: Encontrar duplicatas
let nomes = vec!["Ana", "Bruno", "Ana", "Carlos", "Bruno"];
let mut unicos = HashSet::new();
let duplicados: Vec<&str> = nomes
.into_iter()
.filter(|x| !unicos.insert(*x)) // false = já existia
.collect();
println!("Duplicados: {:?}", duplicados); // ["Ana", "Bruno"]
// Padrão 3: Contar elementos únicos
let texto = "aba caba daba aba";
let palavras_unicas: HashSet<&str> = texto.split_whitespace().collect();
println!("Palavras únicas: {}", palavras_unicas.len()); // 3
}
Exemplo Prático: Análise de Visitantes Únicos
use std::collections::HashSet;
/// Representa o log de acessos de um site
struct LogAcessos {
/// Visitantes por dia: cada entrada é (dia, conjunto de IPs)
visitantes_diarios: Vec<(String, HashSet<String>)>,
}
impl LogAcessos {
fn novo() -> Self {
LogAcessos {
visitantes_diarios: Vec::new(),
}
}
/// Registra visitas de um dia
fn registrar_dia(&mut self, dia: &str, ips: Vec<&str>) {
let conjunto: HashSet<String> = ips.into_iter().map(String::from).collect();
self.visitantes_diarios.push((dia.to_string(), conjunto));
}
/// Retorna todos os visitantes únicos em todo o período
fn visitantes_totais(&self) -> HashSet<&String> {
self.visitantes_diarios
.iter()
.flat_map(|(_, ips)| ips.iter())
.collect()
}
/// Retorna visitantes que acessaram TODOS os dias
fn visitantes_fieis(&self) -> HashSet<String> {
if self.visitantes_diarios.is_empty() {
return HashSet::new();
}
let mut resultado = self.visitantes_diarios[0].1.clone();
for (_, ips) in &self.visitantes_diarios[1..] {
resultado = &resultado & ips; // Interseção
}
resultado
}
/// Retorna visitantes que acessaram apenas uma vez no período
fn visitantes_unicos_dia(&self, dia: &str) -> HashSet<String> {
let alvo = self.visitantes_diarios
.iter()
.find(|(d, _)| d == dia)
.map(|(_, ips)| ips);
let outros: HashSet<String> = self.visitantes_diarios
.iter()
.filter(|(d, _)| d != dia)
.flat_map(|(_, ips)| ips.clone())
.collect();
match alvo {
Some(ips) => ips - &outros, // Diferença
None => HashSet::new(),
}
}
/// Imprime relatório completo
fn relatorio(&self) {
println!("╔══════════════════════════════════════╗");
println!("║ Relatório de Visitantes Únicos ║");
println!("╠══════════════════════════════════════╣");
for (dia, ips) in &self.visitantes_diarios {
println!("║ {}: {} visitantes únicos", dia, ips.len());
}
let totais = self.visitantes_totais();
println!("╠══════════════════════════════════════╣");
println!("║ Total no período: {} visitantes", totais.len());
let fieis = self.visitantes_fieis();
println!("║ Visitantes fiéis: {} (todos os dias)", fieis.len());
if !fieis.is_empty() {
for ip in &fieis {
println!("║ -> {}", ip);
}
}
println!("╚══════════════════════════════════════╝");
}
}
/// Encontra tags em comum entre artigos
fn tags_em_comum(artigos: &[(&str, HashSet<&str>)]) -> HashSet<&str> {
if artigos.is_empty() {
return HashSet::new();
}
let mut comum = artigos[0].1.clone();
for (_, tags) in &artigos[1..] {
comum = comum.intersection(tags).copied().collect();
}
comum
}
fn main() {
// === Análise de Visitantes ===
let mut log = LogAcessos::novo();
log.registrar_dia("2026-01-01", vec![
"192.168.1.1", "10.0.0.1", "172.16.0.1", "192.168.1.1", // duplicata ignorada
]);
log.registrar_dia("2026-01-02", vec![
"10.0.0.1", "172.16.0.1", "10.0.0.2", "10.0.0.3",
]);
log.registrar_dia("2026-01-03", vec![
"10.0.0.1", "172.16.0.1", "192.168.1.2", "10.0.0.1",
]);
log.relatorio();
let exclusivos = log.visitantes_unicos_dia("2026-01-01");
println!("\nVisitantes exclusivos do dia 01: {:?}", exclusivos);
// === Tags em Comum ===
println!("\n=== Análise de Tags ===");
let artigos = vec![
("Artigo 1", ["rust", "programação", "sistemas"].into_iter().collect::<HashSet<&str>>()),
("Artigo 2", ["rust", "web", "programação"].into_iter().collect()),
("Artigo 3", ["rust", "programação", "embarcados"].into_iter().collect()),
];
let comuns = tags_em_comum(&artigos);
println!("Tags em comum: {:?}", comuns); // {"rust", "programação"}
// Todas as tags (união)
let todas: HashSet<&str> = artigos
.iter()
.flat_map(|(_, tags)| tags.iter().copied())
.collect();
println!("Todas as tags: {:?}", todas);
}
Exercícios
Exercício 1: Verificador de Permissões
Implemente um sistema onde cada usuário tem um conjunto de permissões. Crie funções para verificar se um usuário tem todas as permissões necessárias para uma operação e para listar permissões faltantes.
use std::collections::HashSet;
struct Usuario {
nome: String,
permissoes: HashSet<String>,
}
/// Verifica se o usuário tem TODAS as permissões exigidas
fn tem_permissao(usuario: &Usuario, exigidas: &HashSet<String>) -> bool {
// Dica: use is_subset ou is_superset
todo!()
}
/// Retorna as permissões que o usuário não possui
fn permissoes_faltantes(usuario: &Usuario, exigidas: &HashSet<String>) -> HashSet<String> {
// Dica: use difference
todo!()
}
Exercício 2: Detector de Primeiro Caractere Único
Dada uma string, encontre o primeiro caractere que não se repete. Use um HashSet para rastrear caracteres vistos e duplicados.
fn primeiro_unico(s: &str) -> Option<char> {
// Dica: mantenha dois conjuntos — "vistos" e "duplicados"
// Um caractere é único se está em "vistos" mas não em "duplicados"
todo!()
}
// Teste:
// primeiro_unico("abracadabra") => Some('c')
// primeiro_unico("aabb") => None
Exercício 3: Número Feliz
Um número é “feliz” se, ao substituir repetidamente o número pela soma dos quadrados de seus dígitos, eventualmente chega a 1. Use um HashSet para detectar ciclos (evitar loop infinito).
fn eh_feliz(n: u32) -> bool {
// Dica: se o mesmo número aparecer novamente, é um ciclo (não feliz)
// Use HashSet para guardar números já vistos
todo!()
}
// Teste:
// eh_feliz(19) => true (19 -> 82 -> 68 -> 100 -> 1)
// eh_feliz(2) => false (entra em ciclo)
Conclusão
O HashSet é fundamental quando você precisa de:
- Unicidade garantida – elimina duplicatas automaticamente
- Operações de conjunto eficientes – união, interseção, diferença
- Verificação de pertinência em O(1) – muito mais rápido que buscar em Vec
- Comparação entre coleções – subconjunto, superconjunto, disjuntos
Lembre-se: internamente o HashSet<T> é um HashMap<T, ()>, então herda todas as propriedades de performance da Swiss Table. Use-o sempre que a semântica de conjunto fizer sentido para seu problema.
Na próxima página, veremos o Bloom Filter, uma estrutura probabilística que leva o conceito de conjunto a outro nível com uso mínimo de memória.
Conteúdo produzido pela Equipe Rust Brasil para rustlang.com.br