Chapter 14: Hash Tables, Maps and Sets
14.03Beginner / Intermediate6 topics17 min3 interactive animations

Hash table mechanics

What keeps a hash table fast: the load factor, rehashing, capacity growth, why lookups are expected O(1), the worst case, and the memory a table really uses.

Learning goals

  • Compute a load factor and decide when a table must grow
  • Trace a rehash and say why it is amortised O(1)
  • Distinguish expected, worst-case and amortised costs
  • Estimate the memory overhead of a Java HashMap

Prerequisites

Collision strategies
14.03.1

Load factor

The **load factor** is n / m: entries divided by buckets. It is the one number that predicts speed. For separate chaining it is the average chain length; for open addressing it is the fraction of slots in use and must stay below 1.

Java's HashMap resizes when size > 0.75 × capacity. A lower limit wastes memory but keeps chains short; a higher limit saves memory but costs more probes. Under the standard uniform-hashing assumption the expected cost of a lookup grows with the load factor α as shown here.

Load factor αChaining: unsuccessful searchChaining: successful searchLinear probing: unsuccessfulLinear probing: successful
0.500.50 comparisons1.252.5 probes1.5
0.750.751.3758.52.5
0.900.901.4550.55.5
14.03.2

Rehashing

When the load factor passes its limit, the table allocates a bigger array (usually twice the size) and **rehashes**: every key is hashed again with the new size and stored in its new bucket. Rehashing is necessary because the bucket index depends on the table size, so an entry cannot simply be copied to the same index.

Run the “Growing from 2 buckets” preset. Starting with 2 buckets and a limit of 0.75, the table doubles to 4, 8, 16 as keys arrive, and the final message counts the keys moved. Notice how keys that collided in the small table often separate after the resize.

Java
1private void resize() {
2 LinkedList<Integer>[] old = table;
3 table = newTable(old.length * 2); // double the number of buckets
4 for (LinkedList<Integer> chain : old)
5 for (int key : chain) table[Math.floorMod(key, table.length)].add(key); // every key is rehashed
6}

Load factor and rehashing

Interactive · edit the input

Your input

Try

table · 2 buckets

∅0
∅1
Setup

2 empty buckets and a maximum load factor of 0.75: the table doubles as soon as size > 0.75 × buckets.

Step 1 / 31
Java
1class ChainedSet {
2 private LinkedList<Integer>[] table; private int size; private final double maxLoad;
3 void add(int key) {
4 int b = Math.floorMod(key, table.length);
5 if (table[b].contains(key)) return; // already present
6 table[b].add(key); size++;
7 if (size > maxLoad * table.length) resize(); // load factor too high
8 }
9 private void resize() {
10 LinkedList<Integer>[] old = table;
11 table = newTable(old.length * 2); // double the number of buckets
12 for (LinkedList<Integer> chain : old)
13 for (int key : chain) table[Math.floorMod(key, table.length)].add(key); // every key is rehashed
14 }
15}

Live state

0 resizes0 keys moved

size

0

buckets

2

load factor

0.00

limit

0.75

