4. Hash Maps & Hash Sets

Covers custom-key hashing, insertion-order guarantees, default-value idioms, safe iteration-while-mutating, and sorted-map alternatives.

Hash maps and sets are the single most-used structure in coding interviews, but the four languages disagree sharply on identity vs. equality, iteration order, and which conveniences you get for free. This section is about wielding each one correctly and fast, not about how hashing works internally.

See roadmap: Arrays & Hashing

Language Verdict: Pros, Cons & Recommendation

Python

4/5
  • Counter gives free most_common(n), multiset arithmetic, and zero-KeyError counting out of the box
  • defaultdict(factory) auto-vivifies missing keys — the cleanest adjacency-list/grouping idiom of the three
  • Set algebra operators (&, |, -, ^) are the most readable of the three languages
  • @dataclass(frozen=True) makes a custom hashable key a one-line declaration
  • No built-in sorted-dict/sorted-set type — sorted(d.items()) re-sorts from scratch on every call
  • Mutating a dict's size mid-iteration raises RuntimeError immediately, so you must remember to iterate over a copy (list(d))
  • No way to pre-size or tune a dict's load factor for a known element count

Java

5/5
  • TreeMap/TreeSet give a real O(log n) sorted map with range queries (floorKey, ceilingKey) — no equivalent in Python or JS
  • computeIfAbsent/merge/getOrDefault cover every default-value idiom directly on the Map interface, no extra imports
  • record auto-generates a correct equals()/hashCode() pair for custom keys
  • Initial capacity and load factor are exposed constructor args, letting you pre-size to avoid resize-driven rehashing
  • No Counter equivalent — counting and top-N-by-frequency require manual merge() calls plus a hand-written comparator sort
  • Set operations (retainAll/addAll/removeAll) mutate the receiver in place — a missing defensive copy silently destroys the original set
  • Fail-fast iterators throw ConcurrentModificationException on any structural change except via Iterator.remove()

Go

4/5
  • map[K]V is a language primitive — no import, no boxing of int keys, comma-ok and delete built in
  • Structs of comparable fields are valid keys with no hashCode/equals override
  • Zero-value reads (m[k] returns 0/"" if missing) make counting m[k]++ a one-liner
  • A set is just map[T]struct{} — tiny, idiomatic, no separate type to remember
  • Range order is deliberately randomized — never rely on iteration order (the interview gotcha vs Python 3.7+ dict / JS Map)
  • No TreeMap / sorted-map in the stdlib; no defaultdict/Counter — grouping is if _, ok := m[k]; !ok { m[k] = ... }
  • Nil map panics on write; you must make (or a composite literal) before the first insert

JavaScript

3/5
  • Map/Set always preserve insertion order, with none of the plain-object integer-key reordering quirk
  • Set.prototype.union/intersection/difference are now standardized (Baseline 2024), removing the need to hand-roll them
  • ??= gives a lightweight auto-vivification idiom for plain-object default values
  • No TreeMap equivalent at all — sorted-key access means re-sorting entries on every pass or hand-rolling a balanced structure
  • No Counter/defaultdict — counting and grouping are fully manual read-then-write patterns
  • Mutating a Map during iteration doesn't throw — added keys are silently visited later and deleted keys silently skipped
  • Plain objects reorder integer-like string keys ahead of insertion order — a trap if you reach for {} instead of Map
Recommendation: Python remains the default for maps — Counter/defaultdict and insertion-ordered dicts are unmatched for interview speed. Go's maps are the next-best primitive: unboxed keys, comma-ok, no boxing, but **range order is randomized** (do not port a Python solution that iterates a dict and expects insertion order). Use Java's TreeMap/TreeSet when you actually need sorted keys. In JS, prefer Map over plain objects.

Coding Mechanics, Side by Side

Custom Objects as Map/Set Keys

Must-know

All four languages hash on some notion of equality — but they disagree wildly on what 'equal' means for a custom object, and getting it wrong silently corrupts lookups instead of throwing. Go at least fails at compile time if the key type isn't comparable.

from dataclasses import dataclass @dataclass(frozen=True) class Point: x: int y: int seen = set() seen.add(Point(1, 2)) print(Point(1, 2) in seen) # True

@dataclass(frozen=True) auto-generates __hash__ and __eq__ from the declared fields, so two instances with equal field values hash and compare equal. A plain (mutable) @dataclass generates __eq__ but sets __hash__ to None, making it unhashable — you must opt into frozen=True (or write __hash__/__eq__ by hand) to use it as a dict/set key. Plain tuples work too since they're already hashable by value.

Insertion Order Guarantees

Must-know
d = {} d["b"] = 1 d["a"] = 2 d["c"] = 3 print(list(d)) # ['b', 'a', 'c']

