Chapter 12: Maps and
Hash Tables

Data Structures
The map ADTDirect addressingHash functionsCollisions Separate chainingOpen addressingLoad factorResizing
Sercan KÜLCÜ · Giresun University · Computer Engineering

Introduction

Learning Outcomes

The map model

Explain key–value association and how a hash table works.

Collisions

Resolve collisions with chaining and with open addressing.

Load factor

Relate load factor, resizing, and the spread of keys.

Correctness and cost

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.
h(key)0001521-897602521-123403⋮⋮1314521-965515Ali KayaAyşe DemirCan Öz
A map is an abstract data type (it says what to do); a hash table is the most common data structure that implements it (it says how).

Maps and the Idea of Hashing

Core Operations of the Map ADT

Operationjava.util.MapDescription
Insertionput(k, v)Adds the pair to the table
Searchget(k)Returns the value for key k, or null if absent
Deletionremove(k)Removes the entry with key k and returns its value
Updateput(k, v′)If k is present, replaces its value with v′ (returns the old one)
Othersize(), 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
The idea: if we can turn a key directly into an array index, we reach the right cell in one step, with no comparisons.

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.

Key space1356790112334556677899A[k] = k (key = index)

Each key k in the key space goes into cell A[k]; unused cells stay empty.

Key space1356790112334556677899A[k] = k (key = index)search(3)A[3] occupied → found ✓

search(3): look straight at A[3] → occupied → found, O(1).

Key space1356790112334556677899A[k] = k (key = index)search(0)A[0] empty → not found ✗

search(0): look at A[0] → empty → not found, still O(1).

Maps and the Idea of Hashing

Direct Addressing and Its Limits

0123456…2829AliBeyzaDenizOnur
  • 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.
The fix: a function that squeezes the large key space into a small array → a hash function.

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.
key"Ali"hash functionh(key)index8678910buckets

"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.
DeterministicThe same key must always give the same index, or the entry can never be found again.
FastIt should run in O(1) (treating key length as constant).
UniformIt should spread keys evenly over the buckets, which keeps collisions down.
Uses the whole keyA function that looks only at the first letter sends "Ali", "Ayşe" and "Ahmet" to the same bucket.

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

key"Ali"hash code65918compressionmod 13index8any int (may be negative)0 … N−1String, Integer, object…
Division methodh(k) = k mod N
Choosing a prime N keeps regular patterns in the keys (such as multiples of 10) from piling into the same buckets.
MAD (multiply-add-divide)h(k) = ((a·k + b) mod p) mod N
p > N is prime; a and b are random (a ≠ 0).
kk mod 10k mod 11
10010
2009
3008
4007
5006

N = 10: all in one bucket. N = 11 (prime): each in a different bucket.

Hash Functions

The equals and hashCode Contract

Equal objectsIf a.equals(b) is true, then a.hashCode() == b.hashCode() must hold; otherwise equal keys land in different buckets.
Same hash codeTwo objects with the same hash code are not necessarily equal. Inside the bucket, the hash table still compares with equals.
Override bothIf you override equals, override hashCode too, using the same fields.
Mutable keysIf the fields behind equals/hashCode change after insertion, the entry is looked for in the wrong bucket.
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+)
In a hash table, 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 (!)
0123(1,2) → "A"4567

put(p1, "A"): p1.hashCode() picks the bucket. Point doesn't override it, so Object.hashCode() (object identity) is used; say, bucket 3.

0123(1,2) → "A"456null → get returns null7

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!

012(1,2) → "A"34567

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

Modulus (division remainder)
int modulusHash(int key, int arraySize) {
  return Math.floorMod(key, arraySize);
}

floorMod gives a safe index in 0 … arraySize−1 even for negative keys.

CRC32
long crc32Hash(String key) {
  CRC32 crc = new CRC32();  // java.util.zip
  crc.update(key.getBytes());
  return crc.getValue();
}
DJB2 (strings)
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.

Java: String.hashCode()
// Σ 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()

i = 0A65i = 1l108i = 2i105code

hash("Ali") is called; each character is processed by its Unicode code (A = 65, l = 108, i = 105).

i = 0A65i = 1l108i = 2i105codeh = 0

Start with h = 0.

i = 0A65i = 1l108i = 2i105codeh = 0

0 < 3 → the loop continues; the character is s.charAt(0) = 'A' (65).

i = 0A65i = 1l108i = 2i105codeh = 31 · 0 + 65= 65

h = 31 · 0 + 65 = 65. The running total is multiplied by 31, so the character's position matters too.

i = 0A65i = 1l108i = 2i105codeh = 65

1 < 3 → the loop continues; the character is s.charAt(1) = 'l' (108).

i = 0A65i = 1l108i = 2i105codeh = 31 · 65 + 108= 2123

h = 31 · 65 + 108 = 2123. The running total is multiplied by 31, so the character's position matters too.

i = 0A65i = 1l108i = 2i105codeh = 2123

2 < 3 → the loop continues; the character is s.charAt(2) = 'i' (105).

i = 0A65i = 1l108i = 2i105codeh = 31 · 2123 + 105= 65918

h = 31 · 2123 + 105 = 65918. The running total is multiplied by 31, so the character's position matters too.

i = 0A65i = 1l108i = 2i105codeh = 65918

3 < 3 is false → the loop ends.

i = 0A65i = 1l108i = 2i105code65·31² + 108·31 + 105= 65918

return 65918 = 65·31² + 108·31 + 105.

i = 0A65i = 1l108i = 2i105codecode = 65918

code = 65918. For long strings the product overflows int and the result can be negative.

i = 0A65i = 1l108i = 2i105code65918 mod 13= 8

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()

table0123456789

An empty 10-cell table; every cell is null.

table0123456789

Next key: k = 5.

table0123456789▲ j

modulusHash(5, 10): 5 mod 10 = 5 (the remainder).

table01234556789▲ j

table[5] = 5.

table01234556789

Next key: k = 1.

table01234556789▲ j

modulusHash(1, 10): 1 mod 10 = 1 (the remainder).

table011234556789▲ j

table[1] = 1.

table011234556789

