Hash Maps and Hash Sets
Hashing Fundamentals
Hashing transforms a key of arbitrary size or structure (such as a String, struct, or tuple) into a fixed-size 64-bit integer (u64) so that a hash table can map the key directly to a storage bucket in $O(1)$ average time.
The Three Components of Hashing in std::hash
Rust separates hashing into three distinct traits in std::hash:
| Trait | Role | Implemented By |
|---|---|---|
Hash |
Feeds the structural bytes of a key into a Hasher (key.hash(&mut hasher)) |
Domain key types (String, i32, #[derive(Hash)] structs) |
Hasher |
Stateful streaming algorithm that mixes fed bytes and produces a u64 digest (hasher.finish()) |
Hashing algorithms (DefaultHasher / SipHash-1-3, FxHasher, AHasher) |
BuildHasher |
Factory stored inside HashMap<K, V, S> that creates fresh Hasher instances (build_hasher()) |
RandomState (seeded per map), BuildHasherDefault<H> |
Key (e.g., "user_42")
│
▼ 1. map.hasher().build_hasher() creates a new Hasher state
+-----------------------------------+
| H: Hasher (SipHash-1-3 state) |
+-----------------------------------+
│ 2. key.hash(&mut hasher) feeds bytes + length prefix
▼
│ 3. hasher.finish() returns a 64-bit hash digest
▼
u64 Digest (e.g., 0x8a3f_c901_44b2_19d7)
├── Upper 57 bits (h1) ──> Selects initial bucket group (`h1 % num_buckets`)
└── Lower 7 bits (h2) ──> Stored in 1-byte SIMD control tag for fast filtering
Computing a 64-Bit Hash Manually
You can observe the exact u64 digest produced for any T: Hash by feeding it into std::hash::DefaultHasher:
use std::hash::{DefaultHasher, Hash, Hasher};
fn compute_hash<T: Hash>(value: &T) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
fn main() {
let h1 = compute_hash(&"rust-2024");
let h2 = compute_hash(&"rust-2024");
let h3 = compute_hash(&"rust-2021");
println!("Hash of 'rust-2024' (1): 0x{h1:016x}");
println!("Hash of 'rust-2024' (2): 0x{h2:016x} (deterministic per hasher seed)");
println!("Hash of 'rust-2021': 0x{h3:016x} (avalanche effect)");
}
Hash of 'rust-2024' (1): 0x5c36c0f489993524
Hash of 'rust-2024' (2): 0x5c36c0f489993524 (deterministic per hasher seed)
Hash of 'rust-2021': 0x2015a78529e3b98e (avalanche effect)
Fundamental Properties of a Good Hash Function
- Determinism within a table: Feeding two equal keys (
k1 == k2) into identically seededHasherinstances must always produce the exact sameu64output. - Uniform distribution (Avalanche effect): Flipping a single bit in the input key changes roughly 50% of the output bits, spreading similar keys (
"id_1","id_2","id_3") evenly across all buckets. - Prefix-collision resistance: Variable-length types like
strand[T]write a terminator byte (0xFFinstr::hash) or their length (slice.len()) into theHasherso that("ab", "c")and("a", "bc")do not produce identical byte streams.