Separate Chaining
Collision resolution where each bucket holds a linked list (or small array) of all entries that hash to it.
Definition
In separate chaining the bucket array does not hold entries directly; each slot points to a chain — a Singly Linked List, a small Dynamic Array, or (Java 8+) a Red-Black Tree once the chain grows past 8 — containing every entry whose hash lands there.
Insert appends to (or updates within) the chain, lookup scans the chain comparing keys, delete unlinks a node. The table never "fills up": the load factor can exceed 1, only the chains grow longer.
Chaining is the default in Java HashMap, C++ std::unordered_map, and most textbook implementations because it is simple, tolerant of poor hashing, and makes deletion trivial.
Intuition
A mental model before the formal terms.
A row of mailboxes where each mailbox has a hook and every letter for that box is clipped onto a chain hanging from it. Finding a letter means going to the right mailbox and flipping through its chain — usually one or two letters.
How it works
- Allocate
mempty chains. put(k, v):idx = hash(k) mod m; walk chainidx; if a node has keyk, overwrite its value; else prepend or append a new node and incrementn.get(k): walk chainidxand return the first node with keyk.remove(k): walk chainidxwith a trailing pointer and unlink the matching node.- When
n / m > 0.75, allocate2mchains and move each node into its new chain (the node objects can be reused).
Why it works
Expected chain length is α = n / m under uniform hashing; with α ≤ 0.75 most chains hold 0 or 1 entries, so scans are O(1) expected.
Because entries never occupy each other's slots, there is no clustering effect and no need for tombstones — deletion is a plain unlink.
Treeifying long chains bounds the worst case at O(log n) even under hash flooding.
Operations
| Operation | Description | Cost |
|---|---|---|
| put(k, v) | Append/overwrite in the bucket chain. | O(1 + α) expected |
| get(k) | Scan the bucket chain. | O(1 + α) expected |
| remove(k) | Unlink from the bucket chain. | O(1 + α) expected |
| rehash | Redistribute all nodes into 2m chains. | O(n + m) |
Recognition
How to tell a problem wants this.
- You are asked to implement a hash map "with linked lists" or to handle collisions in the simplest robust way.
- The key/value payload is large or deletion is frequent.
- You want predictable behaviour when the hash function quality is uncertain.
Interactive demo
Play, step, change the input. ← → and space work too.
Showing the closely related Hash Table visualization.
1h = key % m2insert: scan bucket[h]; if key present: done, else append to the chain3search: scan bucket[h] comparing each key4delete: scan bucket[h]; unlink the matching nodePseudocode
1class ChainedHashMap:2 chains = array of m empty lists3 put(k, v): c = chains[h(k)]; for node in c: if node.key == k: node.val = v; return4 c.prepend(Node(k, v)); n++; if n > 0.75 * m: rehash()5 get(k): for node in chains[h(k)]: if node.key == k: return node.val; return null6 remove(k): unlink first node in chains[h(k)] with key k; n--Implementation
1from dataclasses import dataclass2from typing import Generic, Hashable, Optional, TypeVar3 4K = TypeVar("K", bound=Hashable)5V = TypeVar("V")6 7 8@dataclass9class Node(Generic[K, V]):10 key: K11 value: V12 next: "Optional[Node[K, V]]" = None13 14 15class ChainedMap(Generic[K, V]):16 """Separate chaining: each bucket is a singly linked list of nodes."""17 181 · Buckets of linked nodes19 def __init__(self, cap: int = 8) -> None:20 self._buckets: list[Optional[Node[K, V]]] = [None] * cap21 self._n = 022 23 def _index(self, key: K) -> int:24 return hash(key) % len(self._buckets)25 262 · Insert or update at the head of the chain27 def put(self, key: K, value: V) -> None:28 i = self._index(key)29 node = self._buckets[i]30 while node is not None:31 if node.key == key:32 node.value = value33 return34 node = node.next35 self._buckets[i] = Node(key, value, self._buckets[i])36 self._n += 137 if self._n > len(self._buckets): # load factor 1.038 self._rehash()39 403 · Walk the chain to find41 def get(self, key: K) -> Optional[V]:42 node = self._buckets[self._index(key)]43 while node is not None:44 if node.key == key:45 return node.value46 node = node.next47 return None48 494 · Unlink the node50 def remove(self, key: K) -> bool:51 i = self._index(key)52 prev: Optional[Node[K, V]] = None53 node = self._buckets[i]54 while node is not None:55 if node.key == key:56 if prev is None:57 self._buckets[i] = node.next58 else:59 prev.next = node.next60 self._n -= 161 return True62 prev, node = node, node.next63 return False64 65 def __len__(self) -> int:66 return self._n67 685 · Rehash into twice as many buckets69 def _rehash(self) -> None:70 old = self._buckets71 self._buckets = [None] * (len(old) * 2)72 for head in old:73 while head is not None:74 nxt = head.next75 i = self._index(head.key)76 head.next = self._buckets[i]77 self._buckets[i] = head # reuse the node, no allocation78 head = nxtNodeis a dataclass with a forward reference to its own type fornext.putwalks the chain with awhileloop; new nodes are prepended soself._buckets[i]becomes the new head.getreturnsNonewhen absent (mirroringdict.get).removetracksprevand relinks;prev, node = node, node.nextadvances both in one statement._rehashrelinks existing nodes into a bigger bucket list.
Python objects are heavy (~56 bytes per node); dict's compact open-addressing layout is far smaller.
- CPython dict does not use chaining; this class is for understanding, not production.
@dataclassgenerates__init__and__repr__; addslots=True(3.10+) to shrink nodes.- A list-of-lists bucket is simpler in Python and often faster because list ops are C-implemented.
- Comparing
node == Noneinstead ofnode is None. - Building buckets with
[[]] * cap(aliasing) — here[None] * capis safe because None is immutable. - Forgetting to save
head.nextbefore relinking in_rehash.
- C++ has a real singly linked list (
std::forward_list); JS/TS/Python build nodes by hand. - Memory: C++ nodes are compact structs; Python and JS nodes are full heap objects with much higher overhead.
- C++ unordered_map is chained; Python dict and V8 Map are not — chaining is a teaching model in those languages.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(1) | O(n) | O(log n) worst with treeified chains. |
| Search | O(1 + α) | O(n) | |
| Insert | O(1) | O(n) | Amortized; rehash is O(n). |
| Delete | O(1 + α) | O(n) | Plain unlink, no tombstones. |
| Update | O(1 + α) | O(n) | |
| Rehash | O(n + m) | O(n + m) | |
| Space | O(n + m) | One extra pointer per entry plus the bucket array. | |
Advantages & disadvantages
- Simple to implement and reason about; deletion is trivial.
- Degrades gracefully as the load factor rises past 1.
- Chains can be upgraded to trees to cap the worst case.
- Each entry costs an extra pointer (and often a separate heap allocation), hurting memory and cache locality.
- Pointer chasing through chains is slower than probing contiguous slots for small keys.
- Bucket array plus node allocations create more garbage-collector pressure.
Use cases
- Java
HashMap/HashSet, C++unordered_map, Gosync.Mapinternals. - Interview "design a hash map" implementations.
- Tables with large values or frequent deletes.
- Default choice when implementing a hash map by hand.
- Frequent deletions, large entries, or unknown hash quality.
- Load factors near or above 1 are acceptable to save memory.
- Entries are tiny (ints, pointers) and cache locality dominates — prefer Open Addressing.
- Allocation is expensive or forbidden (embedded, real-time) — open addressing needs one contiguous block.
Alternatives
Common mistakes
- Forgetting to check for an existing key before appending, creating duplicate entries in a chain.
- Losing the head pointer on delete when the matching node is the first in the chain.
- Rehashing by calling
puton each old node (allocates new nodes) instead of relinking existing ones. - Using
hash % mwith a negative hash in Java/C++/Go.
Interview patterns
- Design HashMap (LeetCode 706): array of linked lists with
put/get/remove. - Explain how Java
HashMaptreeifies a bin when its chain reaches 8 and the table has ≥ 64 buckets. - Compare memory per entry: chaining (key + value + next pointer + node header) vs. open addressing (key + value only).
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Hash map or array?Beginner
- Array versus linked listBeginner
- Two SumBeginner
- Longest Substring Without Repeating CharactersIntermediate
- Subarray Sum Equals KIntermediate