Next key: k = 10.

table011234556789▲ j

modulusHash(10, 10): 10 mod 10 = 0 (the remainder).

table01011234556789▲ j

table[0] = 10.

table01011234556789

Next key: k = 26.

table01011234556789▲ j

modulusHash(26, 10): 26 mod 10 = 6 (the remainder).

table0101123455626789▲ j

table[6] = 26.

table0101123455626789

Next key: k = 99.

table0101123455626789▲ j

modulusHash(99, 10): 99 mod 10 = 9 (the remainder).

table010112345562678999▲ j

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:
010, Leyla11, Tuna23455, Ali626, Ceren78999, Sena5, Ali1, Tuna10, Leyla26, Ceren99, Sena105, Murat105 % 10 = 5 → collision!
Separate chainingEach bucket holds a linked list of the entries that hash to it.
Open addressingAn entry that hits an occupied slot looks for another empty slot in the array.

Separate Chaining

Resolving Collisions: Separate Chaining

0null1null2null3null4null55"Ali"null6null7null8null9nullHashNode[] bucketsh(k) = k % 105 % 10 = 5

h(5) = 5 % 10 = 5 → bucket 5 is empty; node (5, \"Ali\") is linked in.

0null11"Tuna"null2null3null4null55"Ali"null6null7null8null9nullHashNode[] bucketsh(k) = k % 105 % 10 = 51 % 10 = 1

h(1) = 1 % 10 = 1 → bucket 1 is empty; node (1, \"Tuna\") is linked in.

010"Leyla"null11"Tuna"null2null3null4null55"Ali"null6null7null8null9nullHashNode[] bucketsh(k) = k % 105 % 10 = 51 % 10 = 110 % 10 = 0

h(10) = 10 % 10 = 0 → bucket 0 is empty; node (10, \"Leyla\") is linked in.

010"Leyla"null11"Tuna"null2null3null4null55"Ali"null626"Ceren"null7null8null9nullHashNode[] bucketsh(k) = k % 105 % 10 = 51 % 10 = 110 % 10 = 026 % 10 = 6

h(26) = 26 % 10 = 6 → bucket 6 is empty; node (26, \"Ceren\") is linked in.

010"Leyla"null11"Tuna"null2null3null4null55"Ali"null626"Ceren"null7null8null999"Sena"nullHashNode[] bucketsh(k) = k % 105 % 10 = 51 % 10 = 110 % 10 = 026 % 10 = 699 % 10 = 9

h(99) = 99 % 10 = 9 → bucket 9 is empty; node (99, \"Sena\") is linked in.

010"Leyla"null11"Tuna"null2null3null4null5105"Murat"5"Ali"null626"Ceren"null7null8null999"Sena"nullHashNode[] bucketsh(k) = k % 105 % 10 = 51 % 10 = 110 % 10 = 026 % 10 = 699 % 10 = 9105 % 10 = 5

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

010"Leyla"null11"Tuna"null2null3null4null5105"Murat"5"Ali"null626"Ceren"null7null8null999"Sena"nullHashNode[] bucketsbucketsHashNode[] arraybucketone cell of the arraynumBuckets = 10array length = capacitysize = 6number of stored pairschainnodes in the same bucketheadfirst node of the chain↑ HashNodehead ↓

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;
    }
  }
}
bucketsEach cell holds the first node of a chain.
HashNodekey + value + next: a singly linked list node.
new HashTable(10)Creates 10 buckets, all 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);
}
keykey % 10Math.floorMod(key, 10)Result
10555bucket 5
2111bucket 1
4111bucket 1 (collides with 21)
−7−73with %, buckets[-7] → ArrayIndexOutOfBoundsException
In Java, the result of % 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)

key410123456789

getBucketIndex(41) is called; the table has 10 buckets.

key410123456789

Integer.hashCode() is just the number: h = 41.

key410123456789

floorMod(41, 10) is called.

key410123456789▲ r

In Java, 41 % 10 = 1.

key410123456789▲ r

r = 1 ≥ 0 → no correction needed.

key410123456789▲ r

return 1.

key410123456789▲ r

Bucket index 1. (21 also lands in bucket 1: a collision.)

key-70123456789

getBucketIndex(-7) is called; the table has 10 buckets.

key-70123456789

Integer.hashCode() is just the number: h = -7.

key-70123456789

floorMod(-7, 10) is called.

key-70123456789◄ r = -7: buckets[-7] is out of bounds ✗

In Java, -7 % 10 = -7: the result takes the sign of the dividend.

key-70123456789▲ r

r < 0 → r += 10 → r = 3, now in the range 0 … 9.

key-70123456789▲ r

return 3.

key-70123456789▲ r

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
}
1. Find the bucketO(1) with h(key).
2. Scan the chainIf the key is there, only the value changes; size stays the same.
3. Insert at the headOtherwise a new node goes at the head of the chain: O(1).

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
}
01null2345105"Murat"null6789

