3.3.9 · D4Hashing

Exercises — Python dict and set internals

2,531 words12 min readBack to topic

Level 1 — Recognition

Recall Solution L1.1

Use with , so (keep the low 4 bits). . The low 4 bits are . Check against remainder: . ✓ Answer: slot 4.

Recall Solution L1.2

(b) keep probing. A DUMMY means a key that once lived here was deleted. If we stopped, any key whose probe chain passed through this slot would be wrongly reported missing. Only an EMPTY slot ends an unsuccessful search. Answer: keep probing (skip the dummy).

Recall Solution L1.3

A key must be hashable, which in practice means immutable enough that its hash never changes. 42 ✓, "hi" ✓, (1, 2) ✓ (tuple of hashables), frozenset({3}) ✓. The list [1, 2] is mutable → unhashable; hash([1,2]) raises TypeError. Answer: all except [1, 2].


Level 2 — Application

Recall Solution L2.1

(keep low 3 bits).

  • A: . Slot 3 empty → A goes to slot 3.
  • B: . , low 3 bits . Collision with A. Probe once with : , low 3 bits … wait, recompute: . Still slot 3? That is occupied, so probe again. Update perturb: . Slot 0 empty → B goes to slot 0. (Figure below traces this.)
Figure — Python dict and set internals
Recall Solution L2.2

Answer: 2 probes on average. Compare with the CPython cap , where .

Recall Solution L2.3

Threshold: . After the insert, . Test: is ? Yes. Answer: resize fires, growing to the next adequate power of two (16) and rehashing all keys.


Level 3 — Analysis

Recall Solution L3.1

The invariant is: if a == b then hash(a) == hash(b). Here 1 == 1.0 == True is True, and hash(1) == hash(1.0) == hash(True) == 1. So all three land at the same start slot and compare equal there. The table therefore treats them as the same key. Each assignment overwrites the value; the first key object (1) is retained but the last value ("c") wins. Answer: {1: "c"}.

Recall Solution L3.2

A search stops only at EMPTY. With just 2 EMPTY slots out of 8, a probe sequence may pass through many dummies (which look "occupied" to the searcher) before reaching an EMPTY slot. Effectively , giving probes despite only 2 live keys. Fix: counting dummies in pushes the table over , so it resizes and purges dummies, restoring short chains. This is exactly why CPython counts dummies in fill.

Recall Solution L3.3

Choose keys whose hashes all share the same low bits, e.g. hashes — every one has . Then every insert collides and the probe chain for the last key is length . Resizing changes , but if the adversary keeps supplying keys with matching low bits for the new too, the collisions persist. Resizing bounds (average), but it cannot repair a pathological hash distribution. Answer: worst case = O(n); resizing only guarantees the average.


Level 4 — Synthesis

Recall Solution L4.1

Mask . All three of P, Q, R have low 3 bits (), so they pile up starting at slot 1 — a clustering chain.

Insert P (): . Empty → P at slot 1.

Insert Q (): (occupied). Probe, : . Empty → Q at slot 7.

Insert R (): (occupied). Probe, : (occupied by Q). Update : . Empty → R at slot 4.

Delete P: slot 1 becomes DUMMY (not EMPTY).

Final: slot 1 = DUMMY, slot 4 = R, slot 7 = Q, rest EMPTY.

Search X (): → DUMMY, skip, keep probing (probe 1). : → Q, different key (probe 2). : → R, different key (probe 3). : → EMPTY → not found (probe 4). Answer: X takes 4 probes. (The DUMMY at slot 1 cost us a probe — the L3.2 effect in action.)

Figure — Python dict and set internals
Recall Solution L4.2

A resize rehashes all current keys: cost current size. Sizes at resize form a geometric series with . Total rehash work: Plus for the inserts themselves → total . Divide by inserts: amortized per insert. See Amortized Analysis and Big-O Notation.


Level 5 — Mastery

Recall Solution L5.1

Condition: . For : . The smallest integer strictly greater is 6. So the insert that raises from 5 to 6 triggers resize. General rule: resize on the insert making Check : . ✓ For : .

Recall Solution L5.2
0.25
0.5
0.667
0.9

At an unsuccessful search averages 10 probes — the "constant" in O(1) has ballooned. Capping at keeps it near 3, a good speed/memory trade. Pushing to 0.9 saves ~25% memory but triples-plus the probe count. Answer: CPython buys speed with a modest memory cost by capping .

Figure — Python dict and set internals
Recall Solution L5.3
  • Open addressing (CPython): a miss probes inside one shared array, so cost scales with (dummies inflate it — see L3.2). Because all probes hit the same contiguous array, the CPU cache prefetches neighbours → cache-friendly.
  • Separate chaining (Java): a miss walks the linked list (or tree) of one bucket, so cost scales with that bucket's node count. Nodes are heap-scattered → more cache misses, but deletes need no dummy markers (just unlink). See Java HashMap Internals and Collision Resolution. Answer: CPython miss-cost tracks ; Java tracks per-bucket length; open addressing wins on locality by staying in one array.

Recall Feynman recap

Every problem above is the same locker-wall from the parent note: the machine (hash) picks a start locker; if taken, a fixed jump rule (the perturbed probe) sends you to the next; a removed bag leaves a "was-here" sticky note (DUMMY) you must walk past; and when the wall gets two-thirds full the janitor builds a bigger wall and re-files everything (resize + rehash).