Average-Case and Worst-Case Complexity
HashMap<K, V> and HashSet<T> provide $O(1)$ expected average-case time for lookup, insertion, and removal, while their worst-case time is $O(n)$ when either a table resize (rehash) occurs or all keys collide into the same probe chain.
Complete Complexity Summary for HashMap<K, V> and HashSet<T>
($n$ = number of elements currently in the map/set; $c$ = allocated capacity.)
| Operation | Method | Average Case | Worst Case | Cause of Worst Case |
|---|---|---|---|---|
| Lookup | get(&k), contains_key(&k), contains(&x) |
$O(1)$ | $O(n)$ | All $n$ keys collide into a single probe chain |
| Insert | insert(k, v), entry(k).or_insert(v) |
Amortized $O(1)$ | $O(n)$ | Table reaches $7/8$ load factor and rehashes all $n$ elements (or $n$-way collision) |
| Remove | remove(&k), remove_entry(&k), take(&x) |
$O(1)$ | $O(n)$ | Probing past an $O(n)$ collision cluster (never shrinks table) |
| Clear | clear() |
$O(n)$ (or $O(c)$ control reset) | $O(c + n)$ | Drops $n$ elements and resets control bytes to EMPTY |
| Iteration | iter(), keys(), values(), retain() |
$O(c)$ | $O(c)$ | Scans control bytes across the allocated capacity $c$ |
| Reserve | reserve(additional) |
$O(n)$ (when resizing) | $O(n)$ | Allocates new table and re-inserts all $n$ elements |
Why Iteration Is $O(\text{capacity})$ Rather Than $O(\text{len})$
Because open-addressed hash tables scatter elements across the allocated bucket array, iterating over a HashMap scans the control-byte array (16 bytes at a time via SIMD) to find occupied slots.
- If a
HashMaphaslen = 10andcapacity = 14, iteration is near-instantaneous. - However, if a
HashMapgrew to $1,000,000$ elements (capacity$\approx 1.8\text{M}$) and you removed $999,995$ elements via.remove(), itslen()is5but itscapacity()is still $1.8\text{M}$—making.iter()or.retain()scan $1.8\text{M}$ control bytes! Callmap.shrink_to_fit()after massive removals if the map will be iterated repeatedly:
use std::collections::HashMap;
fn main() {
let mut map: HashMap<u32, u32> = (0..1_000).map(|i| (i, i)).collect();
println!("Peak: len={:>4}, cap={:>4}", map.len(), map.capacity());
// Remove 995 elements:
map.retain(|&k, _| k < 5);
println!("After prune: len={:>4}, cap={:>4} (still large!)", map.len(), map.capacity());
// Shrink capacity so future `.iter()` calls scan only a tiny table:
map.shrink_to_fit();
println!("After shrink: len={:>4}, cap={:>4}", map.len(), map.capacity());
}
Peak: len=1000, cap=1792
After prune: len= 5, cap=1792 (still large!)
After shrink: len= 5, cap= 7
Guaranteed Worst-Case Bounds: When to Prefer BTreeMap
In hard real-time systems or adversarial environments where even a single $O(n)$ rehash pause or custom-hasher collision spike is unacceptable, BTreeMap<K, V> and BTreeSet<T> guarantee a strict $O(\log n)$ worst-case bound on every single get, insert, and remove operation without ever performing a full-table rehash.