put(21, \"Leyla\"): h(21) = 21 mod 10 = 1 → only bucket 1 is examined.

01null▲ head2345105"Murat"null6789

Bucket 1 is empty: head = null.

01null▲ head2345105"Murat"null6789

head == null → the loop ends: 21 is not in the table; we need a new node.

01null2345105"Murat"null6789

New pair: size = 2.

01null2345105"Murat"null678921"Leyla"▲ node

A new node (21, \"Leyla\") is created; its next is still null, so it isn't in the table yet.

01null2345105"Murat"null678921"Leyla"null▲ node

node.next = buckets[1] = null (the bucket was empty).

0121"Leyla"null2345105"Murat"null6789

buckets[1] = node: the new node is at the head of the chain. No need to walk to the end → O(1).

0121"Leyla"null2345105"Murat"null6789

put(41, \"Sena\"): h(41) = 41 mod 10 = 1 → only bucket 1 is examined.

0121"Leyla"null▲ head2345105"Murat"null6789

head points to the first node of the chain.

0121"Leyla"null▲ head2345105"Murat"null6789

head != null → enter the loop to examine this node.

0121"Leyla"null▲ head2345105"Murat"null6789

21 ≠ 41 → no match; move on.

0121"Leyla"null▲ head2345105"Murat"null6789

head = head.next → null (end of the chain).

0121"Leyla"null▲ head2345105"Murat"null6789

head == null → the loop ends: 41 is not in the table; we need a new node.

0121"Leyla"null2345105"Murat"null6789

New pair: size = 3.

0121"Leyla"null2345105"Murat"null678941"Sena"▲ node

A new node (41, \"Sena\") is created; its next is still null, so it isn't in the table yet.

0121"Leyla"null2345105"Murat"null678941"Sena"▲ node

node.next points to the old head (21), so the existing chain is kept.

0141"Sena"21"Leyla"null2345105"Murat"null6789

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
}
0141"Sena"21"Leyla"null2345105"Murat"null6789

put(21, \"Ali\"): h(21) = 21 mod 10 = 1 → only bucket 1 is examined.

0141"Sena"21"Leyla"null▲ head2345105"Murat"null6789

head points to the first node of the chain.

0141"Sena"21"Leyla"null▲ head2345105"Murat"null6789

head != null → enter the loop to examine this node.

0141"Sena"21"Leyla"null▲ head2345105"Murat"null6789

41 ≠ 21 → no match; move on.

0141"Sena"21"Leyla"null▲ head2345105"Murat"null6789

head = head.next → node 21.

0141"Sena"21"Leyla"null▲ head2345105"Murat"null6789

head != null → enter the loop to examine this node.

0141"Sena"21"Leyla"null▲ head2345105"Murat"null6789

21.equals(21) → equal: the key is already in the table.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

The value is updated: "Leyla" → "Ali". No new node is added.

0141"Sena"21"Ali"null2345105"Murat"null6789

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
}
Just one bucketNo other bucket is ever examined.
CostO(1 + chain length). The average chain length is n/N.
CallBucketNodes examinedReturns
table.get(105)105 % 10 = 5105"Murat"
table.get(21)21 % 10 = 141, 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
}
0141"Sena"21"Ali"null2345105"Murat"null6789

get(105): h(105) = 105 mod 10 = 5. No other bucket is examined.

0141"Sena"21"Ali"null2345105"Murat"null▲ head6789

head points to the bucket's first node.

0141"Sena"21"Ali"null2345105"Murat"null▲ head6789

head != null → enter the loop body.

0141"Sena"21"Ali"null2345105"Murat"null▲ head6789

105.equals(105) → equal.

0141"Sena"21"Ali"null2345105"Murat"null▲ head6789

return "Murat": one comparison was enough.

0141"Sena"21"Ali"null2345105"Murat"null6789

get(21): h(21) = 21 mod 10 = 1. No other bucket is examined.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

head points to the bucket's first node.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

head != null → enter the loop body.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

41 ≠ 21 → another key that landed in the same bucket (a collision).

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

head = head.next → node 21.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

head != null → enter the loop body.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

21.equals(21) → equal.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

return "Ali": two comparisons were enough.

0141"Sena"21"Ali"null2345105"Murat"null678null9

get(88): h(88) = 88 mod 10 = 8. No other bucket is examined.

0141"Sena"21"Ali"null2345105"Murat"null678null▲ head9

Bucket 8 is empty: head = null.

0141"Sena"21"Ali"null2345105"Murat"null678null▲ head9

head == null → the loop body never runs.

0141"Sena"21"Ali"null2345105"Murat"null678null▲ head9

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;
}
prevUnlinking a node from a singly linked list requires the node before it.
Two casesIf the node is the head, update the bucket; otherwise update prev.next.
CostSame as lookup: O(1 + chain length).

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;
}
0141"Sena"21"Ali"null2345105"Murat"null6789

remove(21): h(21) = 21 mod 10 = 1.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

head points to the first node.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

prev = null: to unlink a node from a singly linked list we need the node before it.

0141"Sena"21"Ali"null▲ head2345105"Murat"null6789

41 ≠ 21 → the condition holds; enter the loop body.

0141"Sena"21"Ali"null▲ prev2345105"Murat"null6789

prev = head → node 41.

0141"Sena"21"Ali"null▲ head▲ prev2345105"Murat"null6789

head = head.next → node 21.

0141"Sena"21"Ali"null▲ head▲ prev2345105"Murat"null6789

21.equals(21) → equal, so the loop ends: we've found the node to remove.

0141"Sena"21"Ali"null▲ head▲ prev2345105"Murat"null6789

head != null → the key is present; remove it.

0141"Sena"21"Ali"null▲ head▲ prev2345105"Murat"null6789

size = 2.

0141"Sena"21"Ali"null▲ head▲ prev2345105"Murat"null6789

prev != null → the node is in the middle or at the end: bypass it through prev.

0141"Sena"null▲ prev2345105"Murat"null678921"Ali"▲ head (unlinked)

prev.next = head.next (null): 41 now skips 21, which is unlinked from the chain.

0141"Sena"null2345105"Murat"null678921"Ali"▲ removed

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;
}
0141"Sena"null2345105"Murat"null6789

remove(105): h(105) = 105 mod 10 = 5.

0141"Sena"null2345105"Murat"null▲ head6789

head points to the first node.

0141"Sena"null2345105"Murat"null▲ head6789

prev = null: to unlink a node from a singly linked list we need the node before it.

0141"Sena"null2345105"Murat"null▲ head6789

105.equals(105) → equal, so the loop ends: we've found the node to remove.

0141"Sena"null2345105"Murat"null▲ head6789

head != null → the key is present; remove it.

0141"Sena"null2345105"Murat"null▲ head6789

size = 1.

0141"Sena"null2345105"Murat"null▲ head6789

prev == null → the node to remove is the head of the chain: take the else branch.

0141"Sena"null2345null6789105"Murat"▲ head (unlinked)

buckets[5] = head.next (null): the bucket no longer points to 105.

0141"Sena"null2345null6789105"Murat"▲ removed

return \"Murat\". The garbage collector reclaims the unlinked node.

0141"Sena"null2345678null9

remove(88): h(88) = 88 mod 10 = 8.

0141"Sena"null2345678null▲ head9

