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
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.
1static int indexOf(String text, String pat, long mod) {2int n = text.length(), m = pat.length(); final long B = 31;3if (m > n) return -1;4long high = 1, hp = 0, hw = 0;5for (int i = 0; i < m - 1; i++) high = high * B % mod; // B^(m-1) % mod6for (int i = 0; i < m; i++) {7hp = (hp * B + pat.charAt(i)) % mod; // hash of the pattern8hw = (hw * B + text.charAt(i)) % mod; // hash of the first window9}10for (int s = 0; s + m <= n; s++) {11if (hp == hw && text.startsWith(pat, s)) return s; // equal hashes may still collide: verify12if (s + m < n) // slide: drop text[s], add text[s + m]13hw = ((hw - text.charAt(s) * high % mod + mod) * B + text.charAt(s + m)) % mod;14}15return -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 inputtext · length 11
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.
1static int indexOf(String text, String pat, long mod) {2int n = text.length(), m = pat.length(); final long B = 31;3if (m > n) return -1;4long high = 1, hp = 0, hw = 0;5for (int i = 0; i < m - 1; i++) high = high * B % mod; // B^(m-1) % mod6for (int i = 0; i < m; i++) {7hp = (hp * B + pat.charAt(i)) % mod; // hash of the pattern8hw = (hw * B + text.charAt(i)) % mod; // hash of the first window9}10for (int s = 0; s + m <= n; s++) {11if (hp == hw && text.startsWith(pat, s)) return s; // equal hashes may still collide: verify12if (s + m < n) // slide: drop text[s], add text[s + m]13hw = ((hw - text.charAt(s) * high % mod + mod) * B + text.charAt(s + m)) % mod;14}15return -1;16}
Live state
pattern hash
98246
window hash
96369
mod
1000000007
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L7 | pattern hash=98246 window hash=96369 mod=1000000007 | 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. |
| 2 | L11 | s=0 pattern hash=98246 window hash=96369 | Hashes differ (96369 vs 98246), so the window "abr" cannot be the pattern. Skip it without looking at its characters. |
| 3 | L13 | s=1 dropped=a added=a window hash=97809 | Slide: remove 'a' (its weight is 31^2), shift, add 'a'. The new hash costs O(1), not O(m). |
| 4 | L11 | s=1 pattern hash=98246 window hash=97809 | Hashes differ (97809 vs 98246), so the window "bra" cannot be the pattern. Skip it without looking at its characters. |
| 5 | L13 | s=2 dropped=b added=c window hash=112660 | Slide: remove 'b' (its weight is 31^2), shift, add 'c'. The new hash costs O(1), not O(m). |
| 6 | L11 | s=2 pattern hash=98246 window hash=112660 | Hashes differ (112660 vs 98246), so the window "rac" cannot be the pattern. Skip it without looking at its characters. |
| 7 | L13 | s=3 dropped=r added=a window hash=96383 | Slide: remove 'r' (its weight is 31^2), shift, add 'a'. The new hash costs O(1), not O(m). |
| 8 | L11 | s=3 pattern hash=98246 window hash=96383 | Hashes differ (96383 vs 98246), so the window "aca" cannot be the pattern. Skip it without looking at its characters. |
| 9 | L13 | s=4 dropped=a added=d window hash=98246 | Slide: remove 'a' (its weight is 31^2), shift, add 'd'. The new hash costs O(1), not O(m). |
| 10 | L11 | s=4 pattern hash=98246 window hash=98246 | The hashes are equal at s = 4: verify with a real character comparison, because different strings can share a hash. |
| 11 | L11 | return=4 | "cad" equals the pattern: found at index 4. |
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.
1final class UniversalHash {2private static final long P = 2_147_483_647L; // prime larger than every int key3private final long a, b; private final int m;4UniversalHash(int m, java.util.Random rnd) {5this.m = m;6this.a = 1 + (long) (rnd.nextDouble() * (P - 1)); // 1 .. P-17this.b = (long) (rnd.nextDouble() * P); // 0 .. P-18}9int hash(int key) { return (int) (((a * key + b) % P) % m); } // a * key stays below 2^63 for 0 <= key < 2^3110}
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. |
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 table | Perfect hashing (FKS) | |
|---|---|---|
| Key set | Changes over time | Fixed in advance |
| Collisions | Handled (chains, probing) | None by construction |
| Lookup | Expected O(1) | Worst-case O(1) |
| Insert / remove | Supported | Not supported (rebuild) |
| Typical use | General maps and sets | Compilers, keyword tables, read-only dictionaries |
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.
1static List<String> collidingStrings(int blocks) {2List<String> strs = List.of("");3for (int i = 0; i < blocks; i++) {4List<String> next = new ArrayList<>();5for (String s : strs) { next.add(s + "Aa"); next.add(s + "BB"); } // "Aa" and "BB" have the same hashCode6strs = next;7}8return strs; // 2^blocks strings, one shared hashCode9}
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 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).
1static void collisionIsNotEquality() {2String a = "Aa", b = "BB";3System.out.println(a.hashCode() == b.hashCode()); // true: both 21124System.out.println(a.equals(b)); // false: different strings5Set<String> set = new HashSet<>(List.of(a, b));6System.out.println(set.size()); // 2: equals separates them inside the shared bucket7}
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.