Hash Table Collision Resolution with Open Addressing and Probing

Arpit Bhayani

Arpit Bhayani

Jul 15, 2022 • 8 min read

Play

Hash Table Collision Resolution with Open Addressing

In any hash table implementation, collisions are fundamentally inevitable. Because a hash function maps an unbounded universe of application keys into a bounded array of size mm, multiple keys will eventually hash to the exact same slot (by the Pigeonhole Principle).

Two primary paradigms exist for handling these collisions:

  1. Separate Chaining: Storing collided elements in an auxiliary external data structure (e.g., a linked list, balanced binary search tree, or dynamic array) attached to each slot.
  2. Open Addressing: Storing all elements directly inside the hash table array itself without allocating external auxiliary structures.

Open addressing offers substantial advantages in memory efficiency and cache locality. However, managing collisions within the table requires a deterministic search strategy known as probing, and changes the fundamental implementation mechanics of insertion, lookup, and deletion.


The Core Premise of Open Addressing

In separate chaining, every collision incurs memory allocation overhead for pointers and auxiliary nodes. This introduces pointer-chasing and poor CPU cache locality.

Open addressing operates on a straightforward design principle: if the hash table array already has empty slots elsewhere, reuse them rather than allocating external memory.

Open Addressing (All keys stored within array bounds):

Index   Slot State & Value
[0]  -> [ Empty ]
[1]  -> [ Occupied: Key A ]
[2]  -> [ Occupied: Key B ]  <-- Collided with Key A, relocated here
[3]  -> [ Empty ]
[4]  -> [ Occupied: Key C ]

When inserting a key whose primary slot is already occupied, the hash table deterministically searches for the next available slot. Because no keys live outside the array, every key occupies exactly one slot in the table.


Probing Functions: The Heart of Open Addressing

To ensure lookups and insertions remain predictable, alternative slot selection cannot be random. It must follow a strict, deterministic sequence. This sequence is governed by a probing function.

A probing function takes the application key kk and the attempt number ii (where 0i<m0 \le i < m, with mm being the table capacity) and outputs an index in the range [0,m1][0, m-1]:

Index=P(k,i)\text{Index} = P(k, i)

  • Attempt 0 (i=0i = 0): The primary slot where the key naturally maps.
  • Attempt 1 (i=1i = 1): The first fallback slot if the primary slot is occupied.
  • Attempt 2 (i=2i = 2): The second fallback slot if Attempt 1 is also occupied.
  • Attempt m1m - 1: The final attempt covering the last remaining slot in the table.
flowchart LR
    Key[Key k, Attempt i] --> PF[Probing Function: P k, i]
    PF --> Index[Array Index in range 0 to m-1]
    Index --> Check{Slot Occupied?}
    Check -- Yes --> Inc[Increment i = i + 1]
    Inc --> PF
    Check -- No --> Place[Store Key]

Requirements for an Effective Probing Function

A valid probing function must generate a complete permutation of all indices from 00 to m1m - 1 for any given key as ii ranges from 00 to m1m - 1.

If the sequence does not form a complete permutation, cycles may occur that skip open slots, causing insertions to fail prematurely even when empty slots remain in the table.

While the sequence of evaluated indices must be deterministic for a specific key, different keys should ideally yield distinct probing sequences to minimize clustering.


Algorithmic Operations under Open Addressing

1. Insertion (Add)

To insert a new key kk:

  1. Initialize attempt counter i=0i = 0.
  2. Compute index = P(k,i)P(k, i).
  3. Check the slot at index:
    • If the slot is empty (or marked as deleted), write the key to this slot and terminate.
    • If the slot contains the identical key, update the value and terminate.
    • If the slot is occupied by a different key, increment ii+1i \leftarrow i + 1.
  4. Repeat steps 2–3 until an empty slot is found or i=mi = m.
  5. If i=mi = m, the hash table is completely full and requires resizing or eviction.

2. Retrieval (Lookup)

Lookup mirrors the insertion path:

  1. Initialize attempt counter i=0i = 0.
  2. Compute index = P(k,i)P(k, i).
  3. Inspect the slot at index:
    • Match: If the slot holds key kk, return the entry.
    • Empty Slot: If the slot has never been occupied, stop immediately. The key does not exist.
    • Occupied by another key (or marked Deleted): Increment ii+1i \leftarrow i + 1 and continue probing.
  4. Repeat until the key is found, an empty slot is reached, or i=mi = m.

Why Lookup Stops at an Empty Slot

Because open addressing places keys deterministically along the probing sequence without leaving unassigned gaps, hitting an empty slot proves that no subsequent insertion for key kk could have probed past this position. If the key had been inserted, it would have claimed this exact empty slot.