Bucket 8 is empty: head = null.

0141"Sena"null2345678null▲ head9

prev = null: to unlink a node from a singly linked list we need the node before it.

0141"Sena"null2345678null▲ head9

head == null → the loop ends immediately.

0141"Sena"null2345678null▲ head9

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, …
Methodf(i)Probe sequence (h(k) = 5, N = 13)Weakness
Linear probingi5, 6, 7, 8, 9, …Primary clustering
Quadratic probingi²5, 6, 9, 14→1, 21→8, …Secondary clustering; may not try every slot
Double hashingi · h₂(k)5, 5+d, 5+2d, … (d = h₂(k))Needs h₂(k) ≠ 0 and the step coprime to N
Compared with chaining: no extra nodes or references, and memory is contiguous (cache-friendly). But: the load factor must stay low, and deletion needs special care. If N is prime, every step from 1 to N−1 is coprime to N.

Open Addressing · Step through the code

Linear Probing

order:18412244593231730124134518678922101112

18 → 5, 41 → 2, 22 → 9: all three find an empty slot on the first try. Next up: 44.

order:18412244593231730124134518678922101112▲ h

Next key 44: h = 44 mod 13 = 5 (home slot).

order:18412244593231730124134518678922101112▲ h, j

i = 0: j = (5 + 0) mod 13 = 5 → occupied (18); next i.

order:18412244593231730124134518678922101112▲ h▲ j

i = 1: j = (5 + 1) mod 13 = 6 → empty.

order:1841224459323173012413451864478922101112▲ h▲ j

table[6] = 44, break (2 probes).

order:1841224459323173012413451864478922101112▲ h

Next key 59: h = 59 mod 13 = 7 (home slot).

order:1841224459323173012413451864478922101112▲ h, j

i = 0: j = (7 + 0) mod 13 = 7 → empty.

order:184122445932317301241345186447598922101112▲ h, j

table[7] = 59, break (1 probe).

order:184122445932317301241345186447598922101112▲ h

Next key 32: h = 32 mod 13 = 6 (home slot).

order:184122445932317301241345186447598922101112▲ h, j

i = 0: j = (6 + 0) mod 13 = 6 → occupied (44); next i.

order:184122445932317301241345186447598922101112▲ h▲ j

i = 1: j = (6 + 1) mod 13 = 7 → occupied (59); next i.

order:184122445932317301241345186447598922101112▲ h▲ j

i = 2: j = (6 + 2) mod 13 = 8 → empty.

order:18412244593231730124134518644759832922101112▲ h▲ j

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

order:18412244593231730124134518644759832922101112▲ h

Next key 31: h = 31 mod 13 = 5 (home slot).

order:18412244593231730124134518644759832922101112▲ h, j

i = 0: j = (5 + 0) mod 13 = 5 → occupied (18); next i.

order:18412244593231730124134518644759832922101112▲ h▲ j

i = 1: j = (5 + 1) mod 13 = 6 → occupied (44); next i.

order:18412244593231730124134518644759832922101112▲ h▲ j

i = 2: j = (5 + 2) mod 13 = 7 → occupied (59); next i.

order:18412244593231730124134518644759832922101112▲ h▲ j

i = 3: j = (5 + 3) mod 13 = 8 → occupied (32); next i.

order:18412244593231730124134518644759832922101112▲ h▲ j

i = 4: j = (5 + 4) mod 13 = 9 → occupied (22); next i.

order:18412244593231730124134518644759832922101112▲ h▲ j

i = 5: j = (5 + 5) mod 13 = 10 → empty.

order:1841224459323173012413451864475983292210311112▲ h▲ j

table[10] = 31, break (6 probes).

order:1841224459323173012413451864475983292210311112▲ h

Next key 73: h = 73 mod 13 = 8 (home slot).

order:1841224459323173012413451864475983292210311112▲ h, j

i = 0: j = (8 + 0) mod 13 = 8 → occupied (32); next i.

order:1841224459323173012413451864475983292210311112▲ h▲ j

i = 1: j = (8 + 1) mod 13 = 9 → occupied (22); next i.

order:1841224459323173012413451864475983292210311112▲ h▲ j

i = 2: j = (8 + 2) mod 13 = 10 → occupied (31); next i.

order:1841224459323173012413451864475983292210311112▲ h▲ j

i = 3: j = (8 + 3) mod 13 = 11 → empty.

order:184122445932317301241345186447598329221031117312▲ h▲ j

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

order:18412244593231730124134518678922101112

18 → 5, 41 → 2, 22 → 9: all three find an empty slot on the first try. Next up: 44.

order:18412244593231730124134518678922101112▲ h

Next key 44: h = 44 mod 13 = 5 (home slot).

order:18412244593231730124134518678922101112▲ h, j

i = 0: j = (5 + 0²) mod 13 = 5 → occupied (18); next i.

order:18412244593231730124134518678922101112▲ h▲ j

i = 1: j = (5 + 1²) mod 13 = 6 → empty.

order:1841224459323173012413451864478922101112▲ h▲ j

table[6] = 44, break (2 probes).

order:1841224459323173012413451864478922101112▲ h

Next key 59: h = 59 mod 13 = 7 (home slot).

order:1841224459323173012413451864478922101112▲ h, j

i = 0: j = (7 + 0²) mod 13 = 7 → empty.

order:184122445932317301241345186447598922101112▲ h, j

table[7] = 59, break (1 probe).

order:184122445932317301241345186447598922101112▲ h

Next key 32: h = 32 mod 13 = 6 (home slot).

order:184122445932317301241345186447598922101112▲ h, j

i = 0: j = (6 + 0²) mod 13 = 6 → occupied (44); next i.

order:184122445932317301241345186447598922101112▲ h▲ j

i = 1: j = (6 + 1²) mod 13 = 7 → occupied (59); next i.

order:184122445932317301241345186447598922101112▲ h▲ j

i = 2: j = (6 + 2²) mod 13 = 10 → empty.

order:18412244593231730124134518644759892210321112▲ h▲ j

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

order:18412244593231730124134518644759892210321112▲ h

Next key 31: h = 31 mod 13 = 5 (home slot).