No condition evaluated on this step
Execution trace (dry run table)Show 31 steps
StepLineStateWhat happens
1L2size=0 buckets=2 load factor=0.00 limit=0.752 empty buckets and a maximum load factor of 0.75: the table doubles as soon as size > 0.75 × buckets.
2L4size=0 buckets=2 load factor=0.00 limit=0.75 key=5 bucket=1add(5): 5 % 2 = 1.
3L7size=1 buckets=2 load factor=0.50 limit=0.75 key=5Added 5: 1 ≤ 1.5, no resize yet.
4L4size=1 buckets=2 load factor=0.50 limit=0.75 key=12 bucket=0add(12): 12 % 2 = 0.
5L7size=2 buckets=2 load factor=1.00 limit=0.75 key=12Added 12: 2 keys exceed the limit 1.5, so the table must grow.
6L11size=2 buckets=2 load factor=1.00 limit=0.75 new buckets=4Allocate a new array of 4 buckets. Bucket indices depend on the table size, so every key must be hashed again.
7L13size=2 buckets=2 load factor=1.00 limit=0.75 moving from=0 new buckets=4Rehash bucket 0 (12): each key goes to key % 4. Keys that collided before often separate now.
8L13size=2 buckets=2 load factor=1.00 limit=0.75 moving from=1 new buckets=4Rehash bucket 1 (5): each key goes to key % 4. Keys that collided before often separate now.
9L14size=2 buckets=4 load factor=0.50 limit=0.75Resize done: 4 buckets, load factor 0.50. Cost: O(n) now, but it happens after the table has doubled, so adding is O(1) amortised.
10L4size=2 buckets=4 load factor=0.50 limit=0.75 key=9 bucket=1add(9): 9 % 4 = 1.
11L7size=3 buckets=4 load factor=0.75 limit=0.75 key=9Added 9: 3 ≤ 3, no resize yet.
12L4size=3 buckets=4 load factor=0.75 limit=0.75 key=20 bucket=0add(20): 20 % 4 = 0.
13L7size=4 buckets=4 load factor=1.00 limit=0.75 key=20Added 20: 4 keys exceed the limit 3, so the table must grow.
14L11size=4 buckets=4 load factor=1.00 limit=0.75 new buckets=8Allocate a new array of 8 buckets. Bucket indices depend on the table size, so every key must be hashed again.
15L13size=4 buckets=4 load factor=1.00 limit=0.75 moving from=0 new buckets=8Rehash bucket 0 (12, 20): each key goes to key % 8. Keys that collided before often separate now.
16L13size=4 buckets=4 load factor=1.00 limit=0.75 moving from=1 new buckets=8Rehash bucket 1 (5, 9): each key goes to key % 8. Keys that collided before often separate now.
17L14size=4 buckets=8 load factor=0.50 limit=0.75Resize done: 8 buckets, load factor 0.50. Cost: O(n) now, but it happens after the table has doubled, so adding is O(1) amortised.
18L4size=4 buckets=8 load factor=0.50 limit=0.75 key=33 bucket=1add(33): 33 % 8 = 1.
19L7size=5 buckets=8 load factor=0.63 limit=0.75 key=33Added 33: 5 ≤ 6, no resize yet.
20L4size=5 buckets=8 load factor=0.63 limit=0.75 key=7 bucket=7add(7): 7 % 8 = 7.
21L7size=6 buckets=8 load factor=0.75 limit=0.75 key=7Added 7: 6 ≤ 6, no resize yet.
22L4size=6 buckets=8 load factor=0.75 limit=0.75 key=18 bucket=2add(18): 18 % 8 = 2.
23L7size=7 buckets=8 load factor=0.88 limit=0.75 key=18Added 18: 7 keys exceed the limit 6, so the table must grow.
24L11size=7 buckets=8 load factor=0.88 limit=0.75 new buckets=16Allocate a new array of 16 buckets. Bucket indices depend on the table size, so every key must be hashed again.
25L13size=7 buckets=8 load factor=0.88 limit=0.75 moving from=1 new buckets=16Rehash bucket 1 (9, 33): each key goes to key % 16. Keys that collided before often separate now.
26L13size=7 buckets=8 load factor=0.88 limit=0.75 moving from=2 new buckets=16Rehash bucket 2 (18): each key goes to key % 16. Keys that collided before often separate now.
27L13size=7 buckets=8 load factor=0.88 limit=0.75 moving from=4 new buckets=16Rehash bucket 4 (12, 20): each key goes to key % 16. Keys that collided before often separate now.
28L13size=7 buckets=8 load factor=0.88 limit=0.75 moving from=5 new buckets=16Rehash bucket 5 (5): each key goes to key % 16. Keys that collided before often separate now.
29L13size=7 buckets=8 load factor=0.88 limit=0.75 moving from=7 new buckets=16Rehash bucket 7 (7): each key goes to key % 16. Keys that collided before often separate now.
30L14size=7 buckets=16 load factor=0.44 limit=0.75Resize done: 16 buckets, load factor 0.44. Cost: O(n) now, but it happens after the table has doubled, so adding is O(1) amortised.
31L15size=7 buckets=16 load factor=0.44 limit=0.75 return=[[],[33],[18],[],[20],[5],[],[7],[],[9],[],[],[12],[],[],[]]7 keys in 16 buckets after 3 resizes; 13 key moves spent on rehashing.
14.03.3

