Chapter 14: Hash Tables, Maps and Sets
14.01Beginner / Intermediate7 topics18 min3 interactive animations

Hashing basics

How hashing turns a key into an array index: key-value indexing, hash functions, buckets, the hashCode and equals contracts, how evenly keys spread, and what happens when two keys collide.

Learning goals

  • Explain how a hash table finds a key without scanning
  • State the hashCode/equals contract and show what breaks without it
  • Judge a hash function by how evenly it spreads keys
  • Describe what a collision is and why it is unavoidable

Prerequisites

Arrays and dynamic arraysLinked lists

A hash table is a coat check: you hand over a coat and get a numbered tag; later the tag tells the attendant exactly which hook to look at instead of searching every coat.

14.01.1

Key-value indexing

An array finds a value in O(1) when you know its index. A hash table gives *any* key an index: a **hash function** turns the key into a number, and the number modulo the table size is the **bucket** where the key's entry lives. Looking up a key repeats the same computation and goes straight to that bucket instead of scanning.

A map stores key-value pairs; a set stores keys only. Both use the same machinery. In the animation, add keys to a table of 5 buckets with key % 5 and watch where each one lands, then search with ?22 and remove with -7.

Best

O(1)

Average

O(1)

Worst

O(n)

Space

O(n)

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.01.2

Hash functions

A good hash function is **deterministic** (the same key always gives the same number), **fast**, and **uniform** (keys spread evenly over the buckets). The simplest function for integers is key % m. For strings Java uses a polynomial: s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1], computed in int arithmetic that silently overflows and wraps.

Other ideas: the multiplicative method floor(m * frac(k * A)) with A ≈ 0.618 (the golden ratio) scrambles patterns in the keys, and universal hashing picks the function at random from a family. The animation lets you choose between key % m, the multiplicative method, a constant function and a universal-style function.

TypehashCode()Example
Integerthe value itselfInteger.hashCode(42) = 42
Long(int)(v ^ (v >>> 32))Long.hashCode(1L << 32) = 1
Boolean1231 for true, 1237 for falseBoolean.hashCode(true) = 1231
Characterthe char code'a' → 97
Strings[0]*31^(n-1) + … + s[n-1]"hello".hashCode() = 99162322
List.of(1, 2)31*(31*1 + 1) + 2994
Java
1static int stringHash(String s) {
2 int h = 0;
3 for (int i = 0; i < s.length(); i++) h = 31 * h + s.charAt(i); // overflow wraps; that is intended
4 return h; // same as s.hashCode()
5}
14.01.3

Buckets

A bucket is one slot of the table's array. It may hold nothing, one entry, or several entries when keys collide. Java's HashMap uses an array whose length is always a power of two, so the bucket index is (n - 1) & hash instead of a slow division, and it first mixes the high bits into the low bits with h ^ (h >>> 16) so that keys differing only in their high bits do not all land in one bucket.

For "hello" the hash is 99162322; after the spread step it is 99163451, and in a 16-bucket table it lands in bucket 11.

Java
1static int bucketIndex(Object key, int tableLength) { // tableLength is a power of two
2 int h = key.hashCode();
3 int spread = h ^ (h >>> 16); // mix high bits into low bits
4 return spread & (tableLength - 1); // same as floorMod(spread, tableLength)
5}
14.01.4

HashCode contract

Every Java object has hashCode(). The contract has three rules: it returns the same value while the object's equality-relevant fields do not change; **if two objects are equal by equals, they must have the same hash code**; and unequal objects may share a hash code (it is only a hint). The second rule is the one that matters for hash tables, because a lookup goes to one bucket only.

If you override equals but not hashCode, two equal objects get different identity-based hashes and usually land in different buckets, so the table cannot find them. With a class BadPoint that overrides only equals, new BadPoint(1, 2).equals(new BadPoint(1, 2)) is true, yet after set.add(new BadPoint(1, 2)), set.contains(new BadPoint(1, 2)) is false. Adding hashCode fixes it.

Java
1static final class Point {
2 final int x, y;
3 Point(int x, int y) { this.x = x; this.y = y; }
4 @Override public boolean equals(Object o) { return o instanceof Point p && p.x == x && p.y == y; }
5 @Override public int hashCode() { return Objects.hash(x, y); } // must be consistent with equals
6}
14.01.5

Equals contract

equals must be an equivalence relation: **reflexive** (x.equals(x)), **symmetric** (x.equals(y) iff y.equals(x)), **transitive**, **consistent** (same answer while nothing changes) and x.equals(null) is false. A hash table relies on it to decide, inside a bucket, which entry is *the* key.

The signature matters: equals(Object), not equals(Point). A method boolean equals(Point p) overloads instead of overriding, so collections never call it. Annotate with @Override and the compiler catches this.

Java
1static final class Pair {
2 final int a, b;
3 Pair(int a, int b) { this.a = a; this.b = b; }
4 @Override public boolean equals(Object o) { // the parameter type must be Object
5 if (this == o) return true;
6 if (!(o instanceof Pair)) return false; // also handles null
7 Pair p = (Pair) o;
8 return a == p.a && b == p.b;
9 }
10 @Override public int hashCode() { return 31 * a + b; }
11}
14.01.6

Distribution

The speed of a hash table depends on how evenly keys fill the buckets. With m buckets and n keys spread perfectly, every bucket has n/m keys and a lookup scans about that many entries. A poor function piles keys into a few buckets and the table degrades toward a linked list.

Compare presets in the animation with the keys 10, 20, 30, 40, 50 and 5 buckets: with key % 5 every key lands in bucket 0 (all are multiples of 5), but the multiplicative function spreads them. The data matters as much as the function: keys that share a factor with the table size are the classic trap, and one reason to use prime sizes or to mix bits first.

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.01.7

Collisions

A **collision** is two different keys mapping to the same bucket. It cannot be avoided: with more possible keys than buckets, the pigeonhole principle forces it, and even with few keys collisions appear surprisingly early (the birthday paradox: 23 random keys in 365 buckets have a better than even chance of at least one collision).

So every hash table needs a **collision strategy**. The two families are separate chaining (each bucket holds a list of entries) and open addressing (a colliding key is stored in another slot of the array). The next lesson covers both.

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.

Practice ladder

Now try it yourself

Easy

  • Compute 'cab'.hashCode() by hand
  • Why must equal objects have equal hash codes?

Medium

  • Design HashSet
  • Design HashMap

Advanced

  • Show that two different strings can have the same hashCode and build 1024 of them

Key takeaways

  • A hash function turns a key into a bucket index so lookup does not scan.
  • equals and hashCode must agree: equal objects need equal hash codes.
  • Collisions are unavoidable; the quality of the spread decides the speed.