Insertion-order preservation for dict is a language guarantee as of Python 3.7 (it was a CPython implementation detail in 3.6). set gives NO ordering guarantee at all — iteration order depends on hash values and insertion history and should never be relied on.

JS-Only Quirk: Integer Keys Reorder in Plain Objects

Recommended

This is a JS-specific spec behavior with no real analog in Python, Java, or Go, included here because it's a genuine debugging trap when someone reaches for a plain {} instead of a Map.

d = {"10": "a", "2": "b", "x": "c"} print(list(d)) # ['10', '2', 'x'] - stays in insertion order

Python dict never reorders keys based on their content — a string key that happens to look like an integer is treated exactly like any other string. There's nothing to watch out for here.

Default-Value Idioms

Must-know
counts = {} counts["a"] = counts.get("a", 0) + 1 from collections import defaultdict graph = defaultdict(list) graph["a"].append("b") # no KeyError, auto-creates [] d = {} d.setdefault("a", []).append(1)

.get(k, default) is the read-only default; .setdefault(k, default) both reads and inserts the default if missing (returns the existing or newly-inserted value); collections.defaultdict(factory) moves the default-creation logic to construction time so every missing-key access auto-vivifies via factory() — the most common interview idiom for building adjacency lists or grouping maps.

Resizing & Load Factor

Optional
# CPython dict: grows once ~2/3 full; small dicts start with 8 slots. # No public API to pre-size or tune the load factor. d = dict() # can't hint an expected size

CPython's dict growth policy and resize threshold are implementation details, not part of the language spec, and there's no way to pre-size a dict/set in pure Python. Building one from an existing iterable (set(items)) can help the allocator but isn't a real capacity hint.

Iterating While Mutating

Must-know
d = {"a": 1, "b": 2} for k in d: d["c"] = 3 # RuntimeError: dictionary changed size during iteration # Safe pattern: iterate over a snapshot for k in list(d): d[k + "!"] = d[k]

Adding or removing keys during iteration raises RuntimeError: dictionary changed size during iteration immediately — Python fails fast and loudly. Merely mutating an existing key's VALUE (not adding/removing keys) is safe. The fix is always to iterate over a copy (list(d), list(d.items())).

Set Algebra: Union, Intersection, Difference

Recommended
a = {1, 2, 3} b = {2, 3, 4} print(a & b) # {2, 3} intersection print(a | b) # {1, 2, 3, 4} union print(a - b) # {1} difference print(a ^ b) # {1, 4} symmetric difference

All four operators return NEW sets and never mutate a or b. In-place variants exist too (a &= b, a |= b, etc.) if you want to mutate. This is the most ergonomic of the three languages for set math.

Counting Pattern (Frequency Maps)

Must-know
from collections import Counter counts = Counter("mississippi") print(counts["s"]) # 3 print(counts.most_common(2)) # [('i', 4), ('s', 4)]

Counter is a dict subclass purpose-built for counting: missing keys return 0 instead of raising KeyError (no defaultdict needed), it supports multiset arithmetic (c1 + c2, c1 - c2), and .most_common(n) gives sorted-by-frequency output for free — no manual sort step.

Sorted-Key Iteration: HashMap vs. TreeMap and Beyond

Optional
d = {"banana": 3, "apple": 5, "cherry": 1} for k, v in sorted(d.items()): print(k, v) # sorted by key, recomputed each time, O(n log n) # For repeated sorted access, a third-party lib fills the gap: # from sortedcontainers import SortedDict

Python has no built-in sorted-dict type. The idiomatic approach is sorted(d.items()) on demand — fine for a one-off pass, wasteful if you need repeated sorted access, in which case the well-known third-party sortedcontainers.SortedDict (O(log n) inserts, always-sorted iteration) is worth knowing exists, though you generally can't pip install it mid-interview.

Further Reading

  • Mapping Types — dictPython official docs

    The canonical reference for dict methods, including get/setdefault semantics and the insertion-order guarantee.

  • Covers Counter, defaultdict, and OrderedDict — the idiomatic building blocks for interview-style frequency maps and default-value dicts.

  • HashMap<K,V>Oracle Java Docs

    Documents the load factor, resize behavior, and the explicit warning that iteration order is unspecified.

  • Effective Java, 3rd Edition — Items 10-11 (equals/hashCode)Effective Java, 3rd Edition (Joshua Bloch)

    The definitive explanation of why equals() and hashCode() must be overridden together, and how records satisfy the contract automatically.

  • MapMDN

    Reference for Map's reference-identity key semantics and its insertion-order iteration guarantee.

  • Spec for the newly-standardized Set.prototype.union/intersection/etc., useful for knowing what's now native vs. still hand-rolled.

  • Official walkthrough of `make`, comma-ok, `delete`, ranging, and the fact that iteration order is randomized — the interview gotcha versus Python/JS ordered maps.