Skip to content

Quadratic Probing Search

Resolve hash collisions by probing with quadratic offsets to reduce clustering.

Quadratic probing improves upon linear probing by spreading out probe sequences. Instead of stepping one slot at a time, it uses a quadratic function of the probe number. This reduces primary clustering and improves distribution under moderate load.

You search by following the same quadratic probe sequence used during insertion.

Problem

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

Model

  • Table TT has size mm
  • Hash function h(k)h(k)
  • Probe sequence:
it=(h(k)+c1t+c2t2)modm i_t = \big(h(k) + c_1 t + c_2 t^2\big) \bmod m

for constants c1,c2c_1, c_2 and t=0,1,2,t = 0, 1, 2, \dots

Algorithm

quadratic_probing_search(T, k):
    i0 = h(k)
    for t from 0 to m - 1:
        i = (i0 + c1*t + c2*t*t) mod m
        if T[i] is empty:
            return NOT_FOUND
        if T[i].key == k:
            return T[i].value
    return NOT_FOUND

Example

Let:

  • m=7m = 7
  • h(k)=kmod7h(k) = k \bmod 7
  • c1=0c_1 = 0, c2=1c_2 = 1

Insert keys:

[10,17,24] [10, 17, 24]

All hash to index 3. Probe sequence:

  • t=0t=0: index 3 → 10
  • t=1t=1: index 4 → 17
  • t=2t=2: index 0 → 24

Search for k=24k = 24:

  • check index 3 → no
  • check index 4 → no
  • check index 0 → match

Correctness

Each key follows a deterministic probe sequence derived from its hash. Lookup uses the same sequence. If the key exists, it must appear at one of these probe positions.

If an empty slot appears before a match, the key was never inserted, so it does not exist.

Complexity

Let α=n/m\alpha = n/m.

casetime
averageO(1)O(1)
worstO(n)O(n)

Quadratic probing reduces primary clustering compared to linear probing but still suffers from secondary clustering.

Space

O(m) O(m)

When to Use

Quadratic probing is appropriate when:

  • clustering from linear probing becomes a problem
  • memory locality is still desired
  • load factor remains moderate

It requires careful choice of mm, c1c_1, and c2c_2 to ensure full table coverage.

Implementation

class HashTable:
    def __init__(self, m=8):
        self.m = m
        self.table = [None] * m

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

    def get(self, key):
        i0 = self._hash(key)

        for t in range(self.m):
            i = (i0 + t*t) % self.m
            if self.table[i] is None:
                return None
            k, v = self.table[i]
            if k == key:
                return v

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

type HashTable[K comparable, V any] struct {
	Table []Entry[K, V]
	M     int
}

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

func (h *HashTable[K, V]) Get(key K) (V, bool) {
	i0 := h.hash(key)

	for t := 0; t < h.M; t++ {
		i := (i0 + t*t) % h.M
		if !h.Table[i].Used {
			break
		}
		if h.Table[i].Key == key {
			return h.Table[i].Value, true
		}
	}

	var zero V
	return zero, false
}