← Hub Principal |
🔑

03: Custom HashMap (Linear Probing & Open Addressing)

Rust 2021 (Data Structures)
Hashing & Algoritmos de Armazenamento Módulo 03

Tabela Hash Manual com Endereçamento Aberto

Uma investigação profunda sobre como estruturas associativas funcionam em nível de máquina. Esta implementação não utiliza a coleção nativa da stdlib, criando do zero um mapa com sondagem linear (*Linear Probing*), reciclagem de registros excluídos via *Tombstones* e redimensionamento dinâmico baseado em limiares de saturação.


01. Decisão de Arquitetura: Endereçamento Aberto vs Encadeamento

Quando duas chaves distintas geram o mesmo índice de hash, ocorre uma colisão. Existem duas formas canônicas de resolver colisões na ciência da computação:

1. Separate Chaining (Encadeamento)

Cada bucket armazena um ponteiro para uma lista ligada. É simples de implementar, mas fragmenta a memória com pequenas alocações avulsas no Heap, gerando altos índices de Cache Misses durante a leitura.

2. Open Addressing (Nossa Abordagem)

Todos os dados residem dentro de um único vetor plano contíguo (Vec<Bucket<K, V>>). Ao colidir no índice $i$, inspecionamos sequencialmente $i+1, i+2, \dots$. Essa técnica maximiza a localidade espacial das linhas de cache L1/L2 do processador.

02. O Padrão Tombstone (Lápides de Deleção)

O maior desafio do Endereçamento Aberto é a remoção. Se ao remover um item simplesmente definíssemos o slot como Empty, a cadeia de busca para elementos que colidiram após ele seria interrompida prematuramente, fazendo buscas legítimas retornarem None.

Como os três estados do enum resolvem o problema:

  • Bucket::Empty: O slot nunca foi preenchido. Finaliza a busca imediatamente com segurança.
  • Bucket::Occupied(K, V): Contém dados ativos. Se a chave for idêntica, retorna o valor.
  • Bucket::Tombstone: Slot previamente deletado. As operações de get() pulam este slot continuando a busca linear, enquanto operações subsequentes de insert() podem reciclar este espaço sem alocar novos slots.

03. Fator de Carga ($\alpha$) & Primary Clustering

Definido como $\alpha = \frac{N + T}{M}$ (onde $N$ são itens ocupados, $T$ são túmulos e $M$ é a capacidade física total). Se $\alpha$ se aproximar de $1.0$, o Linear Probing sofre de Primary Clustering: longos blocos de slots ocupados começam a se fundir, degradando a busca para $O(N)$.

Ao fixar o limiar em $\alpha \ge 0.70$, o método rehash() dobra a capacidade física, purga todos os túmulos e redistribui as chaves ativas, mantendo o custo médio de busca e inserção rigorosamente em $O(1)$.

Operação Caso Médio Pior Caso Comportamento de Memória
insert(k, v) O(1) amortizado O(n) no rehash Recicla túmulos ou insere em slot contíguo
get(k) O(1) O(n) tabela saturada Varredura contígua com alto Cache Hit
remove(k) O(1) O(n) Gravação imediata da flag Tombstone

04. Como a stdlib do Rust (HashBrown) Faz?

💡 Swiss Tables & SIMD:

Historicamente, o Rust utilizava Robin Hood Hashing com SipHash. A partir da versão 1.36, a biblioteca padrão adotou a crate hashbrown (inspirada nas Swiss Tables do Google). Ela separa o array em dois: um array de controle de 1 byte por bucket e um array de pares chave-valor. O array de controle é inspecionado usando instruções SIMD que comparam até 16 buckets simultaneamente em um único ciclo de clock.

cargo run --bin custom_hashmap
Ambiente pronto. Clique em "Compilar & Executar" para rodar os testes de hashing em Rust.