order:18412244593231730124134518644759892210321112▲ h, j

i = 0: j = (5 + 0²) mod 13 = 5 → occupied (18); next i.

order:18412244593231730124134518644759892210321112▲ h▲ j

i = 1: j = (5 + 1²) mod 13 = 6 → occupied (44); next i.

order:18412244593231730124134518644759892210321112▲ h▲ j

i = 2: j = (5 + 2²) mod 13 = 9 → occupied (22); next i.

order:18412244593231730124134518644759892210321112▲ h▲ j

i = 3: j = (5 + 3²) mod 13 = 1 → empty.

order:1841224459323173013124134518644759892210321112▲ h▲ j

table[1] = 31, break (4 probes). The jumps grow, so the probe escapes the cluster quickly.

order:1841224459323173013124134518644759892210321112▲ h

Next key 73: h = 73 mod 13 = 8 (home slot).

order:1841224459323173013124134518644759892210321112▲ h, j

i = 0: j = (8 + 0²) mod 13 = 8 → empty.

order:184122445932317301312413451864475987392210321112▲ h, j

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.

k1841224459323173
h(k) = k mod 1352957658
h₂(k) (on collision)———7−2 = 5——7−3 = 4—
Slots probed5295, 10765, 9, 08
03112413451863275987392210441112
44: 5 occupied → 5+5 = 10.   31: 5 occupied → 5+4 = 9 occupied → 5+8 = 13 mod 13 = 0. Keys with the same home slot advance by different steps, so clustering drops.

Open Addressing · Step through the code

Code Trace: Double Hashing

order:18412244593231730124134518678922101112

18 → 5, 41 → 2, 22 → 9: all three find an empty slot on the first try. Next up: 44.

order:18412244593231730124134518678922101112▲ h

h = 44 mod 13 = 5 (home slot), d = 7 − 2 = 5 (step size, never 0).

order:18412244593231730124134518678922101112▲ h, j

i = 0: j = (5 + 0·5) mod 13 = 5 → occupied (18); next i.

order:18412244593231730124134518678922101112▲ h▲ j

i = 1: j = (5 + 1·5) mod 13 = 10 → empty.

order:1841224459323173012413451867892210441112▲ h▲ j

table[10] = 44, break (2 probes).

order:1841224459323173012413451867892210441112▲ h

h = 59 mod 13 = 7 (home slot), d = 7 − 3 = 4 (step size, never 0).

order:1841224459323173012413451867892210441112▲ h, j

i = 0: j = (7 + 0·4) mod 13 = 7 → empty.

order:184122445932317301241345186759892210441112▲ h, j

table[7] = 59, break (1 probe).

order:184122445932317301241345186759892210441112▲ h

h = 32 mod 13 = 6 (home slot), d = 7 − 4 = 3 (step size, never 0).

order:184122445932317301241345186759892210441112▲ h, j

i = 0: j = (6 + 0·3) mod 13 = 6 → empty.

order:18412244593231730124134518632759892210441112▲ h, j

table[6] = 32, break (1 probe).

order:18412244593231730124134518632759892210441112▲ h

h = 31 mod 13 = 5 (home slot), d = 7 − 3 = 4 (step size, never 0).

order:18412244593231730124134518632759892210441112▲ h, j

i = 0: j = (5 + 0·4) mod 13 = 5 → occupied (18); next i.

order:18412244593231730124134518632759892210441112▲ h▲ j

i = 1: j = (5 + 1·4) mod 13 = 9 → occupied (22); next i.

order:18412244593231730124134518632759892210441112▲ h▲ j

i = 2: j = (5 + 2·4) mod 13 = 0 → empty.

order:1841224459323173031124134518632759892210441112▲ h▲ j

table[0] = 31, break (3 probes).

order:1841224459323173031124134518632759892210441112▲ h

h = 73 mod 13 = 8 (home slot), d = 7 − 3 = 4 (step size, never 0).

order:1841224459323173031124134518632759892210441112▲ h, j

i = 0: j = (8 + 0·4) mod 13 = 8 → empty.

order:184122445932317303112413451863275987392210441112▲ h, j

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

01241345186447598329221031117312

The table after linear probing. We want to delete 44.

01241345186447598329221031117312○i=0✓i=1

remove(44): h(44) = 5 → 18 ≠ 44 → slot 6: found 44. What if we just set the slot to null?

012413451867598329221031117312○i=0

Wrong: slot 6 is emptied. get(32): h(32) = 6 → empty slot → reports "absent". But 32 is sitting in slot 8!

01241345186✗7598329221031117312

Right: mark the slot with a special deleted marker (DEFUNCT, a tombstone ✗).

01241345186✗7598329221031117312○i=0○i=1✓i=2

get(32): h = 6 → ✗ deleted, keep searching → 7: 59 ≠ 32 → 8: found 32.

01241345186577598329221031117312○i=0○i=1○i=2○i=3○i=4○i=5○i=6○i=7

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
}
get(k)j = findSlot(k); if j ≥ 0, return table[j].value, otherwise null.
put(k, v)If j ≥ 0, update; otherwise store the new pair in slot table[−(j+1)].
Other probesQuadratic: (h + i*i) % N
Double hashing: (h + i*h2) % N

The 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 (✗)

01241345186✗7598329221031117312▲ h

findSlot(32): h = 32 mod 13 = 6; no available slot yet (−1).

01241345186✗7598329221031117312▲ h, j

i = 0: j = 6 → the slot is DEFUNCT (✗).

01241345186✗7598329221031117312▲ h, avail, j

First empty or deleted slot: avail = 6 (an insert would go here).

01241345186✗7598329221031117312▲ h, avail, j

Deleted ≠ empty: keep searching (32 may lie beyond).

01241345186✗7598329221031117312▲ h, avail▲ j

i = 1: j = 7 → occupied (59 ≠ 32); next slot.

01241345186✗7598329221031117312▲ h, avail▲ j

i = 2: j = 8 → occupied, and table[8].key = 32 == 32.

01241345186✗7598329221031117312▲ h, avail▲ j

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

01241345186✗7598329221031117312▲ h

