HashSet em Rust: Conjuntos e Operações de Conjunto

Aprenda HashSet em Rust: operações de conjunto (união, interseção, diferença), deduplicação, padrões de uso e exemplos práticos com código completo.

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çãoCaso MédioPior CasoObservaçã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