Chapter 14: Hash Tables, Maps and Sets
14.08Beginner / Intermediate5 topics16 min3 interactive animations

Special hashes

Hashing beyond tables: rolling hashes for substring search, universal and perfect hashing, hash flooding attacks, and why equal hashes do not mean equal keys.

Learning goals

  • Update a rolling hash in O(1) when a window slides
  • Explain universal and perfect hashing at an introductory level
  • Describe a hash flooding attack and Java's defences
  • Keep collisions and equality separate

Prerequisites

Hashing basicsHash table mechanics
14.08.1

Rolling hash basics

A **rolling hash** hashes every window of a string in O(1) per slide instead of O(window). Treat the window as a number in base B: hash = c0·B^(m-1) + c1·B^(m-2) + … + c(m-1), taken modulo a large number. To slide, subtract the leaving character times B^(m-1), multiply by B and add the entering character.

**Rabin-Karp** uses this to search for a pattern: compute the pattern's hash, slide a window over the text, and compare characters only when the two hashes are equal. Different strings can still share a hash, so an equal hash must be *verified* with a real comparison. Choose the “Tiny modulus (a collision)” preset: with modulus 101 the windows adi and dia have the same hash as the pattern aaa and are rejected only by the character check.

Java
1static int indexOf(String text, String pat, long mod) {
2 int n = text.length(), m = pat.length(); final long B = 31;
3 if (m > n) return -1;
4 long high = 1, hp = 0, hw = 0;
5 for (int i = 0; i < m - 1; i++) high = high * B % mod; // B^(m-1) % mod
6 for (int i = 0; i < m; i++) {
7 hp = (hp * B + pat.charAt(i)) % mod; // hash of the pattern
8 hw = (hw * B + text.charAt(i)) % mod; // hash of the first window
9 }
10 for (int s = 0; s + m <= n; s++) {
11 if (hp == hw && text.startsWith(pat, s)) return s; // equal hashes may still collide: verify
12 if (s + m < n) // slide: drop text[s], add text[s + m]
13 hw = ((hw - text.charAt(s) * high % mod + mod) * B + text.charAt(s + m)) % mod;
14 }
15 return -1;
16}

Best

O(n + m)

Average

O(n + m)

Worst

O(n · m)

Space

O(1)

Rolling hash: Rabin-Karp substring search

Interactive · edit the input

Your input

Try

text · length 11

a
b
r
a
c
a
d
a
b
r
a
012345678910
WindowMatchHash hit but not equal (collision)
Setup

Hash the pattern "cad" and the first window "abr" with hash = Σ char × 31^(m-1-i) mod 1000000007. Comparing two numbers replaces comparing m characters.

Step 1 / 11
Java
1static int indexOf(String text, String pat, long mod) {
2 int n = text.length(), m = pat.length(); final long B = 31;
3 if (m > n) return -1;
4 long high = 1, hp = 0, hw = 0;
5 for (int i = 0; i < m - 1; i++) high = high * B % mod; // B^(m-1) % mod
6 for (int i = 0; i < m; i++) {
7 hp = (hp * B + pat.charAt(i)) % mod; // hash of the pattern
8 hw = (hw * B + text.charAt(i)) % mod; // hash of the first window
9 }
10 for (int s = 0; s + m <= n; s++) {
11 if (hp == hw && text.startsWith(pat, s)) return s; // equal hashes may still collide: verify
12 if (s + m < n) // slide: drop text[s], add text[s + m]
13 hw = ((hw - text.charAt(s) * high % mod + mod) * B + text.charAt(s + m)) % mod;
14 }
15 return -1;
16}

Live state

0 hash comparisons0 character checks

pattern hash

98246

window hash

96369

mod

1000000007

No condition evaluated on this step
Execution trace (dry run table)Show 11 steps
StepLineStateWhat happens
1L7pattern hash=98246 window hash=96369 mod=1000000007Hash the pattern "cad" and the first window "abr" with hash = Σ char × 31^(m-1-i) mod 1000000007. Comparing two numbers replaces comparing m characters.
2L11s=0 pattern hash=98246 window hash=96369Hashes differ (96369 vs 98246), so the window "abr" cannot be the pattern. Skip it without looking at its characters.
3L13s=1 dropped=a added=a window hash=97809Slide: remove 'a' (its weight is 31^2), shift, add 'a'. The new hash costs O(1), not O(m).
4L11s=1 pattern hash=98246 window hash=97809Hashes differ (97809 vs 98246), so the window "bra" cannot be the pattern. Skip it without looking at its characters.
5L13s=2 dropped=b added=c window hash=112660Slide: remove 'b' (its weight is 31^2), shift, add 'c'. The new hash costs O(1), not O(m).
6L11s=2 pattern hash=98246 window hash=112660Hashes differ (112660 vs 98246), so the window "rac" cannot be the pattern. Skip it without looking at its characters.
7L13s=3 dropped=r added=a window hash=96383Slide: remove 'r' (its weight is 31^2), shift, add 'a'. The new hash costs O(1), not O(m).
8L11s=3 pattern hash=98246 window hash=96383Hashes differ (96383 vs 98246), so the window "aca" cannot be the pattern. Skip it without looking at its characters.
9L13s=4 dropped=a added=d window hash=98246Slide: remove 'a' (its weight is 31^2), shift, add 'd'. The new hash costs O(1), not O(m).
10L11s=4 pattern hash=98246 window hash=98246The hashes are equal at s = 4: verify with a real character comparison, because different strings can share a hash.
11L11return=4"cad" equals the pattern: found at index 4.
14.08.2