Capacity growth

Capacity doubles on each resize. Doubling makes the growth geometric: after n insertions the total number of rehashed keys is at most about 2n, because each key is moved once per doubling and the sizes of earlier tables add up to less than the last one. That is why put is O(1) *amortised* even though a single put can cost O(n).

In Java the capacity of a HashMap is always a power of two (a request for 100 becomes 128) and a HashSet of 14 values added to a default table resizes once when the 13th value arrives (16 × 0.75 = 12). If you know the number of entries in advance, size the table to expected / 0.75 + 1 to avoid resizing at all. The animation shows exactly this for the 14-value preset.

Java
1static <K, V> Map<K, V> presized(int expectedEntries) {
2 int capacity = (int) (expectedEntries / 0.75f) + 1; // large enough that no resize happens
3 return new HashMap<>(capacity);
4}

Same values, different collections

Interactive · edit the input

Your input

Try

ArrayList

empty

LinkedHashSet

empty

HashSet (capacity 16)

empty

TreeSet

empty

PriorityQueue array

empty
Setup

Five collections receive exactly the same values in the same order. Watch how each one stores them.

Step 1 / 10
Java
1List<Integer> list = new ArrayList<>();
2Set<Integer> linked = new LinkedHashSet<>();
3Set<Integer> hash = new HashSet<>();
4Set<Integer> tree = new TreeSet<>();
5PriorityQueue<Integer> heap = new PriorityQueue<>();
6for (int x : arr) {
7 list.add(x); // keeps insertion order, keeps duplicates
8 linked.add(x); // keeps insertion order, drops duplicates
9 hash.add(x); // order decided by hash buckets
10 tree.add(x); // always sorted
11 heap.add(x); // heap array: only the smallest is guaranteed first
12}
13System.out.println(list + " " + linked + " " + hash + " " + tree + " " + heap);

Live state

values

8

No condition evaluated on this step
Execution trace (dry run table)Show 10 steps
StepLineStateWhat happens
1L1values=8Five collections receive exactly the same values in the same order. Watch how each one stores them.
2L7x=42 adds so far=1 distinct=1 HashSet capacity=1642 goes to bucket 10 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
3L7x=7 adds so far=2 distinct=2 HashSet capacity=167 goes to bucket 7 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
4L7x=19 adds so far=3 distinct=3 HashSet capacity=1619 goes to bucket 3 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
5L7x=7 adds so far=4 distinct=3 HashSet capacity=167 is already present: the ArrayList and the PriorityQueue keep a second copy, the three sets ignore it.
6L7x=3 adds so far=5 distinct=4 HashSet capacity=163 goes to bucket 3 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
7L7x=25 adds so far=6 distinct=5 HashSet capacity=1625 goes to bucket 9 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
8L7x=16 adds so far=7 distinct=6 HashSet capacity=1616 goes to bucket 0 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
9L7x=33 adds so far=8 distinct=7 HashSet capacity=1633 goes to bucket 1 of 16 in the HashSet (index = (h ^ (h >>> 16)) & 15).
10L13distinct=7 capacity=16Same values, five different orders. Only the TreeSet is sorted; the HashSet order is an implementation detail you must never depend on, and the PriorityQueue promises only that index 0 is the minimum.
14.03.4

Expected O(1)

A hash table promises **expected O(1)** lookups: if the hash function spreads keys uniformly and the load factor is bounded by a constant, a lookup scans a constant number of entries on average. Expected is a statement about the average over random keys or a random hash function, not a guarantee for one operation.

