Chapter 12: Maps and
Hash Tables
Introduction
Learning Outcomes
Explain key–value association and how a hash table works.
Resolve collisions with chaining and with open addressing.
Relate load factor, resizing, and the spread of keys.
Honor the equals/hashCode contract and compare average- and worst-case costs.
Maps and the Idea of Hashing
Maps
- A map stores data as key–value pairs.
- Each key identifies its entry uniquely; the value is associated with that key.
- We reach a value through its key: "What is Ayşe Demir's phone number?"
- Implemented with a hash table, operations take O(1) on average.
Maps and the Idea of Hashing
Core Operations of the Map ADT
| Operation | java.util.Map | Description |
|---|---|---|
| Insertion | put(k, v) | Adds the pair to the table |
| Search | get(k) | Returns the value for key k, or null if absent |
| Deletion | remove(k) | Removes the entry with key k and returns its value |
| Update | put(k, v′) | If k is present, replaces its value with v′ (returns the old one) |
| Other | size(), isEmpty(), containsKey(k), keySet(), values() | |
Map<Integer, String> m = new HashMap<>(); m.put(105, "Murat"); m.put(21, "Leyla"); m.put(21, "Ali"); // update: "Leyla" → "Ali" m.get(21); // "Ali" m.remove(105); // "Murat" m.get(88); // null: no such key
Maps and the Idea of Hashing
Searching an Array
- Can we search an array in O(1) time?
- Not by comparing elements: linear search is O(n), and binary search on a sorted array is O(log n).
- But if we know the element's position (its index), access takes one step:
int[] sortedArr = {1, 3, 5, 7, 9}; int idx = 2; // index of the element we want int item = sortedArr[idx]; // O(1): item = 5
Maps and the Idea of Hashing
Indexing: Key = Index
If the keys are integers from 0 to 9, we put key k in cell k of the array, and a search takes one step.
Each key k in the key space goes into cell A[k]; unused cells stay empty.
search(3): look straight at A[3] → occupied → found, O(1).
search(0): look at A[0] → empty → not found, still O(1).
Maps and the Idea of Hashing
Direct Addressing and Its Limits
- Reserve one cell for every possible key: search, insert and delete are all O(1).
- A large key space wastes huge amounts of memory: 9-digit student IDs need 109 cells (≈ 4 GB as an int array) for a department of a few hundred students.
- Keys that aren't integers ("Ali", "Beyza") can't serve as indices at all.
Maps and the Idea of Hashing
Hashing
- Hashing is a technique for storing and retrieving entries.
- It quickly finds a location (a bucket) for each entry.
- Once the location is known, inserting, removing and updating in an array are easy.
- The hash function does the work: it turns a key into an integer index in the range
0 … N−1.
"Ali".hashCode() = 65918 → 65918 mod 13 = 8 (a table with N = 13 buckets)
Hash Functions
Hash Functions
- At the heart of a hash table is its hash function: it takes a key and produces the entry's position (index) in the table.
A bad function
- If every entry hashes to the same slot, the table degenerates into a linked list: search is O(n).
Choosing the capacity N
- Too large wastes memory; too small causes more collisions.
Hash Functions
Hash Code + Compression
h(k) = k mod NChoosing a prime N keeps regular patterns in the keys (such as multiples of 10) from piling into the same buckets.
h(k) = ((a·k + b) mod p) mod Np > N is prime; a and b are random (a ≠ 0).
| k | k mod 10 | k mod 11 |
|---|---|---|
| 10 | 0 | 10 |
| 20 | 0 | 9 |
| 30 | 0 | 8 |
| 40 | 0 | 7 |
| 50 | 0 | 6 |
N = 10: all in one bucket. N = 11 (prime): each in a different bucket.
Hash Functions
The equals and hashCode Contract
a.equals(b) is true, then a.hashCode() == b.hashCode() must hold; otherwise equal keys land in different buckets.equals.equals, override hashCode too, using the same fields.record Student(int id, String name) {} Map<Student, Double> grades = new HashMap<>(); grades.put(new Student(42, "Ada"), 87.5); // record: equals/hashCode come free (Java 16+)
hashCode picks the bucket and equals decides whether two keys are really the same.Hash Functions · Common mistake
Common Mistake: equals Without hashCode
class Point { int x, y; Point(int x, int y) { this.x = x; this.y = y; } public boolean equals(Object o) { return o instanceof Point p && x == p.x && y == p.y; } // no hashCode() -> Object's is used } Map<Point, String> m = new HashMap<>(); m.put(new Point(1, 2), "A"); m.get(new Point(1, 2)); // most likely null (!)
put(p1, "A"): p1.hashCode() picks the bucket. Point doesn't override it, so Object.hashCode() (object identity) is used; say, bucket 3.
get(p2): p2 is equal to p1 (equals is true) but a different object, so its identity hash (almost always) differs → we look in, say, bucket 6, find it empty → null. equals is never even called!
The fix: override hashCode() too: return Objects.hash(x, y); Equal points land in the same bucket, where equals finds the match → "A".
Hash Functions
Sample Hash Functions
int modulusHash(int key, int arraySize) { return Math.floorMod(key, arraySize); }
floorMod gives a safe index in 0 … arraySize−1 even for negative keys.
long crc32Hash(String key) { CRC32 crc = new CRC32(); // java.util.zip crc.update(key.getBytes()); return crc.getValue(); }
int djb2Hash(String s) { int h = 5381; for (int i = 0; i < s.length(); i++) h = (h * 33) ^ s.charAt(i); return h; }
Classic DJB2 uses h*33 + c; this is the XOR variant. The result can be negative, so it needs compressing before use as an index.
// Σ s[i] · 31^(n−1−i) "abc".hashCode() // 96354
97·31² + 98·31 + 99 = 96354
Hash Functions · Step through the code
Code Trace: "Ali".hashCode()
hash("Ali") is called; each character is processed by its Unicode code (A = 65, l = 108, i = 105).
Start with h = 0.
0 < 3 → the loop continues; the character is s.charAt(0) = 'A' (65).
h = 31 · 0 + 65 = 65. The running total is multiplied by 31, so the character's position matters too.
1 < 3 → the loop continues; the character is s.charAt(1) = 'l' (108).
h = 31 · 65 + 108 = 2123. The running total is multiplied by 31, so the character's position matters too.
2 < 3 → the loop continues; the character is s.charAt(2) = 'i' (105).
h = 31 · 2123 + 105 = 65918. The running total is multiplied by 31, so the character's position matters too.
3 < 3 is false → the loop ends.
return 65918 = 65·31² + 108·31 + 105.
code = 65918. For long strings the product overflows int and the result can be negative.
Compression: floorMod(65918, 13) = 8 → bucket 8 in a 13-bucket table.
// same computation as String.hashCode(): int hash(String s) { int h = 0; for (int i = 0; i < s.length(); i++) h = 31 * h + s.charAt(i); return h; } // main: int code = hash("Ali"); // 65918 int index = Math.floorMod(code, 13); // 8
Hash Functions · Step through the code
Code Trace: modulusHash()
An empty 10-cell table; every cell is null.
Next key: k = 5.
modulusHash(5, 10): 5 mod 10 = 5 (the remainder).
table[5] = 5.
Next key: k = 1.
modulusHash(1, 10): 1 mod 10 = 1 (the remainder).
table[1] = 1.
Next key: k = 10.
modulusHash(10, 10): 10 mod 10 = 0 (the remainder).
table[0] = 10.
Next key: k = 26.
modulusHash(26, 10): 26 mod 10 = 6 (the remainder).
table[6] = 26.
Next key: k = 99.
modulusHash(99, 10): 99 mod 10 = 9 (the remainder).
table[9] = 99. Five keys, five different cells: no collisions. A 15, though, would collide with 5.
int modulusHash(int key, int arraySize) { return Math.floorMod(key, arraySize); } // main: Integer[] table = new Integer[10]; for (int k : new int[]{5, 1, 10, 26, 99}) { int j = modulusHash(k, table.length); table[j] = k; }
Hash Functions
Collisions
- A collision happens when different keys produce the same index: h(5) = h(105) = 5.
- Two entries cannot share a cell directly.
- Because the key space is larger than the table, collisions are inevitable, so we need a strategy for them:
Separate Chaining
Resolving Collisions: Separate Chaining
h(5) = 5 % 10 = 5 → bucket 5 is empty; node (5, \"Ali\") is linked in.
h(1) = 1 % 10 = 1 → bucket 1 is empty; node (1, \"Tuna\") is linked in.
h(10) = 10 % 10 = 0 → bucket 0 is empty; node (10, \"Leyla\") is linked in.
h(26) = 26 % 10 = 6 → bucket 6 is empty; node (26, \"Ceren\") is linked in.
h(99) = 99 % 10 = 9 → bucket 9 is empty; node (99, \"Sena\") is linked in.
h(105) = 105 % 10 = 5 → bucket 5 already holds (5, "Ali"): a collision. The new node goes at the head of the chain.
Separate Chaining
Hash Table Terminology
Separate Chaining · Java
Implementing a Hash Table: The Class
public class HashTable { private HashNode[] buckets; // heads of the chains private int numBuckets, size; public HashTable(int capacity) { this.numBuckets = capacity; this.buckets = new HashNode[capacity]; this.size = 0; } private class HashNode { private Integer key; private String value; private HashNode next; public HashNode(Integer key, String value) { this.key = key; this.value = value; } } }
null; size = 0.Separate Chaining · Java
Bucket Index: getBucketIndex()
// Naive: key % buckets.length → may be negative! private int getBucketIndex(Integer key) { int h = key.hashCode(); // = key return Math.floorMod(h, buckets.length); }
| key | key % 10 | Math.floorMod(key, 10) | Result |
|---|---|---|---|
| 105 | 5 | 5 | bucket 5 |
| 21 | 1 | 1 | bucket 1 |
| 41 | 1 | 1 | bucket 1 (collides with 21) |
| −7 | −7 | 3 | with %, buckets[-7] → ArrayIndexOutOfBoundsException |
% takes the sign of the dividend. Integer.hashCode() is just the number, and for keys such as strings hashCode() is often negative. Math.abs(h) % N isn't safe either: Math.abs(Integer.MIN_VALUE) is negative.Separate Chaining · Step through the code
Code Trace: getBucketIndex(41), (−7)
getBucketIndex(41) is called; the table has 10 buckets.
Integer.hashCode() is just the number: h = 41.
floorMod(41, 10) is called.
In Java, 41 % 10 = 1.
r = 1 ≥ 0 → no correction needed.
return 1.
Bucket index 1. (21 also lands in bucket 1: a collision.)
getBucketIndex(-7) is called; the table has 10 buckets.
Integer.hashCode() is just the number: h = -7.
floorMod(-7, 10) is called.
In Java, -7 % 10 = -7: the result takes the sign of the dividend.
r < 0 → r += 10 → r = 3, now in the range 0 … 9.
return 3.
Bucket index 3. With % we would get buckets[-7] → an exception.
private int getBucketIndex(Integer key) { int h = key.hashCode(); // = key return Math.floorMod(h, buckets.length); } // what floorMod does for n > 0: static int floorMod(int x, int n) { int r = x % n; // sign comes from x if (r < 0) r += n; // if negative, add n return r; }
Separate Chaining · Java
Inserting a Key–Value Pair
public void put(Integer key, String value) { int bucketIndex = getBucketIndex(key); HashNode head = buckets[bucketIndex]; while (head != null) { // key present? if (head.key.equals(key)) { head.value = value; // yes: update return; } head = head.next; } size++; HashNode node = new HashNode(key, value); node.next = buckets[bucketIndex]; buckets[bucketIndex] = node; // insert at head }
h(key).Separate Chaining · Step through the code
Code Trace: put(21), put(41)
public void put(Integer key, String value) { int bucketIndex = getBucketIndex(key); HashNode head = buckets[bucketIndex]; while (head != null) { // key present? if (head.key.equals(key)) { head.value = value; // yes: update return; } head = head.next; } size++; HashNode node = new HashNode(key, value); node.next = buckets[bucketIndex]; buckets[bucketIndex] = node; // insert at head }
put(21, \"Leyla\"): h(21) = 21 mod 10 = 1 → only bucket 1 is examined.
Bucket 1 is empty: head = null.
head == null → the loop ends: 21 is not in the table; we need a new node.
New pair: size = 2.
A new node (21, \"Leyla\") is created; its next is still null, so it isn't in the table yet.
node.next = buckets[1] = null (the bucket was empty).
buckets[1] = node: the new node is at the head of the chain. No need to walk to the end → O(1).
put(41, \"Sena\"): h(41) = 41 mod 10 = 1 → only bucket 1 is examined.
head points to the first node of the chain.
head != null → enter the loop to examine this node.
21 ≠ 41 → no match; move on.
head = head.next → null (end of the chain).
head == null → the loop ends: 41 is not in the table; we need a new node.
New pair: size = 3.
A new node (41, \"Sena\") is created; its next is still null, so it isn't in the table yet.
node.next points to the old head (21), so the existing chain is kept.
buckets[1] = node: the new node is at the head of the chain. No need to walk to the end → O(1).
Separate Chaining · Step through the code
Code Trace: put(21, "Ali") (update)
public void put(Integer key, String value) { int bucketIndex = getBucketIndex(key); HashNode head = buckets[bucketIndex]; while (head != null) { // key present? if (head.key.equals(key)) { head.value = value; // yes: update return; } head = head.next; } size++; HashNode node = new HashNode(key, value); node.next = buckets[bucketIndex]; buckets[bucketIndex] = node; // insert at head }
put(21, \"Ali\"): h(21) = 21 mod 10 = 1 → only bucket 1 is examined.
head points to the first node of the chain.
head != null → enter the loop to examine this node.
41 ≠ 21 → no match; move on.
head = head.next → node 21.
head != null → enter the loop to examine this node.
21.equals(21) → equal: the key is already in the table.
The value is updated: "Leyla" → "Ali". No new node is added.
return: size = 3 is unchanged (same number of pairs).
Separate Chaining · Java
Looking Up a Value by Key
public String get(Integer key) { int bucketIndex = getBucketIndex(key); HashNode head = buckets[bucketIndex]; while (head != null) { if (head.key.equals(key)) return head.value; // found head = head.next; } return null; // no such key }
| Call | Bucket | Nodes examined | Returns |
|---|---|---|---|
table.get(105) | 105 % 10 = 5 | 105 | "Murat" |
table.get(21) | 21 % 10 = 1 | 41, 21 | "Ali" |
table.get(88) | 88 % 10 = 8 | — (empty bucket) | null |
Separate Chaining · Step through the code
Code Trace: get(105), get(21), get(88)
public String get(Integer key) { int bucketIndex = getBucketIndex(key); HashNode head = buckets[bucketIndex]; while (head != null) { if (head.key.equals(key)) return head.value; // found head = head.next; } return null; // no such key }
get(105): h(105) = 105 mod 10 = 5. No other bucket is examined.
head points to the bucket's first node.
head != null → enter the loop body.
105.equals(105) → equal.
return "Murat": one comparison was enough.
get(21): h(21) = 21 mod 10 = 1. No other bucket is examined.
head points to the bucket's first node.
head != null → enter the loop body.
41 ≠ 21 → another key that landed in the same bucket (a collision).
head = head.next → node 21.
head != null → enter the loop body.
21.equals(21) → equal.
return "Ali": two comparisons were enough.
get(88): h(88) = 88 mod 10 = 8. No other bucket is examined.
Bucket 8 is empty: head = null.
head == null → the loop body never runs.
return null: 88 is not in the table. An empty bucket answers in one step.
Separate Chaining · Java
Removing a Key from a Hash Table
public String remove(Integer key) { int bucketIndex = getBucketIndex(key); HashNode head = buckets[bucketIndex]; HashNode prev = null; while (head != null && !head.key.equals(key)) { prev = head; head = head.next; } if (head == null) return null; // absent size--; if (prev != null) // middle/end prev.next = head.next; else // head buckets[bucketIndex] = head.next; return head.value; }
prev.next.Separate Chaining · Step through the code
Code Trace: remove(21) (from the middle of a chain)
public String remove(Integer key) { int bucketIndex = getBucketIndex(key); HashNode head = buckets[bucketIndex]; HashNode prev = null; while (head != null && !head.key.equals(key)) { prev = head; head = head.next; } if (head == null) return null; // absent size--; if (prev != null) // middle/end prev.next = head.next; else // head buckets[bucketIndex] = head.next; return head.value; }
remove(21): h(21) = 21 mod 10 = 1.
head points to the first node.
prev = null: to unlink a node from a singly linked list we need the node before it.
41 ≠ 21 → the condition holds; enter the loop body.
prev = head → node 41.
head = head.next → node 21.
21.equals(21) → equal, so the loop ends: we've found the node to remove.
head != null → the key is present; remove it.
size = 2.
prev != null → the node is in the middle or at the end: bypass it through prev.
prev.next = head.next (null): 41 now skips 21, which is unlinked from the chain.
return \"Ali\". The garbage collector reclaims the unlinked node.
Separate Chaining · Step through the code
Code Trace: remove(105) (head), remove(88) (absent)
public String remove(Integer key) { int bucketIndex = getBucketIndex(key); HashNode head = buckets[bucketIndex]; HashNode prev = null; while (head != null && !head.key.equals(key)) { prev = head; head = head.next; } if (head == null) return null; // absent size--; if (prev != null) // middle/end prev.next = head.next; else // head buckets[bucketIndex] = head.next; return head.value; }
remove(105): h(105) = 105 mod 10 = 5.
head points to the first node.
prev = null: to unlink a node from a singly linked list we need the node before it.
105.equals(105) → equal, so the loop ends: we've found the node to remove.
head != null → the key is present; remove it.
size = 1.
prev == null → the node to remove is the head of the chain: take the else branch.
buckets[5] = head.next (null): the bucket no longer points to 105.
return \"Murat\". The garbage collector reclaims the unlinked node.
remove(88): h(88) = 88 mod 10 = 8.
Bucket 8 is empty: head = null.
prev = null: to unlink a node from a singly linked list we need the node before it.
head == null → the loop ends immediately.
return null: 88 is absent; size is unchanged.
Open Addressing
Open Addressing
- All pairs live in the array itself, at most one per slot (n ≤ N).
- On a collision, a probe sequence searches for an empty slot:
h(k, i) = (h(k) + f(i)) mod N, i = 0, 1, 2, …
| Method | f(i) | Probe sequence (h(k) = 5, N = 13) | Weakness |
|---|---|---|---|
| Linear probing | i | 5, 6, 7, 8, 9, … | Primary clustering |
| Quadratic probing | i² | 5, 6, 9, 14→1, 21→8, … | Secondary clustering; may not try every slot |
| Double hashing | i · h₂(k) | 5, 5+d, 5+2d, … (d = h₂(k)) | Needs h₂(k) ≠ 0 and the step coprime to N |
Open Addressing · Step through the code
Linear Probing
18 → 5, 41 → 2, 22 → 9: all three find an empty slot on the first try. Next up: 44.
Next key 44: h = 44 mod 13 = 5 (home slot).
i = 0: j = (5 + 0) mod 13 = 5 → occupied (18); next i.
i = 1: j = (5 + 1) mod 13 = 6 → empty.
table[6] = 44, break (2 probes).
Next key 59: h = 59 mod 13 = 7 (home slot).
i = 0: j = (7 + 0) mod 13 = 7 → empty.
table[7] = 59, break (1 probe).
Next key 32: h = 32 mod 13 = 6 (home slot).
i = 0: j = (6 + 0) mod 13 = 6 → occupied (44); next i.
i = 1: j = (6 + 1) mod 13 = 7 → occupied (59); next i.
i = 2: j = (6 + 2) mod 13 = 8 → empty.
table[8] = 32, break (3 probes).
// put(k): linear probing, N = 13 int h = k % 13; // home slot for (int i = 0; i < 13; i++) { int j = (h + i) % 13; // h, h+1, h+2… if (table[j] == null) { // empty? table[j] = k; break; } // occupied: i++ }
Open Addressing · Step through the code
Linear Probing (cont.): 31 and 73
Next key 31: h = 31 mod 13 = 5 (home slot).
i = 0: j = (5 + 0) mod 13 = 5 → occupied (18); next i.
i = 1: j = (5 + 1) mod 13 = 6 → occupied (44); next i.
i = 2: j = (5 + 2) mod 13 = 7 → occupied (59); next i.
i = 3: j = (5 + 3) mod 13 = 8 → occupied (32); next i.
i = 4: j = (5 + 4) mod 13 = 9 → occupied (22); next i.
i = 5: j = (5 + 5) mod 13 = 10 → empty.
table[10] = 31, break (6 probes).
Next key 73: h = 73 mod 13 = 8 (home slot).
i = 0: j = (8 + 0) mod 13 = 8 → occupied (32); next i.
i = 1: j = (8 + 1) mod 13 = 9 → occupied (22); next i.
i = 2: j = (8 + 2) mod 13 = 10 → occupied (31); next i.
i = 3: j = (8 + 3) mod 13 = 11 → empty.
table[11] = 73, break (4 probes). Slots 5–11 form one big cluster: primary clustering.
// put(k): linear probing, N = 13 int h = k % 13; // home slot for (int i = 0; i < 13; i++) { int j = (h + i) % 13; // h, h+1, h+2… if (table[j] == null) { // empty? table[j] = k; break; } // occupied: i++ }
Open Addressing · Step through the code
Quadratic Probing
18 → 5, 41 → 2, 22 → 9: all three find an empty slot on the first try. Next up: 44.
Next key 44: h = 44 mod 13 = 5 (home slot).
i = 0: j = (5 + 0²) mod 13 = 5 → occupied (18); next i.
i = 1: j = (5 + 1²) mod 13 = 6 → empty.
table[6] = 44, break (2 probes).
Next key 59: h = 59 mod 13 = 7 (home slot).
i = 0: j = (7 + 0²) mod 13 = 7 → empty.
table[7] = 59, break (1 probe).
Next key 32: h = 32 mod 13 = 6 (home slot).
i = 0: j = (6 + 0²) mod 13 = 6 → occupied (44); next i.
i = 1: j = (6 + 1²) mod 13 = 7 → occupied (59); next i.
i = 2: j = (6 + 2²) mod 13 = 10 → empty.
table[10] = 32, break (3 probes).
// put(k): quadratic probing, N = 13 int h = k % 13; // home slot for (int i = 0; i < 13; i++) { int j = (h + i * i) % 13; // h, h+1, h+4… if (table[j] == null) { // empty? table[j] = k; break; } // occupied: i++ }
Open Addressing · Step through the code
Quadratic Probing (cont.): 31 and 73
Next key 31: h = 31 mod 13 = 5 (home slot).
i = 0: j = (5 + 0²) mod 13 = 5 → occupied (18); next i.
i = 1: j = (5 + 1²) mod 13 = 6 → occupied (44); next i.
i = 2: j = (5 + 2²) mod 13 = 9 → occupied (22); next i.
i = 3: j = (5 + 3²) mod 13 = 1 → empty.
table[1] = 31, break (4 probes). The jumps grow, so the probe escapes the cluster quickly.
Next key 73: h = 73 mod 13 = 8 (home slot).
i = 0: j = (8 + 0²) mod 13 = 8 → empty.
table[8] = 73, break (1 probe).
// put(k): quadratic probing, N = 13 int h = k % 13; // home slot for (int i = 0; i < 13; i++) { int j = (h + i * i) % 13; // h, h+1, h+4… if (table[j] == null) { // empty? table[j] = k; break; } // occupied: i++ }
Open Addressing
Double Hashing
h(k, i) = (h(k) + i · h₂(k)) mod 13, h₂(k) = 7 − (k mod 7) → the step is 1…7, never 0.
| k | 18 | 41 | 22 | 44 | 59 | 32 | 31 | 73 |
|---|---|---|---|---|---|---|---|---|
| h(k) = k mod 13 | 5 | 2 | 9 | 5 | 7 | 6 | 5 | 8 |
| h₂(k) (on collision) | — | — | — | 7−2 = 5 | — | — | 7−3 = 4 | — |
| Slots probed | 5 | 2 | 9 | 5, 10 | 7 | 6 | 5, 9, 0 | 8 |
Open Addressing · Step through the code
Code Trace: Double Hashing
18 → 5, 41 → 2, 22 → 9: all three find an empty slot on the first try. Next up: 44.
h = 44 mod 13 = 5 (home slot), d = 7 − 2 = 5 (step size, never 0).
i = 0: j = (5 + 0·5) mod 13 = 5 → occupied (18); next i.
i = 1: j = (5 + 1·5) mod 13 = 10 → empty.
table[10] = 44, break (2 probes).
h = 59 mod 13 = 7 (home slot), d = 7 − 3 = 4 (step size, never 0).
i = 0: j = (7 + 0·4) mod 13 = 7 → empty.
table[7] = 59, break (1 probe).
h = 32 mod 13 = 6 (home slot), d = 7 − 4 = 3 (step size, never 0).
i = 0: j = (6 + 0·3) mod 13 = 6 → empty.
table[6] = 32, break (1 probe).
h = 31 mod 13 = 5 (home slot), d = 7 − 3 = 4 (step size, never 0).
i = 0: j = (5 + 0·4) mod 13 = 5 → occupied (18); next i.
i = 1: j = (5 + 1·4) mod 13 = 9 → occupied (22); next i.
i = 2: j = (5 + 2·4) mod 13 = 0 → empty.
table[0] = 31, break (3 probes).
h = 73 mod 13 = 8 (home slot), d = 7 − 3 = 4 (step size, never 0).
i = 0: j = (8 + 0·4) mod 13 = 8 → empty.
table[8] = 73, break (1 probe).
// put(k): double hashing, N = 13 int h = k % 13; int d = 7 - k % 7; // step: 1…7 for (int i = 0; i < 13; i++) { int j = (h + i * d) % 13; if (table[j] == null) { // empty? table[j] = k; break; } // occupied: i++ }
Open Addressing
Deleting Under Open Addressing: The "Deleted" Marker
The table after linear probing. We want to delete 44.
remove(44): h(44) = 5 → 18 ≠ 44 → slot 6: found 44. What if we just set the slot to null?
Wrong: slot 6 is emptied. get(32): h(32) = 6 → empty slot → reports "absent". But 32 is sitting in slot 8!
Right: mark the slot with a special deleted marker (DEFUNCT, a tombstone ✗).
get(32): h = 6 → ✗ deleted, keep searching → 7: 59 ≠ 32 → 8: found 32.
put(57): h = 5 → 5, 6(✗), 7 … 11 occupied, 12 empty → 57 is absent. It goes into the first available slot, 6.
✗ = deleted (DEFUNCT). A search skips this slot as if occupied; an insert may reuse it as if empty. ○ = probed slot, ✓ = found.
Open Addressing
Finding a Slot: The Step Search and Insert Share
private int findSlot(int k) { // slot index int h = Math.floorMod(k, N), avail = -1; for (int i = 0; i < N; i++) { int j = (h + i) % N; // linear if (table[j] == null || table[j] == DEFUNCT) { if (avail == -1) avail = j; if (table[j] == null) break; } else if (table[j].key == k) return j; // found } return -(avail + 1); // absent: encoded slot }
j = findSlot(k); if j ≥ 0, return table[j].value, otherwise null.table[−(j+1)].(h + i*i) % NDouble hashing:
(h + i*h2) % NThe table never fills up completely (λ is kept below 1 and the table grows when needed), so avail is always found.
Open Addressing · Step through the code
Code Trace: findSlot(32), with 44 deleted (✗)
findSlot(32): h = 32 mod 13 = 6; no available slot yet (−1).
i = 0: j = 6 → the slot is DEFUNCT (✗).
First empty or deleted slot: avail = 6 (an insert would go here).
Deleted ≠ empty: keep searching (32 may lie beyond).
i = 1: j = 7 → occupied (59 ≠ 32); next slot.
i = 2: j = 8 → occupied, and table[8].key = 32 == 32.
return 8 (≥ 0 means found). get returns table[8].value; the ✗ did not break the probe sequence.
private int findSlot(int k) { // slot index int h = Math.floorMod(k, N), avail = -1; for (int i = 0; i < N; i++) { int j = (h + i) % N; // linear if (table[j] == null || table[j] == DEFUNCT) { if (avail == -1) avail = j; if (table[j] == null) break; } else if (table[j].key == k) return j; // found } return -(avail + 1); // absent: encoded slot }
Open Addressing · Step through the code
Code Trace: findSlot(57) and Insertion
findSlot(57): h = 57 mod 13 = 5; no available slot yet (−1).
i = 0: j = 5 → occupied (18 ≠ 57); next slot.
i = 1: j = 6 → the slot is DEFUNCT (✗).
First empty or deleted slot: avail = 6 (an insert would go here).
Deleted ≠ empty: keep searching (57 may lie beyond).
i = 2: j = 7 → occupied (59 ≠ 57); next slot.
i = 3: j = 8 → occupied (32 ≠ 57); next slot.
i = 4: j = 9 → occupied (22 ≠ 57); next slot.
i = 5: j = 10 → occupied (31 ≠ 57); next slot.
i = 6: j = 11 → occupied (73 ≠ 57); next slot.
i = 7: j = 12 → the slot is empty.
avail is already 6 and stays (we keep the first available slot).
A truly empty slot → 57 cannot lie beyond it: break.
57 is absent: return −(6 + 1) = -7 (negative means "absent" and encodes where to insert).
put: j = -7 < 0 → table[−(j + 1)] = table[6] = 57. The deleted slot is reused.
private int findSlot(int k) { // slot index int h = Math.floorMod(k, N), avail = -1; for (int i = 0; i < N; i++) { int j = (h + i) % N; // linear if (table[j] == null || table[j] == DEFUNCT) { if (avail == -1) avail = j; if (table[j] == null) break; } else if (table[j].key == k) return j; // found } return -(avail + 1); // absent: encoded slot }
Open Addressing
Try It: Open Addressing (N = 11)
h(k) = k mod 11. Dashed outline: occupied slot probed · orange: result slot · ✗: deleted (DEFUNCT). Enter = insert.
Performance and Applications
Load Factor
- λ = n / N (n: number of pairs = size, N: number of buckets).
- With chaining, λ is the average chain length; λ > 1 is allowed, but operations slow down.
- With open addressing, λ ≤ 1 is mandatory, and the number of probes explodes as λ → 1.
- Practical thresholds: λ ≈ 0.75 for chaining (Java's
HashMap), λ ≤ 0.5 for open addressing. - Once the threshold is crossed, the table roughly doubles.
Expected probes under linear probing (Knuth):
successful ½(1 + 1/(1−λ)) · unsuccessful ½(1 + 1/(1−λ)²)
Performance and Applications
Resizing and Rehashing
12, 7, 20 and 33 are inserted into a table with N = 5 buckets. λ = 0.80 exceeds the 0.75 threshold → time to resize.
A new array is allocated and every key is rehashed: indices change, so the old positions are meaningless.
A single resize costs O(n), which is expensive; but because the capacity roughly doubles each time, the cost per insertion is amortized O(1).
Performance and Applications · Step through the code
Code Trace: resize()
private void resize() { HashNode[] old = buckets; int newN = 2 * old.length + 1; buckets = new HashNode[newN]; size = 0; numBuckets = newN; for (HashNode head : old) { // each bucket while (head != null) { put(head.key, head.value); head = head.next; } } }
Called because λ = 4/5 = 0.8 > 0.75. old holds the old array.
newN = 2·5 + 1 = 11: about twice as large, and odd (prime, in this case).
buckets now points to the new array of 11 empty buckets.
size = 0: put will count every pair again.
Old bucket 0: head = node 20.
put(20): it was 20 mod 5 = 0; now 20 mod 11 = 9 → new bucket 9.
head = null: this chain is done.
Old bucket 1 is empty: head = null, so the while loop is skipped.
Old bucket 2: head = node 7.
put(7): it was 7 mod 5 = 2; now 7 mod 11 = 7 → new bucket 7.
head = head.next → node 12 (same old chain).
put(12): it was 12 mod 5 = 2; now 12 mod 11 = 1 → new bucket 1.
head = null: this chain is done.
Old bucket 3: head = node 33.
put(33): it was 33 mod 5 = 3; now 33 mod 11 = 0 → new bucket 0.
head = null: this chain is done.
Old bucket 4 is empty: head = null, so the while loop is skipped.
Done: 7 and 12 are now in different buckets. λ = 4/11 ≈ 0.36. Every pair was moved, so this is O(n).
Performance and Applications
Comparing Complexities
| Map implementation | get | put | remove |
|---|---|---|---|
| Unsorted array / list | O(n) | O(n)* | O(n) |
| Sorted array (binary search) | O(log n) | O(n) | O(n) |
| Hash table: average | O(1) | O(1) | O(1) |
| Hash table: worst case | O(n) | O(n) | O(n) |
* The whole list must be scanned to check whether the key is already present.
Performance and Applications
How Java's HashMap Works Inside
& (n − 1) mask replaces mod.h ^ (h >>> 16) folds the high bits into the index too.Performance and Applications · Step through the code
Code Trace: index(k, 16): Mix, Then Mask
A table with 16 buckets (n = 2⁴). The three keys share their low 16 bits (…0001) and differ only in the high bits. The mask & 15 sees only the last 4 bits.
Integer.hashCode() is the value itself: h = 1 = 0x00000001. Without mixing, the bucket would be h & 15 = 1 (dashed).
h >>> 16 = 0: the high half shifts down. XOR gives m = 1; the low 4 bits are now 0001.
m & 15 = 1 → key 1 goes to bucket 1; print it.
Integer.hashCode() is the value itself: h = 65537 = 0x00010001. Without mixing, the bucket would be h & 15 = 1 (dashed).
h >>> 16 = 1: the high half shifts down. XOR gives m = 65536; the low 4 bits are now 0000.
m & 15 = 0 → key 65537 goes to bucket 0; print it.
Integer.hashCode() is the value itself: h = 131073 = 0x00020001. Without mixing, the bucket would be h & 15 = 1 (dashed).
h >>> 16 = 2: the high half shifts down. XOR gives m = 131075; the low 4 bits are now 0011.
m & 15 = 3 → key 131073 goes to bucket 3; print it.
Without mixing, all three would collide in bucket 1; with mixing they spread to buckets 1, 0 and 3. The cost: one shift and one XOR.
static int index(Object key, int n) { // n = 2^k int h = key.hashCode(); int m = h ^ (h >>> 16); // mix in the high bits return m & (n - 1); // low k bits } // main: for (int k : new int[]{1, 65537, 131073}) System.out.println(k + " -> " + index(k, 16));
Performance and Applications
Where Hash Tables Are Used
HashMap, HashSet.Performance and Applications · Step through the code
Application: twoSum([8, 3, 11, 5, 7], 12)
We want two indices whose values sum to 12. Brute force tries every pair: O(n²). Here a map remembers each value seen so far, with its index.
i = 0: a[0] = 8, so the partner is need = 4. containsKey(4) → false: no 4 seen yet.
seen.put(8, 0): later elements can find 8 as a partner.
i = 1: a[1] = 3, so the partner is need = 9. containsKey(9) → false: no 9 seen yet.
seen.put(3, 1): later elements can find 3 as a partner.
i = 2: a[2] = 11, so the partner is need = 1. containsKey(1) → false: no 1 seen yet.
seen.put(11, 2): later elements can find 11 as a partner.
i = 3: a[3] = 5, so the partner is need = 7. containsKey(7) → false: no 7 seen yet.
seen.put(5, 3): later elements can find 5 as a partner.
i = 4: need = 5, and containsKey(5) is true (O(1) on average) → a[3] + a[4] = 5 + 7 = 12. return [3, 4].
One pass, with an average O(1) lookup and insert per step → O(n) time in total, plus O(n) extra memory.
static int[] twoSum(int[] a, int target) { Map<Integer, Integer> seen = new HashMap<>(); for (int i = 0; i < a.length; i++) { int need = target - a[i]; // partner needed if (seen.containsKey(need)) return new int[]{seen.get(need), i}; seen.put(a[i], i); // value -> index } return null; // no pair }
Worked Example
A Hash Function for Student IDs
ID format: ab070c0de · example: 210707003
- ab: enrollment year (20–24) · c: department (6 or 7) · de: sequence number (00–99, two digits)
- Goal: map this format to an array index in the range 0–999.
Worked Example
The Student ID Hash Function
| ID | ab | c | de | Computation | Index |
|---|---|---|---|---|---|
210706003 | 21 | 6 | 03 | (1×100 + 0×500 + 3) mod 1000 | 103 |
240707050 | 24 | 7 | 50 | (4×100 + 1×500 + 50) mod 1000 | 950 |
230707042 | 23 | 7 | 42 | (3×100 + 1×500 + 42) mod 1000 | 842 |
Worked Example
The calculateHash Function
// s: student ID of the form "ab070c0de" public int calculateHash(String s) { int ab = Integer.parseInt(s.substring(0, 2)); int c = Integer.parseInt(s.substring(5, 6)); int de = Integer.parseInt(s.substring(7, 9)); int year = (ab - 20) * 100; // year block int dept = (c - 6) * 500; // dept: 0/500 return (year + dept + de) % 1000; }
substring(5, 6) gives only character 5 (c); substring(7, 9) gives characters 7 and 8 (de).calculateHash("240707050")ab = 24, c = 7, de = 50
400 + 500 + 50 = 950
Input that breaks the format (a year outside 20–24, c ∉ {6, 7}) can collide or even go negative; real code must validate its input.
Worked Example · Step through the code
Code Trace: calculateHash()
calculateHash(\"240707050\"): indices 0 … 8.
substring(0, 2) = \"24\" (end excluded) → ab = 24 (enrollment year).
substring(5, 6) = \"7\" → c = 7 (department).
substring(7, 9) = \"50\" → de = 50 (sequence number).
year = (24 − 20) · 100 = 400: each year gets a block of 100.
dept = (7 − 6) · 500 = 500: department 7 shifts by 500.
return (400 + 500 + 50) % 1000 = 950.
calculateHash(\"210706003\"): indices 0 … 8.
substring(0, 2) = \"21\" (end excluded) → ab = 21 (enrollment year).
substring(5, 6) = \"6\" → c = 6 (department).
substring(7, 9) = \"03\" → de = 3 (sequence number).
year = (21 − 20) · 100 = 100: each year gets a block of 100.
dept = (6 − 6) · 500 = 0: department 6 is not shifted.
return (100 + 0 + 3) % 1000 = 103.
// s: student ID of the form "ab070c0de" public int calculateHash(String s) { int ab = Integer.parseInt(s.substring(0, 2)); int c = Integer.parseInt(s.substring(5, 6)); int de = Integer.parseInt(s.substring(7, 9)); int year = (ab - 20) * 100; // year block int dept = (c - 6) * 500; // dept: 0/500 return (year + dept + de) % 1000; }
Summary
Summary
put, get, remove; a put with an existing key is an update.mod N, N prime). Deterministic, fast, uniform.Summary
Interactive Simulators
Summary
Common Mistakes
Math.abs(h) % NMath.abs(Integer.MIN_VALUE) is negative → an index-out-of-bounds error. Use Math.floorMod(h, N).equalsEqual keys land in different buckets and get returns null. Override both.== on IntegerBoxed values outside −128…127 are separate objects, so == gives false; use equals.null under open addressingIt breaks probe sequences; you need a "deleted" (DEFUNCT) marker.HashMap does not iterate in insertion order; use LinkedHashMap or TreeMap.Summary
Sample Problems
- With N = 7, h(k) = k mod 7 and linear probing, insert 10, 17, 24, 3, 5 in order. What is the final table?
- Insert the same keys with quadratic probing.
- A chained table with N = 10 buckets holds 25 pairs. What is λ? How many nodes does an unsuccessful search examine on average?
- Under open addressing, why can't we simply set a deleted slot to
null? - Compute hash(ab070c0de) for the ID 230706015.
- Which buckets do the keys 0, 12, 24, 36 land in under
k mod Nwith N = 12 and with N = 11?
Show answers
- 10→3; 17→3,4; 24→3,4,5; 3→3,4,5,6; 5→5,6,0. Table: 0:5, 3:10, 4:17, 5:24, 6:3.
- 10→3; 17→3,4; 24→3,4,(3+4=7→0); 3→3,4,0,(3+9=12→5); 5→5,6. Table: 0:24, 3:10, 4:17, 5:3, 6:5.
- λ = 25/10 = 2.5; an unsuccessful search examines λ = 2.5 nodes on average (it scans the whole chain).
- It breaks the probe sequence: keys placed beyond that slot would be reported absent. The fix is a "deleted" (DEFUNCT) marker.
- ab = 23, c = 6, de = 15 → (300 + 0 + 15) mod 1000 = 315.
- N = 12: all go to 0 (collisions). N = 11: 0, 1, 2, 3.
Summary
Exercises
- With N = 11, h(k) = k mod 11 and h₂(k) = 7 − (k mod 7), use double hashing: which slots do 14, 25, 36, 3 end up in?
- A chained table with N = 5 holds the keys 12, 7, 20, 33. Which buckets do they move to when it grows to N = 11?
- Under linear probing with λ = 0.8, how many probes does an unsuccessful search make on average?
- Write a loop that uses a
HashMapto count how often each word occurs in an array of words. - How does the table behave if
hashCode()returns 42 for every object? - What are
index(32, 16)andindex(65568, 16)? And without mixing?
Answers and hints
- Slots probed: 14→3; 25→3, 6; 36→3, 9; 3→3, 7.
- k mod 11: 12→1, 7→7, 20→9, 33→0 (every key is rehashed).
- ½(1 + 1/(1 − 0.8)²) = ½(1 + 25) = 13.
for (String w : words) f.put(w, f.getOrDefault(w, 0) + 1);→ O(n) on average.- Everything lands in one bucket, so operations are O(n). Java 8+ turns that bucket into a tree (O(log n) if the keys are Comparable).
- 0 and 1; without mixing, both are 0 (a collision).
Summary
Further Reading
- Goodrich, Tamassia, and Goldwasser, Data Structures and Algorithms in Java, 6th ed., 2014.
- Cormen, Leiserson, Rivest, and Stein, Introduction to Algorithms, 4th ed., 2022.
- Oracle, Java SE 17 API documentation for
Map,HashMap, andHashSet.