findSlot(57): h = 57 mod 13 = 5; no available slot yet (−1).

01241345186✗7598329221031117312▲ h, j

i = 0: j = 5 → occupied (18 ≠ 57); next slot.

01241345186✗7598329221031117312▲ h▲ j

i = 1: j = 6 → the slot is DEFUNCT (✗).

01241345186✗7598329221031117312▲ h▲ avail, j

First empty or deleted slot: avail = 6 (an insert would go here).

01241345186✗7598329221031117312▲ h▲ avail, j

Deleted ≠ empty: keep searching (57 may lie beyond).

01241345186✗7598329221031117312▲ h▲ avail▲ j

i = 2: j = 7 → occupied (59 ≠ 57); next slot.

01241345186✗7598329221031117312▲ h▲ avail▲ j

i = 3: j = 8 → occupied (32 ≠ 57); next slot.

01241345186✗7598329221031117312▲ h▲ avail▲ j

i = 4: j = 9 → occupied (22 ≠ 57); next slot.

01241345186✗7598329221031117312▲ h▲ avail▲ j

i = 5: j = 10 → occupied (31 ≠ 57); next slot.

01241345186✗7598329221031117312▲ h▲ avail▲ j

i = 6: j = 11 → occupied (73 ≠ 57); next slot.

01241345186✗7598329221031117312▲ h▲ avail▲ j

i = 7: j = 12 → the slot is empty.

01241345186✗7598329221031117312▲ h▲ avail▲ j

avail is already 6 and stays (we keep the first available slot).

01241345186✗7598329221031117312▲ h▲ avail▲ j

A truly empty slot → 57 cannot lie beyond it: break.

01241345186✗7598329221031117312▲ h▲ avail

57 is absent: return −(6 + 1) = -7 (negative means "absent" and encodes where to insert).

01241345186577598329221031117312▲ avail

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.
024681000,250,50,751load factor λ1,52,52,58,55,5↑ 50,5unsuccessfulsuccessfulchaining 1 + λ (dashed)

Expected probes under linear probing (Knuth):
successful ½(1 + 1/(1−λ))  ·  unsuccessful ½(1 + 1/(1−λ)²)

With chaining the expected cost is O(1 + λ): keep λ constant (grow the table along with n) and insert, search and delete are O(1) on average.

Performance and Applications

Resizing and Rehashing

0201null27123334nullN = 5, h(k) = k mod 512 → 2, 7 → 2, 20 → 0, 33 → 3λ = 4 / 5 = 0.80 > 0.75→ grow the table

12, 7, 20 and 33 are inserted into a table with N = 5 buckets. λ = 0.80 exceeds the 0.75 threshold → time to resize.

0331122null3null4null5null6null778null92010nullNew N = 11 (first prime above 2·5)20 mod 11 = 97 mod 11 = 712 mod 11 = 133 mod 11 = 0

A new array is allocated and every key is rehashed: indices change, so the old positions are meaningless.

0331122null3null4null5null6null778null92010nullλ = 4 / 11 ≈ 0,36no collisions leftcost: O(n), onceamortized: O(1) per insert

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;
    }
  }
}
old (N = 5)020127123334

Called because λ = 4/5 = 0.8 > 0.75. old holds the old array.

old (N = 5)020127123334

newN = 2·5 + 1 = 11: about twice as large, and odd (prime, in this case).

old (N = 5)020127123334buckets (N = 11)012345678910

buckets now points to the new array of 11 empty buckets.

old (N = 5)020127123334buckets (N = 11)012345678910

size = 0: put will count every pair again.

old (N = 5)020127123334buckets (N = 11)012345678910

Old bucket 0: head = node 20.

old (N = 5)020127123334buckets (N = 11)01234567892010

put(20): it was 20 mod 5 = 0; now 20 mod 11 = 9 → new bucket 9.

old (N = 5)020127123334buckets (N = 11)01234567892010

head = null: this chain is done.

old (N = 5)020127123334buckets (N = 11)01234567892010

Old bucket 1 is empty: head = null, so the while loop is skipped.

old (N = 5)020127123334buckets (N = 11)01234567892010

Old bucket 2: head = node 7.

old (N = 5)020127123334buckets (N = 11)012345677892010

put(7): it was 7 mod 5 = 2; now 7 mod 11 = 7 → new bucket 7.

old (N = 5)020127123334buckets (N = 11)012345677892010

head = head.next → node 12 (same old chain).

old (N = 5)020127123334buckets (N = 11)01122345677892010

put(12): it was 12 mod 5 = 2; now 12 mod 11 = 1 → new bucket 1.

old (N = 5)020127123334buckets (N = 11)01122345677892010

head = null: this chain is done.

old (N = 5)020127123334buckets (N = 11)01122345677892010

Old bucket 3: head = node 33.

old (N = 5)020127123334buckets (N = 11)0331122345677892010

put(33): it was 33 mod 5 = 3; now 33 mod 11 = 0 → new bucket 0.

old (N = 5)020127123334buckets (N = 11)0331122345677892010

head = null: this chain is done.

old (N = 5)020127123334buckets (N = 11)0331122345677892010

Old bucket 4 is empty: head = null, so the while loop is skipped.

old (N = 5)020127123334buckets (N = 11)0331122345677892010

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 implementationgetputremove
Unsorted array / listO(n)O(n)*O(n)
Sorted array (binary search)O(log n)O(n)O(n)
Hash table: averageO(1)O(1)O(1)
Hash table: worst caseO(n)O(n)O(n)

* The whole list must be scanned to check whether the key is already present.

Worst caseIf every key lands in the same bucket, we get one long chain (or cluster): O(n).
SpaceO(n + N): the bucket array plus the pairs. Chaining adds one reference per node.
No orderFinding the smallest key or iterating in order costs O(n); for ordered access, use search trees.

Performance and Applications

How Java's HashMap Works Inside