Lookup Termination States

A lookup operation terminates under one of three conditions:

  1. Found: Slot contains key kk.
  2. Definitive Miss: An empty slot is encountered.
  3. Table Exhaustion: i=mi = m attempts have been evaluated without finding the key or an empty slot.

The Deletion Dilemma: Hard Delete vs. Soft Delete

A naive implementation of delete(k) might clear the slot, setting its state back to EMPTY. Doing so breaks the lookup invariant for other keys that collided on that same sequence.

The Broken Invariant Walkthrough

Consider three keys (k1,k2,k3k_1, k_2, k_3) that all hash to index 55 via P(k,0)P(k, 0):

  1. Insert k1k_1: Placed at index 55 (i=0i = 0).
  2. Insert k2k_2: Index 55 is occupied \rightarrow Probes to index 77 (i=1i = 1). Placed at index 77.
  3. Insert k3k_3: Index 55 and 77 are occupied \rightarrow Probes to index 22 (i=2i = 2). Placed at index 22.

Now, perform a hard delete of k2k_2 by setting index 77 to EMPTY:

Index:   [ 2 ]         ...      [ 5 ]         ...      [ 7 ]
State:   Occupied              Occupied               EMPTY
Value:   k3                    k1                     (Cleared!)

Next, perform a lookup for k3k_3:

  • Probe 0 (i=0i = 0): Evaluates index 55. Holds k1k_1 (collision, continue).
  • Probe 1 (i=1i = 1): Evaluates index 77. Finds EMPTY.
  • Failure: The lookup logic halts and reports that k3k_3 does not exist, even though k3k_3 is present at index 22.
sequenceDiagram
    autonumber
    participant Client
    participant Table as Hash Table Slot Array
    Client->>Table: Delete k2 (Hard Delete: Slot 7 -> EMPTY)
    Client->>Table: Lookup k3
    Table->>Table: Probe i=0: Slot 5 contains k1 (Mismatch)
    Table->>Table: Probe i=1: Slot 7 is EMPTY
    Note over Table: Lookup halts immediately at EMPTY slot!
    Table-->>Client: Key k3 Not Found (False Negative!)

The Solution: Soft Deletes with Tombstones

To preserve search chain continuity, open addressing relies on tombstones (soft deletion). A slot must support three distinct states:

  1. EMPTY: The slot has never held a key.
  2. OCCUPIED: The slot currently holds an active key-value pair.
  3. DELETED (Tombstone): The slot previously held a key that has since been removed.
Slot State Machine:

       +-------------- Insert ----------------+
       |                                      |
       v                                      |
  [ EMPTY ] --- Insert ---> [ OCCUPIED ] -----+
                               |              ^
                             Delete         Insert
                               |              |
                               v              |
                          [ DELETED ] --------+

Operational Rules with Tombstones

  • Lookup: Continues probing past DELETED slots just as it would past OCCUPIED slots. It only halts when encountering an EMPTY slot or finding the target key.
  • Insert: Can safely reuse DELETED slots to place new keys, preventing unnecessary table growth.

Architectural Trade-offs and Constraints

AttributeSeparate ChainingOpen Addressing
Auxiliary StorageRequired (pointers, linked list / tree nodes)None (data stored inline in array)
Cache LocalityPoor (pointer-chasing across heap memory)High (sequential or localized array accesses)
Max Load Factor (α=N/M\alpha = N/M)Can exceed 1.01.0 (chains grow arbitrarily)Strictly bounded: α<1.0\alpha < 1.0
Performance DegradationDegrades gracefully (O(1+α)O(1 + \alpha))Degrades sharply as α1.0\alpha \to 1.0
Deletion ComplexitySimple node unlinkingRequires tombstones / soft-delete markers

Load Factor Boundedness

The most critical limitation of open addressing is capacity. In separate chaining, the table can accommodate more elements than its total slot count (N>MN > M). In open addressing, the maximum number of elements is strictly upper-bounded by the table capacity (NMN \le M).

As the load factor approaches 1.01.0, empty slots become rare. Probe sequences lengthen significantly, causing both insertion and lookup performance to degrade from O(1)O(1) toward O(M)O(M) linear scans. Consequently, open-addressed hash tables require proactive resizing and rehashing (typically when the load factor crosses 0.60.6 to 0.750.75) to maintain optimal performance.

Arpit Bhayani

Principal Engineer II at Razorpay - building Agent Studio, Ex-staff engg at GCP Memorystore & Dataproc, Creator of DiceDB, ex-Amazon Fast Data, ex-Director of Engg. SRE and Data Engineering at Unacademy. I spark engineering curiosity through my no-fluff engineering videos on YouTube and my courses