Three costs are easy to confuse: *amortised* O(1) (put, averaged over a sequence that includes resizes), *expected* O(1) (lookup, averaged over the randomness of the hash), and *worst-case* O(n) (all keys in one bucket). Hash code computation itself costs time too: hashing a string of length L is O(L), so lookups of long strings are not truly constant.

OperationExpectedWorst case
put / addO(1) amortisedO(n)
get / containsO(1)O(n), O(log n) with treeified bins
removeO(1)O(n)
iterate allO(n + m)O(n + m)
14.03.5

Worst-case behavior

If all keys hash to one bucket, the table is a linked list: every operation scans up to n entries. Pick the “Worst case: all collide” preset: the constant hash function puts all keys in bucket 0 and the search for the last key compares with every one before it.

Java mitigates this. Since Java 8 a HashMap bucket with 8 or more entries (in a table of at least 64 buckets) is converted into a red-black tree ordered by hash code and, for keys that implement Comparable, by compareTo. Lookups in such a bucket take O(log n) instead of O(n). Smaller tables resize instead of treeifying.

Hash table with separate chaining

Interactive · edit the input

Your input

Try

bucket 0

∅

bucket 1

∅

bucket 2

∅

bucket 3

∅

bucket 4

∅
Setup

5 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain.

Step 1 / 30
Java
1class ChainedHashSet {
2 private final int m, variant, a; // m buckets
3 private final LinkedList<Integer>[] table; // one chain per bucket
4 int hash(int key) { // the option picks one variant
5 switch (variant) {
6 case 0: return Math.floorMod(key, m);
7 case 1: return (int) (m * ((key * 0.6180339887) % 1.0));
8 case 2: return 0; // worst case: every key collides
9 default: return ((a * key + 7) % 101) % m; // universal-style family
10 }
11 }
12 boolean add(int key) {
13 LinkedList<Integer> chain = table[hash(key)];
14 if (chain.contains(key)) return false; // walk the chain: duplicate
15 chain.add(key); return true; // append to the chain
16 }
17 boolean contains(int key) { return table[hash(key)].contains(key); }
18 boolean remove(int key) { return table[hash(key)].remove((Integer) key); }
19}

Live state

0 key comparisons

size

0

load factor

0.00

longest chain

0