[ ]0•123•45678•91011•12131415173349bucket 1: linked list(17, 33, 49 → low 4 bits 0001)bucket 9: chain longer than 8 (if n ≥ 64) →red-black treeindex = (n − 1) & (h ^ (h >>> 16)), n = 16 = 2⁴past 12 entries (0.75·16) → n = 32
Separate chaining, n = 2kThe bucket count is always a power of 2 (16 by default), so a fast & (n − 1) mask replaces mod.
Bit mixingThe mask sees only the low bits; h ^ (h >>> 16) folds the high bits into the index too.
The λ = 0.75 thresholdOnce the entry count exceeds 0.75·n, the array doubles and the nodes are redistributed.
Treeification (Java 8+)A chain longer than 8 nodes becomes a tree (if n ≥ 64; otherwise the table grows): worst case O(log n).

Performance and Applications · Step through the code

Code Trace: index(k, 16): Mix, Then Mask

high 16 bitslow 16 bits0123456789101112131415dashed:unmixed

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.

high 16 bitslow 16 bitsh00000000000000000000000000000001= 10123456789101112131415dashed:unmixed

Integer.hashCode() is the value itself: h = 1 = 0x00000001. Without mixing, the bucket would be h & 15 = 1 (dashed).

high 16 bitslow 16 bitsh00000000000000000000000000000001= 1h>>>1600000000000000000000000000000000= 0m00000000000000000000000000000001= 10123456789101112131415dashed:unmixed

h >>> 16 = 0: the high half shifts down. XOR gives m = 1; the low 4 bits are now 0001.

high 16 bitslow 16 bitsh00000000000000000000000000000001= 1h>>>1600000000000000000000000000000000= 0m00000000000000000000000000000001= 101123456789101112131415dashed:unmixed

m & 15 = 1 → key 1 goes to bucket 1; print it.

high 16 bitslow 16 bitsh00000000000000010000000000000001= 6553701123456789101112131415dashed:unmixed

Integer.hashCode() is the value itself: h = 65537 = 0x00010001. Without mixing, the bucket would be h & 15 = 1 (dashed).

high 16 bitslow 16 bitsh00000000000000010000000000000001= 65537h>>>1600000000000000000000000000000001= 1m00000000000000010000000000000000= 6553601123456789101112131415dashed:unmixed

h >>> 16 = 1: the high half shifts down. XOR gives m = 65536; the low 4 bits are now 0000.

high 16 bitslow 16 bitsh00000000000000010000000000000001= 65537h>>>1600000000000000000000000000000001= 1m00000000000000010000000000000000= 655360655371123456789101112131415dashed:unmixed

m & 15 = 0 → key 65537 goes to bucket 0; print it.

high 16 bitslow 16 bitsh00000000000000100000000000000001= 1310730655371123456789101112131415dashed:unmixed

Integer.hashCode() is the value itself: h = 131073 = 0x00020001. Without mixing, the bucket would be h & 15 = 1 (dashed).

high 16 bitslow 16 bitsh00000000000000100000000000000001= 131073h>>>1600000000000000000000000000000010= 2m00000000000000100000000000000011= 1310750655371123456789101112131415dashed:unmixed

h >>> 16 = 2: the high half shifts down. XOR gives m = 131075; the low 4 bits are now 0011.

high 16 bitslow 16 bitsh00000000000000100000000000000001= 131073h>>>1600000000000000000000000000000010= 2m00000000000000100000000000000011= 1310750655371123131073456789101112131415dashed:unmixed

m & 15 = 3 → key 131073 goes to bucket 3; print it.

high 16 bitslow 16 bitsh00000000000000100000000000000001= 131073h>>>1600000000000000000000000000000010= 2m00000000000000100000000000000011= 1310750655371123131073456789101112131415dashed:unmixed

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

DictionariesWord → meaning; in spell checking, "is this a word?"
DatabasesFast access by key through hash indexes and hash joins.
In-memory cachesURL or query → precomputed result (e.g., Redis, memcached).
Browser historyVisited address → time of last visit; "have I opened this before?"
Cookie managementCookie name → value; session ID → user data.
CompilersSymbol table: variable name → type, address. In Java: HashMap, HashSet.

Performance and Applications · Step through the code

Application: twoSum([8, 3, 11, 5, 7], 12)

atarget = 1280311125374seen (value → index){ } empty

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.

atarget = 1280311125374▲ ineed = 12 − 8 = 4seen (value → index){ } empty

i = 0: a[0] = 8, so the partner is need = 4. containsKey(4) → false: no 4 seen yet.

atarget = 1280311125374▲ iseen (value → index)8→0

seen.put(8, 0): later elements can find 8 as a partner.

atarget = 1280311125374▲ ineed = 12 − 3 = 9seen (value → index)8→0

i = 1: a[1] = 3, so the partner is need = 9. containsKey(9) → false: no 9 seen yet.

atarget = 1280311125374▲ iseen (value → index)8→03→1

seen.put(3, 1): later elements can find 3 as a partner.

atarget = 1280311125374▲ ineed = 12 − 11 = 1seen (value → index)8→03→1

i = 2: a[2] = 11, so the partner is need = 1. containsKey(1) → false: no 1 seen yet.

atarget = 1280311125374▲ iseen (value → index)8→03→111→2

seen.put(11, 2): later elements can find 11 as a partner.

atarget = 1280311125374▲ ineed = 12 − 5 = 7seen (value → index)8→03→111→2

i = 3: a[3] = 5, so the partner is need = 7. containsKey(7) → false: no 7 seen yet.

atarget = 1280311125374▲ iseen (value → index)8→03→111→25→3

seen.put(5, 3): later elements can find 5 as a partner.

atarget = 1280311125374▲ ineed = 12 − 7 = 5seen (value → index)8→03→111→25→3

i = 4: need = 5, and containsKey(5) is true (O(1) on average) → a[3] + a[4] = 5 + 7 = 12. return [3, 4].

atarget = 1280311125374▲ iseen (value → index)8→03→111→25→3

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

ProblemSpread student IDs evenly over a 1000-element array.
SolutionDesign a hash function that exploits the structure of the ID, cutting collisions.

ID format: ab070c0de  ·  example: 210707003

201102730475060738ab: yearc: dept.de: seq. no.fixed
  • 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