Universal hashing intro

Any fixed hash function has inputs that collide badly: an adversary who knows it can pick keys that all land in one bucket. **Universal hashing** defeats this by choosing the function *at random* from a family when the table is created. A classic family for integer keys is h(k) = ((a·k + b) mod p) mod m, where p is a prime larger than every key, a is chosen from 1..p-1 and b from 0..p-1.

For such a family, any two distinct keys collide with probability at most 1/m, whatever the keys are. The guarantee is about the random choice, not about the data, so no input is bad in advance. The animation's “Universal hash” preset uses ((a·k + 7) mod 101) mod m with a multiplier a that you can change: the same keys spread differently for different a.

Java
1final class UniversalHash {
2 private static final long P = 2_147_483_647L; // prime larger than every int key
3 private final long a, b; private final int m;
4 UniversalHash(int m, java.util.Random rnd) {
5 this.m = m;
6 this.a = 1 + (long) (rnd.nextDouble() * (P - 1)); // 1 .. P-1
7 this.b = (long) (rnd.nextDouble() * P); // 0 .. P-1
8 }
9 int hash(int key) { return (int) (((a * key + b) % P) % m); } // a * key stays below 2^63 for 0 <= key < 2^31
10}

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

Perfect hashing intro

A **perfect hash function** maps a *fixed* set of keys to distinct slots: no collisions at all, so a lookup is a single probe in the worst case. It only works for a known, static set, such as the reserved words of a language.

The standard construction (FKS) uses two levels. A first universal hash splits the n keys into n buckets. A bucket holding k keys gets its own second-level table of size k² with a hash function re-picked until it has no collision; because the expected number of collisions in a k²-sized table is below one half, a random function succeeds after a couple of tries. The total space is O(n) on average, and every lookup costs two hash computations.

Ordinary hash tablePerfect hashing (FKS)
Key setChanges over timeFixed in advance
CollisionsHandled (chains, probing)None by construction
LookupExpected O(1)Worst-case O(1)
Insert / removeSupportedNot supported (rebuild)
Typical useGeneral maps and setsCompilers, keyword tables, read-only dictionaries
14.08.4

Hash flooding

If a service builds a hash table from attacker-controlled keys (HTTP parameters, JSON field names) and the hash function is known, the attacker can send many keys that all collide. The table degrades to a list, each insertion costs O(n) and n keys cost O(n²): a small request can burn seconds of CPU. This is a **hash flooding** denial-of-service attack.

With Java's String.hashCode, colliding strings are easy to build: "Aa" and "BB" both hash to 2112, and so do "AaAa", "AaBB", "BBAa" and "BBBB" (2031744). Concatenating 10 blocks, each Aa or BB, gives 1024 different strings with the *same* hash code. Defences: Java 8+ treeifies long buckets for Comparable keys, many languages randomise their hash seed per process, and servers limit the number of parameters accepted. Run the “Worst case: all collide” preset to see the effect.

Java
1static List<String> collidingStrings(int blocks) {
2 List<String> strs = List.of("");
3 for (int i = 0; i < blocks; i++) {
4 List<String> next = new ArrayList<>();
5 for (String s : strs) { next.add(s + "Aa"); next.add(s + "BB"); } // "Aa" and "BB" have the same hashCode
6 strs = next;
7 }
8 return strs; // 2^blocks strings, one shared hashCode
9}

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

Hash collisions vs equality

Two facts are easy to mix up. Equal objects **must** have equal hash codes. Unequal objects **may** have equal hash codes. So a matching hash proves nothing about equality: it only says the two keys are worth comparing. A hash table always finishes with equals before declaring a hit.

The same applies to hash-based shortcuts in algorithms: a Rabin-Karp window with the pattern's hash, or a file with the same checksum as another, is a *candidate*. Use the hash to reject quickly, and confirm candidates with a real comparison unless a false positive is acceptable (and with a cryptographic hash, it is negligibly unlikely).

Java
1static void collisionIsNotEquality() {
2 String a = "Aa", b = "BB";
3 System.out.println(a.hashCode() == b.hashCode()); // true: both 2112
4 System.out.println(a.equals(b)); // false: different strings
5 Set<String> set = new HashSet<>(List.of(a, b));
6 System.out.println(set.size()); // 2: equals separates them inside the shared bucket
7}

Practice ladder

Now try it yourself

Easy

  • Implement strStr
  • Compute the polynomial hash of 'abc' with base 31

Medium

  • Repeated DNA Sequences
  • Find All Anagrams in a String

Advanced

  • Longest Duplicate Substring
  • Shortest Palindrome (rolling hash)

Key takeaways

  • A rolling hash slides in O(1); Rabin-Karp verifies equal hashes with a real comparison.
  • Universal hashing picks the function at random so no input is bad in advance; perfect hashing has no collisions for a fixed key set.
  • Equal hashes never prove equal keys, and predictable hashes allow flooding attacks.