No condition evaluated on this step
Execution trace (dry run table)Show 30 steps
StepLineStateWhat happens
1L3size=0 load factor=0.00 longest chain=05 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain.
2L6size=0 load factor=0.00 longest chain=0 op=add(12) hash=2add(12): hash = 12 % 5 = 2, so go to bucket 2.
3L15size=1 load factor=0.20 longest chain=1 op=add(12) return=true12 is not in the chain (the bucket was empty): append it to bucket 2.
4L6size=1 load factor=0.20 longest chain=1 op=add(7) hash=2add(7): hash = 7 % 5 = 2, so go to bucket 2.
5L14size=1 load factor=0.20 longest chain=1 op=add(7) hash=212 ≠ 7: keep walking the chain.
6L15size=2 load factor=0.40 longest chain=2 op=add(7) return=true7 is not in the chain (1 comparison): append it to bucket 2.
7L6size=2 load factor=0.40 longest chain=2 op=add(22) hash=2add(22): hash = 22 % 5 = 2, so go to bucket 2.
8L14size=2 load factor=0.40 longest chain=2 op=add(22) hash=212 ≠ 22: keep walking the chain.
9L14size=2 load factor=0.40 longest chain=2 op=add(22) hash=27 ≠ 22: keep walking the chain.
10L15size=3 load factor=0.60 longest chain=3 op=add(22) return=true22 is not in the chain (2 comparisons): append it to bucket 2.
11L6size=3 load factor=0.60 longest chain=3 op=add(17) hash=2add(17): hash = 17 % 5 = 2, so go to bucket 2.
12L14size=3 load factor=0.60 longest chain=3 op=add(17) hash=212 ≠ 17: keep walking the chain.
13L14size=3 load factor=0.60 longest chain=3 op=add(17) hash=27 ≠ 17: keep walking the chain.
14L14size=3 load factor=0.60 longest chain=3 op=add(17) hash=222 ≠ 17: keep walking the chain.
15L15size=4 load factor=0.80 longest chain=4 op=add(17) return=true17 is not in the chain (3 comparisons): append it to bucket 2.
16L6size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2contains(22): hash = 22 % 5 = 2, so go to bucket 2.
17L17size=4 load factor=0.80 longest chain=4 op=contains(22) hash=212 ≠ 22: keep walking the chain.
18L17size=4 load factor=0.80 longest chain=4 op=contains(22) hash=27 ≠ 22: keep walking the chain.
19L17size=4 load factor=0.80 longest chain=4 op=contains(22) hash=222 equals 22: found after 3 comparisons.
20L17size=4 load factor=0.80 longest chain=4 op=contains(22) return=truecontains(22) is true.
21L6size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2remove(7): hash = 7 % 5 = 2, so go to bucket 2.
22L18size=4 load factor=0.80 longest chain=4 op=remove(7) hash=212 ≠ 7: keep walking the chain.
23L18size=4 load factor=0.80 longest chain=4 op=remove(7) hash=27 equals 7: found after 2 comparisons.
24L18size=3 load factor=0.60 longest chain=3 op=remove(7) return=true7 unlinked from bucket 2.
25L6size=3 load factor=0.60 longest chain=3 op=add(32) hash=2add(32): hash = 32 % 5 = 2, so go to bucket 2.
26L14size=3 load factor=0.60 longest chain=3 op=add(32) hash=212 ≠ 32: keep walking the chain.
27L14size=3 load factor=0.60 longest chain=3 op=add(32) hash=222 ≠ 32: keep walking the chain.
28L14size=3 load factor=0.60 longest chain=3 op=add(32) hash=217 ≠ 32: keep walking the chain.
29L15size=4 load factor=0.80 longest chain=4 op=add(32) return=true32 is not in the chain (3 comparisons): append it to bucket 2.
30L19size=4 load factor=0.80 longest chain=4 return=[[],[],[12,22,17,32],[],[]]4 keys in 5 buckets; the longest chain has 4. Lookup cost is the chain length, so the spread of the hash function decides the speed.
14.03.6

Memory overhead

A hash table spends memory on empty buckets and on bookkeeping. A Java HashMap stores each entry in a node object holding the cached hash, the key, the value and the next pointer, and boxes int keys and values as separate Integer objects. On a typical 64-bit JVM with compressed references that is roughly 32 bytes for the node, plus 16 bytes for each boxed Integer, plus 4 bytes per bucket in the array. A million Integer→Integer entries (keys beyond the small-value cache) therefore cost roughly 70 MB, compared with 4 MB for a plain int[].

This is why primitive-specialised or open-addressed maps (from libraries) can be several times smaller, and why a boolean[] or int[] count array beats a HashMap<Integer, Integer> when the keys form a small dense range. Measure with a tool such as JOL if the exact number matters.

ComponentApproximate size (64-bit, compressed references)
HashMap node (hash, key, value, next)32 bytes
Boxed Integer key or value16 bytes each
Table slot4 bytes
int in an int[]4 bytes

Practice ladder

Now try it yourself

Easy

  • Compute the load factor of 12 keys in 16 buckets
  • Why does HashMap use a power-of-two capacity?

Medium

  • Why is put amortised O(1) with doubling?
  • Design HashMap with automatic resizing

Advanced

  • Compare expected probes at load factors 0.5 and 0.9 for linear probing

Key takeaways

  • Load factor n/m predicts lookup cost; Java resizes at 0.75.
  • Rehashing doubles the table and re-hashes every key: O(n) once, O(1) amortised.
  • Expected O(1) assumes a good hash; the worst case is O(n), softened by treeified bins.