Skip to content

Hopscotch Hashing Lookup

Search in a hash table that maintains elements within a small neighborhood of their home bucket.

Hopscotch hashing maintains the invariant that each key resides within a small fixed distance from its home bucket. This distance is called the neighborhood size. Lookup only scans this bounded region, so it remains fast and cache friendly even at high load.

You search by computing the home index and checking a compact neighborhood using a bitmap or mask that records which nearby slots are occupied by keys belonging to that bucket.

Problem

Given a hopscotch hash table and a key kk, return the associated value if it exists.

Model

  • Table TT of size mm
  • Hash function h(k)h(k)
  • Neighborhood size HH

Each bucket ii owns positions:

[i,i+H1]modm [i, i + H - 1] \bmod m

A bitmap at T[i]T[i] marks which offsets in this range contain keys whose home is ii.

Algorithm

hopscotch_lookup(T, k):
    i = h(k)
    mask = T[i].bitmap

    for offset from 0 to H - 1:
        if mask has bit offset set:
            j = (i + offset) mod m
            if T[j].key == k:
                return T[j].value

    return NOT_FOUND

Example

Let:

  • m=10m = 10
  • H=4H = 4
  • h(k)=kmod10h(k) = k \bmod 10

Suppose bucket 3 has bitmap:

bitmap = 1010

This means keys for bucket 3 appear at offsets 1 and 3:

offsetindex
14
36

Search for a key with home bucket 3:

  • compute i=3i = 3
  • check positions 4 and 6
  • compare keys until match found or exhausted

Correctness

Insertion ensures that every key stays within its home bucket neighborhood and that the bitmap correctly records its position. Lookup reads this bitmap and checks exactly those positions.

If the key exists, it must appear in one of these marked offsets. If none match, the key does not exist.

Complexity

casetime
worstO(H)O(H)
averageO(1)O(1)

Since HH is a fixed constant, lookup runs in constant time.

Space

O(m) O(m)

Each bucket stores a small bitmap in addition to the table entries.

When to Use

Hopscotch hashing is appropriate when:

  • high load factors are required
  • predictable constant time lookup is needed
  • cache locality is important
  • concurrent or lock free variants are desired

It offers better locality and stability than linear probing while keeping lookup bounded.

Implementation

class HopscotchHashTable:
    def __init__(self, m=16, H=4):
        self.m = m
        self.H = H
        self.table = [None] * m
        self.bitmap = [0] * m

    def _hash(self, key):
        return hash(key) % self.m

    def get(self, key):
        i = self._hash(key)
        mask = self.bitmap[i]

        for offset in range(self.H):
            if (mask >> offset) & 1:
                j = (i + offset) % self.m
                k, v = self.table[j]
                if k == key:
                    return v

        return None
type Entry[K comparable, V any] struct {
	Key   K
	Value V
}

type HopscotchHashTable[K comparable, V any] struct {
	Table  []Entry[K, V]
	Bitmap []uint32
	M      int
	H      int
}

func (h *HopscotchHashTable[K, V]) hash(key K) int {
	return int(uintptr(any(key))) % h.M
}

func (h *HopscotchHashTable[K, V]) Get(key K) (V, bool) {
	i := h.hash(key)
	mask := h.Bitmap[i]

	for offset := 0; offset < h.H; offset++ {
		if (mask>>offset)&1 == 1 {
			j := (i + offset) % h.M
			if h.Table[j].Key == key {
				return h.Table[j].Value, true
			}
		}
	}

	var zero V
	return zero, false
}