Quadratic Probing and Double Hashing
Chapter One Hundred Six
Syllabus topic Module 2, "collision avoidance techniques"
Pages 376 to 383 of 411
In one line
Quadratic probing spreads the probe sequence out so that clusters do not form, at the price of being unable to reach every slot; double hashing uses a second hash function to give each key its own probe sequence, which is the best of the three schemes.
Why linear probing needed replacing
Linear probing's defect was primary clustering: a key that hashes anywhere inside a run of occupied slots is placed at the end of that run, so runs grow and merge. The cause is that the probe sequence steps by 1, so every key that enters a cluster follows the same path out of it.
Both schemes here change the step.
Quadratic probing
h_i(k) = ( h(k) + i^2 ) mod m for i = 0, 1, 2, 3, ...
so the offsets are 0, 1, 4, 9, 16, 25, ...
The jumps grow, so a key displaced from a cluster lands far away rather than at the cluster's edge, and clusters cannot build up by adjacency.
M = 11
EMPTY = None
def quadratic_insert(slots, key, trace=False):
m = len(slots)
for i in range(m):
at = (key % m + i * i) % m
if slots[at] is EMPTY:
slots[at] = key
if trace:
print(" %-4d h=%-3d i=%-2d offset %-3d -> slot %-3d (%d probes)"
% (key, key % m, i, i * i, at, i + 1))
return at
return None # could not place it
slots = [EMPTY] * M
print("quadratic probing, m = %d, six keys that ALL hash to slot 3:" % M)
KEYS = [25, 36, 47, 58, 69, 80]
for k in KEYS:
print(" %d mod %d = %d" % (k, M, k % M))
print()
for k in KEYS:
at = quadratic_insert(slots, k, trace=True)
if at is None:
print(" %-4d COULD NOT BE PLACED" % k)
print()
print("the table:")
print(" " + " ".join("%4s" % (s if s is not None else "-") for s in slots))
print(" " + " ".join("%4d" % i for i in range(M)))
print()
print("slots used:", sum(1 for s in slots if s is not None), "of", M)
print("the offsets were 0, 1, 4, 9, 16, 25 modulo 11, which is",
[(i * i) % M for i in range(6)])
print("so the records are scattered, not in a run: no primary clustering.")quadratic probing, m = 11, six keys that ALL hash to slot 3:
25 mod 11 = 3
36 mod 11 = 3
47 mod 11 = 3
58 mod 11 = 3
69 mod 11 = 3
80 mod 11 = 3
25 h=3 i=0 offset 0 -> slot 3 (1 probes)
36 h=3 i=1 offset 1 -> slot 4 (2 probes)
47 h=3 i=2 offset 4 -> slot 7 (3 probes)
58 h=3 i=3 offset 9 -> slot 1 (4 probes)
69 h=3 i=4 offset 16 -> slot 8 (5 probes)
80 h=3 i=5 offset 25 -> slot 6 (6 probes)
the table:
- 58 - 25 36 - 80 47 69 - -
0 1 2 3 4 5 6 7 8 9 10
slots used: 6 of 11
the offsets were 0, 1, 4, 9, 16, 25 modulo 11, which is [0, 1, 4, 9, 5, 3]
so the records are scattered, not in a run: no primary clustering.Quadratic Probing and Double Hashing
The failure quadratic probing is usually not told about
EMPTY = None
def quadratic_insert(slots, key):
m = len(slots)
for i in range(m):
at = (key % m + i * i) % m
if slots[at] is EMPTY:
slots[at] = key
return at
return None
for M in (16, 11):
slots = [EMPTY] * M
reachable = sorted({(0 + i * i) % M for i in range(M)})
print("m = %d (%s)" % (M, "a power of 2" if M == 16 else "prime"))
print(" offsets i*i mod m, for i = 0..%d : %s"
% (M - 1, [(i * i) % M for i in range(M)]))
print(" DISTINCT slots a key hashing to 0 can ever reach: %s" % reachable)
print(" that is %d of %d slots, which is %.0f%%"
% (len(reachable), M, 100 * len(reachable) / M))
placed, refused = 0, []
for j in range(M):
key = j * M # every key is 0 mod M
at = quadratic_insert(slots, key)
if at is None:
refused.append(key)
else:
placed += 1
free = sum(1 for s in slots if s is EMPTY)
print(" inserting %d keys that all hash to 0: %d placed, %d REFUSED"
% (M, placed, len(refused)))
print(" and the table still has %d free slots out of %d" % (free, M))
print()
print("so quadratic probing can refuse an insertion into a table that is")
print("mostly empty. this is not a bug in the code; it is the arithmetic.")
print("i*i mod m simply does not visit every residue.")m = 16 (a power of 2)
offsets i*i mod m, for i = 0..15 : [0, 1, 4, 9, 0, 9, 4, 1, 0, 1, 4, 9, 0, 9, 4, 1]
DISTINCT slots a key hashing to 0 can ever reach: [0, 1, 4, 9]
that is 4 of 16 slots, which is 25%
inserting 16 keys that all hash to 0: 4 placed, 12 REFUSED
and the table still has 12 free slots out of 16
m = 11 (prime)
offsets i*i mod m, for i = 0..10 : [0, 1, 4, 9, 5, 3, 3, 5, 9, 4, 1]
DISTINCT slots a key hashing to 0 can ever reach: [0, 1, 3, 4, 5, 9]
that is 6 of 11 slots, which is 55%
inserting 11 keys that all hash to 0: 6 placed, 5 REFUSED
and the table still has 5 free slots out of 11
so quadratic probing can refuse an insertion into a table that is
mostly empty. this is not a bug in the code; it is the arithmetic.
i*i mod m simply does not visit every residue.Quadratic Probing and Double Hashing
That is the defect. With m = 16 a key that hashes to slot 0 can only ever reach 4 of the 16 slots, so the fifth such key cannot be inserted although 12 slots are free. The insertion does not slow down. It fails.
The rescue is a theorem, and it is the origin of a rule students learn without the reason:
If m is prime, quadratic probing with offsets i squared is guaranteed to find an empty slot whenever
the table is less than half full.
With m prime, the offsets i squared mod m take exactly (m+1)/2 distinct values, so a key can reach just over half the table. Hence alpha must stay below 0.5, and m must be prime.
def reachable_count(m):
return len({(i * i) % m for i in range(m)})
print("%8s %10s %18s %16s %14s"
% ("m", "prime?", "slots reachable", "(m+1)/2", "fraction"))
def is_prime(n):
if n < 2:
return False
d = 2
while d * d <= n:
if n % d == 0:
return False
d += 1
return True
for m in (11, 13, 16, 17, 31, 32, 101, 128):
count = reachable_count(m)
print("%8d %10s %18d %16d %13.0f%%"
% (m, "yes" if is_prime(m) else "NO", count, (m + 1) // 2,
100 * count / m))
print()
primes = [m for m in (11, 13, 17, 31, 101) ]
print("for every prime above, reachable slots equal (m+1)/2 exactly:",
all(reachable_count(m) == (m + 1) // 2 for m in primes))
print("for the powers of 2 it is far worse:",
[(m, reachable_count(m)) for m in (16, 32, 128)])
print()
print("hence the two rules together: m PRIME, and alpha below 0.5.")
print("neither rule alone is enough.") m prime? slots reachable (m+1)/2 fraction
11 yes 6 6 55%
13 yes 7 7 54%
16 NO 4 8 25%
17 yes 9 9 53%
31 yes 16 16 52%
32 NO 7 16 22%
101 yes 51 51 50%
128 NO 23 64 18%
for every prime above, reachable slots equal (m+1)/2 exactly: True
for the powers of 2 it is far worse: [(16, 4), (32, 7), (128, 23)]
hence the two rules together: m PRIME, and alpha below 0.5.
neither rule alone is enough.Quadratic Probing and Double Hashing
Secondary clustering
Quadratic probing removes primary clustering and leaves a weaker relative behind. Two keys with the same hash value follow exactly the same probe sequence, because the sequence depends only on h(k). So keys that collide at the start collide at every step. That is secondary clustering.
M = 101
same_hash = [i * M + 7 for i in range(5)] # all 7 mod 101
print("five keys, all %d mod %d:" % (7, M), same_hash)
print()
print("their quadratic probe sequences, first 6 steps:")
for k in same_hash:
seq = [(k % M + i * i) % M for i in range(6)]
print(" %-8d %s" % (k, seq))
print()
print("identical, because the sequence depends only on h(k) = %d." % 7)
print("that is SECONDARY clustering: fewer keys are affected than under")
print("primary clustering, but those that are, are affected completely.")five keys, all 7 mod 101: [7, 108, 209, 310, 411]
their quadratic probe sequences, first 6 steps:
7 [7, 8, 11, 16, 23, 32]
108 [7, 8, 11, 16, 23, 32]
209 [7, 8, 11, 16, 23, 32]
310 [7, 8, 11, 16, 23, 32]
411 [7, 8, 11, 16, 23, 32]
identical, because the sequence depends only on h(k) = 7.
that is SECONDARY clustering: fewer keys are affected than under
primary clustering, but those that are, are affected completely.Double hashing
The cure for secondary clustering is to make the step itself depend on the key.
h_i(k) = ( h1(k) + i x h2(k) ) mod m for i = 0, 1, 2, ...
Two conditions on h2, and both are examinable:
h2(k) must never be 0, or the probe sequence never moves and the insertion loops for ever.
h2(k) must be coprime with m, or the sequence visits only some slots. The usual arrangement is m prime and h2(k) = 1 + (k mod (m - 1)), which is between 1 and m-1 and therefore coprime with a prime m.
M = 11
EMPTY = None
def h1(k, m):
return k % m
def h2(k, m):
return 1 + (k % (m - 1)) # never 0, always coprime with a prime m
def double_insert(slots, key, trace=False):
m = len(slots)
step = h2(key, m)
for i in range(m):
at = (h1(key, m) + i * step) % m
if slots[at] is EMPTY:
slots[at] = key
if trace:
print(" %-4d h1=%-3d h2=%-3d i=%-2d -> slot %-3d (%d probes)"
% (key, h1(key, m), step, i, at, i + 1))
return at
return None
slots = [EMPTY] * M
KEYS = [25, 36, 47, 58, 69, 80]
print("double hashing, m = %d, the same six keys that all hash to slot 3:" % M)
for k in KEYS:
double_insert(slots, k, trace=True)
print()
print("the table:")
print(" " + " ".join("%4s" % (s if s is not None else "-") for s in slots))
print(" " + " ".join("%4d" % i for i in range(M)))
print()
print("every key has its OWN step, so no two sequences agree:")
for k in KEYS:
print(" %-4d step %-3d sequence %s"
% (k, h2(k, M), [(h1(k, M) + i * h2(k, M)) % M for i in range(5)]))
print()
print("no two of those sequences are the same, although all six keys have")
print("the same h1. that is secondary clustering removed.")
print()
print("and with m prime and h2 between 1 and m-1, a sequence reaches every")
print("slot. for key 25, step %d:" % h2(25, M))
print(" ", sorted({(h1(25, M) + i * h2(25, M)) % M for i in range(M)}))
print(" all %d slots:" % M,
len({(h1(25, M) + i * h2(25, M)) % M for i in range(M)}) == M)Quadratic Probing and Double Hashing
double hashing, m = 11, the same six keys that all hash to slot 3:
25 h1=3 h2=6 i=0 -> slot 3 (1 probes)
36 h1=3 h2=7 i=1 -> slot 10 (2 probes)
47 h1=3 h2=8 i=1 -> slot 0 (2 probes)
58 h1=3 h2=9 i=1 -> slot 1 (2 probes)
69 h1=3 h2=10 i=1 -> slot 2 (2 probes)
80 h1=3 h2=1 i=1 -> slot 4 (2 probes)
the table:
47 58 69 25 80 - - - - - 36
0 1 2 3 4 5 6 7 8 9 10
every key has its OWN step, so no two sequences agree:
25 step 6 sequence [3, 9, 4, 10, 5]
36 step 7 sequence [3, 10, 6, 2, 9]
47 step 8 sequence [3, 0, 8, 5, 2]
58 step 9 sequence [3, 1, 10, 8, 6]
69 step 10 sequence [3, 2, 1, 0, 10]
80 step 1 sequence [3, 4, 5, 6, 7]
no two of those sequences are the same, although all six keys have
the same h1. that is secondary clustering removed.
and with m prime and h2 between 1 and m-1, a sequence reaches every
slot. for key 25, step 6:
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
all 11 slots: TrueCompare the two defects, one line each: quadratic probing can reach only half the table, double hashing reaches all of it.
The three schemes measured on the same keys
def mix(i):
x = ((i + 1) * 2654435761) % (2 ** 32)
x ^= x >> 13
x = (x * 2246822519) % (2 ** 32)
x ^= x >> 17
return x
M = 1009
EMPTY = None
def fill(n, step_of):
slots = [EMPTY] * M
total = 0
for j in range(n):
k = mix(j)
for i in range(M):
at = step_of(k, i)
total += 1
if slots[at] is EMPTY:
slots[at] = k
break
return slots, total / n
def linear(k, i):
return (k % M + i) % M
def quadratic(k, i):
return (k % M + i * i) % M
def double(k, i):
return (k % M + i * (1 + k % (M - 1))) % M
def longest_run(slots):
best = run = 0
for cell in slots + slots: # as a ring
run = run + 1 if cell is not EMPTY else 0
best = max(best, run)
return min(best, M)
def chance_fill(n):
"""Occupy n slots with NO probing at all: the baseline for a run length."""
slots = [EMPTY] * M
placed, j = 0, 0
while placed < n:
at = mix(j + 10 ** 6) % M
j += 1
if slots[at] is EMPTY:
slots[at] = True
placed += 1
return slots
print("%7s %22s %16s %14s %14s"
% ("alpha", "scheme", "probes per insert", "longest run", "x chance"))
for alpha in (0.4, 0.6, 0.8):
n = int(alpha * M)
base = longest_run(chance_fill(n))
for name, fn in (("linear probing", linear),
("quadratic probing", quadratic),
("double hashing", double)):
slots, avg = fill(n, fn)
run = longest_run(slots)
print("%7.2f %22s %17.2f %14d %13.1fx"
% (alpha, name, avg, run, run / base))
print("%7.2f %22s %17s %14d %13.1fx"
% (alpha, "(no probing: chance)", "n/a", base, 1.0))
print()
print("double hashing needs the fewest probes per insert at every alpha and")
print("quadratic probing is second. that is the column that matters.")
print()
print("the longest-run column needs the chance row beside it. a run of")
print("ADJACENT occupied slots is what linear probing builds on purpose, and")
print("at alpha 0.8 its run is almost 5 times what chance alone gives. the")
print("other two are above chance as well, at roughly 1.5 and 2.5 times, so")
print("they are not free of adjacency at high load: they are merely far")
print("better than linear probing at it. the ordering between those two is")
print("not stable and should not be read as a result.")Quadratic Probing and Double Hashing
alpha scheme probes per insert longest run x chance
0.40 linear probing 1.33 12 1.5x
0.40 quadratic probing 1.29 11 1.4x
0.40 double hashing 1.27 7 0.9x
0.40 (no probing: chance) n/a 8 1.0x
0.60 linear probing 2.08 35 2.5x
0.60 quadratic probing 1.66 19 1.4x
0.60 double hashing 1.52 14 1.0x
0.60 (no probing: chance) n/a 14 1.0x
0.80 linear probing 3.24 96 4.8x
0.80 quadratic probing 2.24 32 1.6x
0.80 double hashing 2.01 48 2.4x
0.80 (no probing: chance) n/a 20 1.0x
double hashing needs the fewest probes per insert at every alpha and
quadratic probing is second. that is the column that matters.
the longest-run column needs the chance row beside it. a run of
ADJACENT occupied slots is what linear probing builds on purpose, and
at alpha 0.8 its run is almost 5 times what chance alone gives. the
other two are above chance as well, at roughly 1.5 and 2.5 times, so
they are not free of adjacency at high load: they are merely far
better than linear probing at it. the ordering between those two is
not stable and should not be read as a result.Quadratic Probing and Double Hashing
The three schemes compared
| Linear probing | Quadratic probing | Double hashing | |
|---|---|---|---|
| Probe sequence | h + i | h + i squared | h1 + i x h2 |
| Primary clustering | yes | no | no |
| Secondary clustering | yes | yes | no |
| Slots reachable | all m | (m+1)/2 when m is prime | all m |
| Load factor limit | below 1, in practice 0.7 | below 0.5 | below 1, in practice 0.7 |
| Table size | any | must be prime | prime, or h2 coprime with m |
| Cache locality | best | moderate | worst |
| Hash cost | one hash | one hash | two hashes |
| Probe distribution | poorest | middling | best |
What to say when asked which to use. Double hashing has the best theory and is the right answer for an open addressing table that must run near its limit. Linear probing is often faster in practice at moderate load because of cache locality. Quadratic probing sits between them and carries the hard constraint that the table must be prime and less than half full, which is why it is the least used of the three.
Quick revision
- Quadratic probing: h_i(k) = (h(k) + i squared) mod m. The growing jumps prevent primary clustering.
- It cannot reach every slot. With m = 16 a key hashing to 0 reaches only 4 slots, and a fifth such key was refused while 12 slots were free.
- With m prime it reaches exactly (m+1)/2 slots, so m must be prime AND alpha must stay below 0.5. Neither rule alone suffices.
- Secondary clustering: two keys with the same h(k) have identical probe sequences, since the sequence depends only on h(k).
- Double hashing: h_i(k) = (h1(k) + i x h2(k)) mod m, so the step itself depends on the key.
- h2(k) must never be 0, or the sequence never moves, and must be coprime with m, or it cannot reach every slot. With m prime, h2(k) = 1 + (k mod (m-1)) satisfies both.
- Double hashing has no clustering of either kind, reaches every slot, and measured the fewest probes per insert at every load factor.
- Linear probing keeps the best cache locality, so it is often fastest in practice at moderate load despite the worst theory.
Quadratic Probing and Double Hashing
Test yourself
1. Give quadratic probing's probe sequence and say what it fixes. h_i(k) = (h(k) + i squared) mod m, so offsets 0, 1, 4, 9, 16. The growing jumps mean a displaced record is not placed beside the record that displaced it, so primary clustering does not form.
2. What is quadratic probing's serious limitation? Give the measured example. It cannot reach every slot. With m = 16, a key hashing to slot 0 can reach only slots 0, 1, 4 and 9, so the fifth key hashing to 0 was refused while 12 of the 16 slots were free.
3. State the theorem that rescues it, and the two rules it implies. If m is prime, quadratic probing finds an empty slot whenever the table is less than half full. So m must be prime and the load factor must stay below 0.5.
4. Define secondary clustering. Two keys with the same hash value follow identical probe sequences, because the sequence depends only on h(k). Fewer keys are affected than under primary clustering, but those that are, are affected at every step.
5. Give double hashing's probe sequence and the two conditions on the second hash function. h_i(k) = (h1(k) + i x h2(k)) mod m. h2(k) must never be 0, or the probe sequence never advances, and h2(k) must be coprime with m, or the sequence cannot reach every slot.
6. Give a second hash function that satisfies both conditions, with m prime. h2(k) = 1 + (k mod (m - 1)). It lies between 1 and m-1, so it is never 0 and is always coprime with a prime m.
7. Why is linear probing often the fastest in practice despite the worst theory? Its probes walk consecutive array positions, which suits the memory cache, while double hashing jumps around the array and misses the cache.
8. Which scheme would you choose for a table that must run close to its capacity, and why? Double hashing: it has no primary or secondary clustering, it reaches every slot, and it measured the fewest probes per insertion at every load factor tested.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.