hash(ab070c0de) = ((ab − 20) × 100 + (c − 6) × 500 + de) mod 1000
IDabcdeComputationIndex
21070600321603(1×100 + 0×500 + 3) mod 1000103
24070705024750(4×100 + 1×500 + 50) mod 1000950
23070704223742(3×100 + 1×500 + 42) mod 1000842
Year(ab − 20) ∈ 0…4 selects a block of 100.
Department(c − 6) ∈ 0, 1 shifts by 500.
Sequencede ∈ 0…99 is added directly.
5 years × 2 departments × 100 sequence numbers = 1000 possible IDs, mapped to 1000 different indices: no collisions at all. This is a perfect hash, possible only because we know the structure of the keys.

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(begin, end)end is excluded: substring(5, 6) gives only character 5 (c); substring(7, 9) gives characters 7 and 8 (de).
ExamplecalculateHash("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()

021420374057607580

calculateHash(\"240707050\"): indices 0 … 8.

021420374057607580ab = 24

substring(0, 2) = \"24\" (end excluded) → ab = 24 (enrollment year).

021420374057607580ab = 24c = 7

substring(5, 6) = \"7\" → c = 7 (department).

021420374057607580ab = 24c = 7de = 50

substring(7, 9) = \"50\" → de = 50 (sequence number).

021420374057607580ab = 24c = 7de = 50

year = (24 − 20) · 100 = 400: each year gets a block of 100.

021420374057607580ab = 24c = 7de = 50

dept = (7 − 6) · 500 = 500: department 7 shifts by 500.

021420374057607580ab = 24c = 7de = 50

return (400 + 500 + 50) % 1000 = 950.

021120374056607083

calculateHash(\"210706003\"): indices 0 … 8.

021120374056607083ab = 21

substring(0, 2) = \"21\" (end excluded) → ab = 21 (enrollment year).

021120374056607083ab = 21c = 6

substring(5, 6) = \"6\" → c = 6 (department).

021120374056607083ab = 21c = 6de = 3

substring(7, 9) = \"03\" → de = 3 (sequence number).

021120374056607083ab = 21c = 6de = 3

year = (21 − 20) · 100 = 100: each year gets a block of 100.

021120374056607083ab = 21c = 6de = 3

dept = (6 − 6) · 500 = 0: department 6 is not shifted.

021120374056607083ab = 21c = 6de = 3

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

The map ADTUnique key → value. put, get, remove; a put with an existing key is an update.
Hash functionsHash code + compression (mod N, N prime). Deterministic, fast, uniform.
Separate chainingEach bucket is a linked list. Insert at the head; cost O(1 + λ). λ > 1 is allowed.
Open addressingLinear (primary clustering), quadratic (secondary clustering), double hashing. Deletion uses a "deleted" marker.
Load factor λ = n/NPast the threshold (≈ 0.5–0.75) the table roughly doubles and every key is rehashed.
Correctness and complexityEqual keys must have equal hash codes. O(1) on average, O(n) in the worst case; resizing is amortized O(1).

Summary

Interactive Simulators

Hash Table Simulator →Separate chaining, step-by-step hash computation, visualized collisions, and automatic resizing.
Map Simulator →set, get, has and delete; iterating over keys and tracking insertion order.
Open Addressing Simulator →Linear and quadratic probing and double hashing; probe sequences step by step, the deleted marker (✗), and the load factor.

Summary

Common Mistakes

Math.abs(h) % NMath.abs(Integer.MIN_VALUE) is negative → an index-out-of-bounds error. Use Math.floorMod(h, N).
Overriding only equalsEqual keys land in different buckets and get returns null. Override both.
Mutable keysA key whose hashed fields change after insertion is stranded in the wrong bucket and can't be found.
== on IntegerBoxed values outside −128…127 are separate objects, so == gives false; use equals.
Deleting with null under open addressingIt breaks probe sequences; you need a "deleted" (DEFUNCT) marker.
Expecting orderHashMap does not iterate in insertion order; use LinkedHashMap or TreeMap.

Summary

Sample Problems

  1. With N = 7, h(k) = k mod 7 and linear probing, insert 10, 17, 24, 3, 5 in order. What is the final table?
  2. Insert the same keys with quadratic probing.
  3. A chained table with N = 10 buckets holds 25 pairs. What is λ? How many nodes does an unsuccessful search examine on average?
  4. Under open addressing, why can't we simply set a deleted slot to null?
  5. Compute hash(ab070c0de) for the ID 230706015.
  6. Which buckets do the keys 0, 12, 24, 36 land in under k mod N with N = 12 and with N = 11?
Show answers
  1. 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.
  2. 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.
  3. λ = 25/10 = 2.5; an unsuccessful search examines λ = 2.5 nodes on average (it scans the whole chain).
  4. It breaks the probe sequence: keys placed beyond that slot would be reported absent. The fix is a "deleted" (DEFUNCT) marker.
  5. ab = 23, c = 6, de = 15 → (300 + 0 + 15) mod 1000 = 315.
  6. N = 12: all go to 0 (collisions). N = 11: 0, 1, 2, 3.

Summary

Exercises

  1. 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?
  2. 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?
  3. Under linear probing with λ = 0.8, how many probes does an unsuccessful search make on average?
  4. Write a loop that uses a HashMap to count how often each word occurs in an array of words.
  5. How does the table behave if hashCode() returns 42 for every object?
  6. What are index(32, 16) and index(65568, 16)? And without mixing?
Answers and hints
  1. Slots probed: 14→3; 25→3, 6; 36→3, 9; 3→3, 7.
  2. k mod 11: 12→1, 7→7, 20→9, 33→0 (every key is rehashed).
  3. ½(1 + 1/(1 − 0.8)²) = ½(1 + 25) = 13.
  4. for (String w : words) f.put(w, f.getOrDefault(w, 0) + 1); → O(n) on average.
  5. 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).
  6. 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, and HashSet.

The End

Chapter 12: Maps and Hash Tables
Next deck: Chapter 13: Sets and Disjoint Sets · Study questions · Simulators: Hash Table, Map, Open Addressing · Glossary
1 / 1 Sercan KÜLCÜ. All rights reserved.