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
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.
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 inputbucket 0
bucket 1
bucket 2
bucket 3
bucket 4
5 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain.
1class ChainedHashSet {2private final int m, variant, a; // m buckets3private final LinkedList<Integer>[] table; // one chain per bucket4int hash(int key) { // the option picks one variant5switch (variant) {6case 0: return Math.floorMod(key, m);7case 1: return (int) (m * ((key * 0.6180339887) % 1.0));8case 2: return 0; // worst case: every key collides9default: return ((a * key + 7) % 101) % m; // universal-style family10}11}12boolean add(int key) {13LinkedList<Integer> chain = table[hash(key)];14if (chain.contains(key)) return false; // walk the chain: duplicate15chain.add(key); return true; // append to the chain16}17boolean contains(int key) { return table[hash(key)].contains(key); }18boolean remove(int key) { return table[hash(key)].remove((Integer) key); }19}
Live state
size
0
load factor
0.00
longest chain
0
Execution trace (dry run table)Show 30 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 load factor=0.00 longest chain=0 | 5 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain. |
| 2 | L6 | size=0 load factor=0.00 longest chain=0 op=add(12) hash=2 | add(12): hash = 12 % 5 = 2, so go to bucket 2. |
| 3 | L15 | size=1 load factor=0.20 longest chain=1 op=add(12) return=true | 12 is not in the chain (the bucket was empty): append it to bucket 2. |
| 4 | L6 | size=1 load factor=0.20 longest chain=1 op=add(7) hash=2 | add(7): hash = 7 % 5 = 2, so go to bucket 2. |
| 5 | L14 | size=1 load factor=0.20 longest chain=1 op=add(7) hash=2 | 12 ≠ 7: keep walking the chain. |
| 6 | L15 | size=2 load factor=0.40 longest chain=2 op=add(7) return=true | 7 is not in the chain (1 comparison): append it to bucket 2. |
| 7 | L6 | size=2 load factor=0.40 longest chain=2 op=add(22) hash=2 | add(22): hash = 22 % 5 = 2, so go to bucket 2. |
| 8 | L14 | size=2 load factor=0.40 longest chain=2 op=add(22) hash=2 | 12 ≠ 22: keep walking the chain. |
| 9 | L14 | size=2 load factor=0.40 longest chain=2 op=add(22) hash=2 | 7 ≠ 22: keep walking the chain. |
| 10 | L15 | size=3 load factor=0.60 longest chain=3 op=add(22) return=true | 22 is not in the chain (2 comparisons): append it to bucket 2. |
| 11 | L6 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | add(17): hash = 17 % 5 = 2, so go to bucket 2. |
| 12 | L14 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | 12 ≠ 17: keep walking the chain. |
| 13 | L14 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | 7 ≠ 17: keep walking the chain. |
| 14 | L14 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | 22 ≠ 17: keep walking the chain. |
| 15 | L15 | size=4 load factor=0.80 longest chain=4 op=add(17) return=true | 17 is not in the chain (3 comparisons): append it to bucket 2. |
| 16 | L6 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | contains(22): hash = 22 % 5 = 2, so go to bucket 2. |
| 17 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | 12 ≠ 22: keep walking the chain. |
| 18 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | 7 ≠ 22: keep walking the chain. |
| 19 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | 22 equals 22: found after 3 comparisons. |
| 20 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) return=true | contains(22) is true. |
| 21 | L6 | size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2 | remove(7): hash = 7 % 5 = 2, so go to bucket 2. |
| 22 | L18 | size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2 | 12 ≠ 7: keep walking the chain. |
| 23 | L18 | size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2 | 7 equals 7: found after 2 comparisons. |
| 24 | L18 | size=3 load factor=0.60 longest chain=3 op=remove(7) return=true | 7 unlinked from bucket 2. |
| 25 | L6 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | add(32): hash = 32 % 5 = 2, so go to bucket 2. |
| 26 | L14 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | 12 ≠ 32: keep walking the chain. |
| 27 | L14 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | 22 ≠ 32: keep walking the chain. |
| 28 | L14 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | 17 ≠ 32: keep walking the chain. |
| 29 | L15 | size=4 load factor=0.80 longest chain=4 op=add(32) return=true | 32 is not in the chain (3 comparisons): append it to bucket 2. |
| 30 | L19 | size=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. |
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.
| Type | hashCode() | Example |
|---|---|---|
| Integer | the value itself | Integer.hashCode(42) = 42 |
| Long | (int)(v ^ (v >>> 32)) | Long.hashCode(1L << 32) = 1 |
| Boolean | 1231 for true, 1237 for false | Boolean.hashCode(true) = 1231 |
| Character | the char code | 'a' → 97 |
| String | s[0]*31^(n-1) + … + s[n-1] | "hello".hashCode() = 99162322 |
| List.of(1, 2) | 31*(31*1 + 1) + 2 | 994 |
1static int stringHash(String s) {2int h = 0;3for (int i = 0; i < s.length(); i++) h = 31 * h + s.charAt(i); // overflow wraps; that is intended4return h; // same as s.hashCode()5}
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.
1static int bucketIndex(Object key, int tableLength) { // tableLength is a power of two2int h = key.hashCode();3int spread = h ^ (h >>> 16); // mix high bits into low bits4return spread & (tableLength - 1); // same as floorMod(spread, tableLength)5}
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.
1static final class Point {2final int x, y;3Point(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 equals6}
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.
1static final class Pair {2final int a, b;3Pair(int a, int b) { this.a = a; this.b = b; }4@Override public boolean equals(Object o) { // the parameter type must be Object5if (this == o) return true;6if (!(o instanceof Pair)) return false; // also handles null7Pair p = (Pair) o;8return a == p.a && b == p.b;9}10@Override public int hashCode() { return 31 * a + b; }11}
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 inputbucket 0
bucket 1
bucket 2
bucket 3
bucket 4
5 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain.
1class ChainedHashSet {2private final int m, variant, a; // m buckets3private final LinkedList<Integer>[] table; // one chain per bucket4int hash(int key) { // the option picks one variant5switch (variant) {6case 0: return Math.floorMod(key, m);7case 1: return (int) (m * ((key * 0.6180339887) % 1.0));8case 2: return 0; // worst case: every key collides9default: return ((a * key + 7) % 101) % m; // universal-style family10}11}12boolean add(int key) {13LinkedList<Integer> chain = table[hash(key)];14if (chain.contains(key)) return false; // walk the chain: duplicate15chain.add(key); return true; // append to the chain16}17boolean contains(int key) { return table[hash(key)].contains(key); }18boolean remove(int key) { return table[hash(key)].remove((Integer) key); }19}
Live state
size
0
load factor
0.00
longest chain
0
Execution trace (dry run table)Show 30 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 load factor=0.00 longest chain=0 | 5 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain. |
| 2 | L6 | size=0 load factor=0.00 longest chain=0 op=add(12) hash=2 | add(12): hash = 12 % 5 = 2, so go to bucket 2. |
| 3 | L15 | size=1 load factor=0.20 longest chain=1 op=add(12) return=true | 12 is not in the chain (the bucket was empty): append it to bucket 2. |
| 4 | L6 | size=1 load factor=0.20 longest chain=1 op=add(7) hash=2 | add(7): hash = 7 % 5 = 2, so go to bucket 2. |
| 5 | L14 | size=1 load factor=0.20 longest chain=1 op=add(7) hash=2 | 12 ≠ 7: keep walking the chain. |
| 6 | L15 | size=2 load factor=0.40 longest chain=2 op=add(7) return=true | 7 is not in the chain (1 comparison): append it to bucket 2. |
| 7 | L6 | size=2 load factor=0.40 longest chain=2 op=add(22) hash=2 | add(22): hash = 22 % 5 = 2, so go to bucket 2. |
| 8 | L14 | size=2 load factor=0.40 longest chain=2 op=add(22) hash=2 | 12 ≠ 22: keep walking the chain. |
| 9 | L14 | size=2 load factor=0.40 longest chain=2 op=add(22) hash=2 | 7 ≠ 22: keep walking the chain. |
| 10 | L15 | size=3 load factor=0.60 longest chain=3 op=add(22) return=true | 22 is not in the chain (2 comparisons): append it to bucket 2. |
| 11 | L6 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | add(17): hash = 17 % 5 = 2, so go to bucket 2. |
| 12 | L14 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | 12 ≠ 17: keep walking the chain. |
| 13 | L14 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | 7 ≠ 17: keep walking the chain. |
| 14 | L14 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | 22 ≠ 17: keep walking the chain. |
| 15 | L15 | size=4 load factor=0.80 longest chain=4 op=add(17) return=true | 17 is not in the chain (3 comparisons): append it to bucket 2. |
| 16 | L6 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | contains(22): hash = 22 % 5 = 2, so go to bucket 2. |
| 17 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | 12 ≠ 22: keep walking the chain. |
| 18 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | 7 ≠ 22: keep walking the chain. |
| 19 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | 22 equals 22: found after 3 comparisons. |
| 20 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) return=true | contains(22) is true. |
| 21 | L6 | size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2 | remove(7): hash = 7 % 5 = 2, so go to bucket 2. |
| 22 | L18 | size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2 | 12 ≠ 7: keep walking the chain. |
| 23 | L18 | size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2 | 7 equals 7: found after 2 comparisons. |
| 24 | L18 | size=3 load factor=0.60 longest chain=3 op=remove(7) return=true | 7 unlinked from bucket 2. |
| 25 | L6 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | add(32): hash = 32 % 5 = 2, so go to bucket 2. |
| 26 | L14 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | 12 ≠ 32: keep walking the chain. |
| 27 | L14 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | 22 ≠ 32: keep walking the chain. |
| 28 | L14 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | 17 ≠ 32: keep walking the chain. |
| 29 | L15 | size=4 load factor=0.80 longest chain=4 op=add(32) return=true | 32 is not in the chain (3 comparisons): append it to bucket 2. |
| 30 | L19 | size=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. |
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 inputbucket 0
bucket 1
bucket 2
bucket 3
bucket 4
5 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain.
1class ChainedHashSet {2private final int m, variant, a; // m buckets3private final LinkedList<Integer>[] table; // one chain per bucket4int hash(int key) { // the option picks one variant5switch (variant) {6case 0: return Math.floorMod(key, m);7case 1: return (int) (m * ((key * 0.6180339887) % 1.0));8case 2: return 0; // worst case: every key collides9default: return ((a * key + 7) % 101) % m; // universal-style family10}11}12boolean add(int key) {13LinkedList<Integer> chain = table[hash(key)];14if (chain.contains(key)) return false; // walk the chain: duplicate15chain.add(key); return true; // append to the chain16}17boolean contains(int key) { return table[hash(key)].contains(key); }18boolean remove(int key) { return table[hash(key)].remove((Integer) key); }19}
Live state
size
0
load factor
0.00
longest chain
0
Execution trace (dry run table)Show 30 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | size=0 load factor=0.00 longest chain=0 | 5 empty buckets. A hash function maps every key to a bucket index; keys that land in the same bucket share a chain. |
| 2 | L6 | size=0 load factor=0.00 longest chain=0 op=add(12) hash=2 | add(12): hash = 12 % 5 = 2, so go to bucket 2. |
| 3 | L15 | size=1 load factor=0.20 longest chain=1 op=add(12) return=true | 12 is not in the chain (the bucket was empty): append it to bucket 2. |
| 4 | L6 | size=1 load factor=0.20 longest chain=1 op=add(7) hash=2 | add(7): hash = 7 % 5 = 2, so go to bucket 2. |
| 5 | L14 | size=1 load factor=0.20 longest chain=1 op=add(7) hash=2 | 12 ≠ 7: keep walking the chain. |
| 6 | L15 | size=2 load factor=0.40 longest chain=2 op=add(7) return=true | 7 is not in the chain (1 comparison): append it to bucket 2. |
| 7 | L6 | size=2 load factor=0.40 longest chain=2 op=add(22) hash=2 | add(22): hash = 22 % 5 = 2, so go to bucket 2. |
| 8 | L14 | size=2 load factor=0.40 longest chain=2 op=add(22) hash=2 | 12 ≠ 22: keep walking the chain. |
| 9 | L14 | size=2 load factor=0.40 longest chain=2 op=add(22) hash=2 | 7 ≠ 22: keep walking the chain. |
| 10 | L15 | size=3 load factor=0.60 longest chain=3 op=add(22) return=true | 22 is not in the chain (2 comparisons): append it to bucket 2. |
| 11 | L6 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | add(17): hash = 17 % 5 = 2, so go to bucket 2. |
| 12 | L14 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | 12 ≠ 17: keep walking the chain. |
| 13 | L14 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | 7 ≠ 17: keep walking the chain. |
| 14 | L14 | size=3 load factor=0.60 longest chain=3 op=add(17) hash=2 | 22 ≠ 17: keep walking the chain. |
| 15 | L15 | size=4 load factor=0.80 longest chain=4 op=add(17) return=true | 17 is not in the chain (3 comparisons): append it to bucket 2. |
| 16 | L6 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | contains(22): hash = 22 % 5 = 2, so go to bucket 2. |
| 17 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | 12 ≠ 22: keep walking the chain. |
| 18 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | 7 ≠ 22: keep walking the chain. |
| 19 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) hash=2 | 22 equals 22: found after 3 comparisons. |
| 20 | L17 | size=4 load factor=0.80 longest chain=4 op=contains(22) return=true | contains(22) is true. |
| 21 | L6 | size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2 | remove(7): hash = 7 % 5 = 2, so go to bucket 2. |
| 22 | L18 | size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2 | 12 ≠ 7: keep walking the chain. |
| 23 | L18 | size=4 load factor=0.80 longest chain=4 op=remove(7) hash=2 | 7 equals 7: found after 2 comparisons. |
| 24 | L18 | size=3 load factor=0.60 longest chain=3 op=remove(7) return=true | 7 unlinked from bucket 2. |
| 25 | L6 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | add(32): hash = 32 % 5 = 2, so go to bucket 2. |
| 26 | L14 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | 12 ≠ 32: keep walking the chain. |
| 27 | L14 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | 22 ≠ 32: keep walking the chain. |
| 28 | L14 | size=3 load factor=0.60 longest chain=3 op=add(32) hash=2 | 17 ≠ 32: keep walking the chain. |
| 29 | L15 | size=4 load factor=0.80 longest chain=4 op=add(32) return=true | 32 is not in the chain (3 comparisons): append it to bucket 2. |
| 30 | L19 | size=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.