DSA Hashing
Hash Maps
At n = 10⁵ line items per checkout, each “have we priced this SKU?” scan is Θ(n). Nested scans become ~10¹⁰ comparisons. A map from SKU → price turns each lookup into under a healthy .
A hash map stores a partial function from unique keys to values. Hashing narrows the search; equality finishes it. are normal. Worst case remains Θ(n) when the table degenerates.
What happens first?
Association contract
One current value per equality-distinct key.
Stable keys; equal ⇒ same hash.
Separate missing from stored null.
Average vs worst
Expected O(1); Θ(n) under collisions.
What actually happens
Naive idea: keep a list of (key, value) pairs and scan for each query. Correct. At n = 10⁵ with n queries, that is Θ(n²) work.
Better idea: hash the key to a bucket, resolve , confirm with equality, then read or replace the value. Keep bounded so expected chain/probe length stays constant. Resizes amortize into expected O(1) inserts.
The map still fails when keys mutate after insert, when you treat get-null as absence, when adversarial keys force Θ(n) probes, or when you need sorted ranges (use a tree map instead).
Invariant
Every live entry is reachable by hashing its key and following the collision policy; equal keys share at most one association. The holds for every resident key. Load stays in the regime the implementation promises for expected O(1).
How to choose a hash map
1. Exact key → value, order secondary? Hash map. Need sorted keys or ranges? Balanced tree map.
2. Many membership-only checks? A hash set may suffice. Need counts, indices, or payloads? Stay with a map.
3. Can you keep keys hash-stable? Prefer immutable keys. Concurrent compound updates need atomic compute/merge, not contains-then-put.
Quantitative check
Build a map of n = 10⁵ SKUs, then answer 10⁵ lookups. Expected work is roughly a few hash+equality steps per op (~10⁶), versus ~10¹⁰ for nested list scans. If all keys collide into one bucket, each lookup becomes Θ(n) and total work collapses back toward Θ(n²). Load factor and hash quality are the difference.
Logical model
key → value, with one current value per equality-distinct key
Typical API
put · get · containsKey · remove · compute
Expected update
O(1), amortized across occasional table resizes
Worst case
O(n) when collision handling degenerates
Interactive hash map demo
Add key-value pairs and watch how they are stored in buckets.
Association semantics, step by step
The bucket demo shows physical storage. This trace focuses on observable behavior: insert versus update, present-null versus missing, compute-style updates, and removal.
Collection semantics laboratory
What does each API operation promise?
Follow logical state changes independently of buckets, capacity, and collision strategy.
start with {}
Step 1 / 8
Logical map state
size = 0
Rows show associations, not a promised iteration order.
Return / effect
size = 0
Contract
A map stores at most one value for each equality-distinct key.
| Query | containsKey | get | Meaning |
|---|---|---|---|
| Linus | false | null / default | missing |
| Grace | false | null / default | missing |
Key → value association
Directory lookup
Given a name (the key), retrieve a phone number (the value) without scanning every entry. The structure promises expected constant-time access when load and hashing stay healthy.
Why use it?
- Fast Lookups: O(1) average time to find a value.
- Flexible Keys: Keys can be strings, numbers, or any hashable type whose equality and hash remain stable while stored.
- Data Association: Perfect for representing real-world relationships (User ID → User Profile).
Limitations
- Order varies: the abstract structure is unordered; concrete implementations may document insertion order.
- Memory Overhead: Uses more memory than arrays to store the hash table structure.
- Key Uniqueness: Keys must be unique; duplicates overwrite the previous value.
Map API: similar names, different contracts
| Operation | If key is missing | If key is present | Use it when… |
|---|---|---|---|
| get(key) | error, null, or None: API-specific | returns the associated value | you only need to read |
| put(key, value) | inserts; size grows | replaces the old value | the new value is already known |
| containsKey(key) | false | true, even for stored null | presence and value must be separated |
| remove(key) | no-op, false, or error: API-specific | deletes and may return old value | the association must disappear |
| compute / merge | derives an initial value | atomically derives an update in supporting maps | counting or grouping depends on old state |
Keys obey the same equality–hash contract
a == b ⇒ hash(a) == hash(b)
A hash chooses where to search; equality decides whether a candidate is the same key. Equal keys must hash equally, but unequal keys may share a hash. Collisions are normal and must not overwrite one another.
Why mutable keys break lookup
If a key's hash-relevant fields change after insertion, it remains physically stored under its old hash. A lookup using its new hash visits a different location. Use immutable value objects, stable IDs, or defensive copies as keys.
Implementation
Python
from collections import defaultdict
words = ["to", "be", "or", "not", "to", "be"]
# Frequency map: missing keys start at zero.
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
# Index/grouping map: missing keys start with a list.
positions = defaultdict(list)
for index, word in enumerate(words):
positions[word].append(index)
# Presence is separate from the stored value.
cache = {"report": None}
assert cache.get("report") is None
assert cache.get("missing") is None
assert "report" in cache and "missing" not in cache
# Python dict preserves insertion order.
for key, value in counts.items():
print(key, value)Java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
List<String> words = List.of("to", "be", "or", "not", "to", "be");
// LinkedHashMap matches Python dict's insertion-order iteration.
Map<String, Integer> counts = new LinkedHashMap<>();
for (String word : words) {
counts.merge(word, 1, Integer::sum);
}
Map<String, List<Integer>> positions = new LinkedHashMap<>();
for (int i = 0; i < words.size(); i++) {
positions.computeIfAbsent(words.get(i), key -> new ArrayList<>())
.add(i);
}
Map<String, String> cache = new LinkedHashMap<>();
cache.put("report", null);
assert cache.get("report") == null;
assert cache.get("missing") == null;
assert cache.containsKey("report");
assert !cache.containsKey("missing");
for (Map.Entry<String, Integer> entry : counts.entrySet()) {
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
}Missing is not the same as stored null
{"job": null}
Present, value is null
The key participates in size and iteration. It may mean “loaded, and no result.”
{}
Key is absent
There is no association. In a cache this might mean “not loaded yet.”
containsKey(key)
Ask the right question
Use a presence test or found flag. An Option/Optional works when null is not a legal payload; otherwise use a distinct sentinel or a result that carries presence separately.
API details matter: Java HashMap.get returns null in both cases, while Python dict[key] raises for absence. Java's computeIfAbsent also treats a null mapping as absent.
Patterns that naturally produce a map
Frequency
item → count
Anagrams, mode, sliding-window validity, multiset comparison
Index
value → index / indices
Two-sum complements, inverted indexes, last-seen positions
Grouping
group key → list
Anagram buckets, adjacency lists, records partitioned by owner
Memoization
state → answer
Top-down DP and expensive pure-function caching
Prefix-state lookup
prefix sum → earliest index / count
Subarray sums, especially when negative values prevent two pointers
Entity cache
stable ID → object
Identity maps, request deduplication, database hydration
Iteration order is a separate feature
| Map family | Order guarantee | Core cost | Choose it for |
|---|---|---|---|
| Hash map | Implementation-specific or unspecified | Expected O(1) | Fast general lookup |
| Insertion-ordered hash map | First insertion order; update rules vary | Expected O(1) | Stable serialization and predictable iteration |
| Access-ordered hash map | Most/least recently accessed | Expected O(1) | LRU bookkeeping |
| Balanced-tree map | Sorted by key/comparator | O(log n) worst case | Range queries, floor/ceiling, ordered traversal |
Python dictionaries preserve insertion order. Java HashMap does not; LinkedHashMap adds insertion/access order, while TreeMap provides sorted keys. Rely only on guarantees documented by the exact implementation.
Complexity ledger
| Operation | Expected / amortized | Worst case | Notes |
|---|---|---|---|
| get / containsKey | O(1) | O(n) | Hash plus probe/chain equality checks |
| put / update | O(1) amortized | O(n) | May trigger an O(n) resize/rebuild |
| remove | O(1) | O(n) | May repair a probe cluster or unlink a chain node |
| iterate entries | Θ(n) | Θ(n + capacity) | Open-addressed tables may scan empty slots |
| build n mappings | O(n) | O(n²) | Assumes expected-constant hashing per insertion |
These bounds treat key hashing/equality as constant-time. Hashing a long uncached string or comparing large composite keys adds their own cost.
Caching and concurrency require stronger contracts
A map is storage, not a complete cache
- Define eviction: capacity, LRU/LFU, TTL, or explicit invalidation.
- Define whether null means a cached negative result or absence.
- Prevent cache stampedes when many callers miss the same key.
- Account for mutable cached values and memory retention.
Compound operations must be atomic
if missing → compute → put is a race even if each individual operation is thread-safe. Use the concurrent map's atomic compute, merge, or putIfAbsent contract, or protect the whole transition with a lock. Iteration may be snapshot, fail-fast, or weakly consistent; verify the API. Java's ConcurrentHashMap, for example, rejects null keys and values, which removes missing-versus-null ambiguity.
Common failure modes
- Mutable keys: changing a hash-relevant field makes the mapping unreachable by normal lookup.
- Accidental overwrite:
puton an existing key updates; use put-if-absent when replacement is a bug. - Shared mutable defaults: create a fresh list/set per key when grouping.
- Order assumptions: deterministic-looking output is not a guarantee.
- Adversarial collisions: expected O(1) can become O(n); runtime defenses and key design matter.
- Oversized keys: composite/string hashing and equality may dominate the table operation.
Exercises
- A cache stores
report → nullmeaning “loaded, empty.” How do you distinguish that from a missing key in JavaHashMapvs Pythondict? - n = 10⁵ inserts into a table that never resizes (tiny fixed capacity). What happens to average probe length as load factor grows past 1 for chaining? For open addressing?
- Construct a failure timeline: insert a mutable key, change a hash field, then lookup. Where is the entry physically, and what does containsKey return?
Primary sources
- Cormen et al., Introduction to Algorithms (CLRS): hash tables, chaining, open addressing, load factor.
- Java
Map/HashMapdocumentation: null keys/values, compute, and iteration contracts. - Python data model:
__hash__/__eq__and dict insertion-order guarantees.
Takeaway
Use a hash map for fast key → value association when order and ranges are secondary. Expected O(1) assumes healthy load and hashing; worst case is Θ(n). Define absence explicitly, keep keys hash-stable, and choose an ordered or concurrent variant when those guarantees are required.