Algorithm Search⌘K

Proofs: Hopscotch hashing

These are the human proofs of Hopscotch hashing, at full length.

Worst-case and expected cost of hopscotch hashing sketch

This proof shows: time.worst, time.expected.

Worst case of search and delete. The loops of lines L18 and L22 run HH times, and each pass tests one bit and makes at most one key comparison. So each operation takes O(H)O(H) time, when a hash value and a key comparison cost O(1)O(1). The bound is reached: put HH keys with the same home ii in a table with H≤mH \le m; a search for another key with home ii compares it with all HH keys.

Worst case of HOP-PLACE. By Lemma 3 of the correctness proof, the loop of line L7 makes at most mm passes, and the loop of lines L9 to L15 makes at most e−H+1≤me - H + 1 \le m passes, since ee falls by at least 1 at each pass and the loop stops when e<He < H. A pass looks at the H−1H - 1 bitmaps hop[(j−t) mod m]\mathrm{hop}[(j - t) \bmod m] for t=H−1,…,1t = H - 1, \ldots, 1. On the word RAM with H≤wH \le w, the test of line L11 for one bitmap is x∧(2t−1)≠0x \wedge (2^t - 1) \ne 0, and the smallest such dd is the index of the lowest set bit of that word, both O(1)O(1) word operations. So a pass takes O(H)O(H) time, and HOP-PLACE takes O(m+mH)=O(mH)O(m + mH) = O(mH) time. One attempt of RESIZE to m′m' slots makes n≤m′n \le m' calls of HOP-PLACE, each in O(m′H)O(m'H); a sharper count follows from the expected case below.

Lemma 4 (the same full slots as linear probing). Consider a sequence of inserts of distinct keys into an empty hopscotch table with mm slots, with no delete and no failed HOP-PLACE, and the same sequence in a linear-probing table with mm slots and the same homes. After each insert, the two tables have the same set of full slots.

Proof. By induction on the number of inserts. Before an insert of kk, the two sets of full slots are equal, so line L7 stops at the slot jj where linear probing puts kk: the first empty slot at or after home(k)\mathrm{home}(k). Each pass of the loop L9 to L15 fills the empty slot jj and empties a slot ss that was full; and line L16 fills the last empty slot of the chain. So the net change of the set of full slots is that slot jj becomes full, as in linear probing. □\square

Expected cost of HOP-PLACE. Assume the homes of the keys are independent and uniform on the mm slots, and that n=αmn = \alpha m keys are in the table, inserted as in Lemma 4. Then the distance ee found by line L7 is the number of probes of an unsuccessful search of linear probing in a table with nn keys, minus 1. Knuth (Volume 3, Section 6.4, the analysis of Algorithm L) shows that the expected number of such probes is about 12(1+1/(1−α)2)\frac{1}{2}\big(1 + 1/(1 - \alpha)^2\big) for large mm and fixed α<1\alpha < 1; see the analysis of linear probing. We quote this result and do not prove it here. The loop L9 to L15 makes at most ee passes of O(H)O(H) time each, so the expected time of HOP-PLACE is

O(1+E[e]⋅H)=O(1+H(1−α)2).O\big(1 + E[e] \cdot H\big) = O\Big(1 + \frac{H}{(1 - \alpha)^2}\Big).

This bounds an insert that does not trigger a resize. The probability that HOP-PLACE fails depends on HH and α\alpha; Herlihy, Shavit and Tzafrir study it by experiment, and this proof does not bound it. With deletes, Lemma 4 no longer holds as stated, because hopscotch hashing empties the slot of a deleted key while linear probing must keep a tombstone or move keys back. ■\blacksquare

Hopscotch hashing keeps its neighbourhood invariant and implements a dictionary full

This proof shows: correctness, termination.

Write home(k)=h(k) mod m\mathrm{home}(k) = h(k) \bmod m and δ(s,i)=(s−i) mod m\delta(s, i) = (s - i) \bmod m for the distance of slot ss from slot ii. Assume 1≤H≤m1 \le H \le m. Then for each slot ii, the slots (i+d) mod m(i + d) \bmod m for d=0,…,H−1d = 0, \ldots, H - 1 are HH distinct slots, so bit dd of hop[i]\mathrm{hop}[i] names one slot. The invariant (P1), (P2), (P3) is the one on the page, and DD is the set of pairs in full slots.

Lemma 1 (one neighbourhood suffices). Assume (P1) and (P2). A key kk is in the table if and only if, with i=home(k)i = \mathrm{home}(k), there is a d<Hd < H such that bit dd of hop[i]\mathrm{hop}[i] is 1 and slot (i+d) mod m(i + d) \bmod m holds kk.

Proof. If kk is in slot ss, let d=δ(s,i)d = \delta(s, i). By (P1), d<Hd < H, and s=(i+d) mod ms = (i + d) \bmod m. Slot ss holds a key with home ii, so bit dd of hop[i]\mathrm{hop}[i] is 1 by (P2). The converse is clear. □\square

So the loops of lines L2 and L3, L18 and L19, and L22 and L23 find kk if and only if kk is in the table, and they find its only slot, by (P3).

Lemma 2 (one displacement). Assume (P1) and (P2), let slot jj be empty, and let tt, dd, bb and ss be as in lines L11 and L13, so 1≤t≤H−11 \le t \le H - 1, 0≤d<t0 \le d < t, b=(j−t) mod mb = (j - t) \bmod m, bit dd of hop[b]\mathrm{hop}[b] is 1 and s=(b+d) mod ms = (b + d) \bmod m. After line L14, (P1) and (P2) hold, slot ss is empty, and DD is the same.

Proof. By (P2), slot ss holds a key xx with home bb. Line L14 moves the pair of xx to slot jj, which was empty, and empties slot ss; so DD does not change. Now δ(j,b)=t<H\delta(j, b) = t < H, so (P1) holds for xx, and it holds for every other key, which did not move.

For (P2), only the bits that refer to slots ss and jj can change their truth. Slot ss is (b+d) mod m(b + d) \bmod m and slot jj is (b+t) mod m(b + t) \bmod m. For bucket bb: slot ss no longer holds a key with home bb, and line L14 clears bit dd; slot jj now holds xx, with home bb, and line L14 sets bit tt. For a bucket c≠bc \ne b: before the move, slot ss held xx, whose home is not cc, and slot jj was empty, so all the bits of cc that name ss or jj were 0. After the move, slot ss is empty and slot jj holds xx, whose home is not cc, so these bits must still be 0, and they are, since line L14 changes only hop[b]\mathrm{hop}[b]. □\square

Lemma 3 (HOP-PLACE). Assume (P1), (P2) and (P3), and that kk is not in the table. Then HOP-PLACE(T,k,v)(T, k, v) ends after at most mm passes of each of its loops. If it returns OK, (P1) and (P2) hold, and the full slots hold the old pairs and (k,v)(k, v), each once. If it returns FAIL, (P1), (P2) and (P3) hold and DD is the same as before the call.

Proof. The loop of line L7 raises ee at each pass and stops at e=me = m, so it ends after at most mm passes. If it stops with e=me = m, all slots are full, and line L8 returns FAIL with nothing changed. Otherwise slot (i+e) mod m(i + e) \bmod m is empty, with e<me < m.

For the loop of lines L9 to L15, use the loop invariant: (P1) and (P2) hold, DD is the set of the old pairs, 0≤e<m0 \le e < m, and slot (i+e) mod m(i + e) \bmod m is empty. It holds at the first test of line L9 by the paragraph above. In a pass, e≥He \ge H, and line L10 sets j=(i+e) mod mj = (i + e) \bmod m, which is empty. If line L12 returns FAIL, nothing has changed in this pass, and the claim of the lemma for FAIL follows from the loop invariant; (P3) holds since DD and the set of full slots did not change. Otherwise Lemma 2 applies, so after line L14, (P1) and (P2) hold, DD is the same, and slot ss is empty, where

s=(b+d) mod m=(j−t+d) mod m=(i+e−(t−d)) mod m.s = (b + d) \bmod m = (j - t + d) \bmod m = (i + e - (t - d)) \bmod m.

Line L15 sets e′=e−(t−d)e' = e - (t - d). Since 1≤t−d≤t≤H−11 \le t - d \le t \le H - 1 and e≥He \ge H, we get 1≤e′<e<m1 \le e' < e < m, and slot (i+e′) mod m=s(i + e') \bmod m = s is empty. So the loop invariant holds again. As ee falls by at least 1 at each pass and stays at least 1, the loop makes at most mm passes.

When the loop ends, e<He < H and slot q=(i+e) mod mq = (i + e) \bmod m is empty. By (P2), every bit that names slot qq is 0, in particular bit ee of hop[i]\mathrm{hop}[i]. Line L16 puts (k,v)(k, v) in slot qq and sets that bit. Then (P1) holds for kk, as δ(q,i)=e<H\delta(q, i) = e < H, and (P2) holds: the only bits that name slot qq belong to the buckets cc with δ(q,c)<H\delta(q, c) < H, and of these only bucket ii is the home of the key in slot qq. The full slots hold the old pairs, which are distinct, and (k,v)(k, v), and kk was absent. □\square

Theorem. Start from the empty table, with 1≤H≤m1 \le H \le m, and apply any sequence of HOP-INSERT, HOP-SEARCH and HOP-DELETE in which every call to RESIZE returns. After each call, (P1), (P2) and (P3) hold, DD is the map that the same sequence gives on an abstract dictionary, and each search returns the value of its key in DD, or NIL. HOP-SEARCH, HOP-DELETE and each call to HOP-PLACE end after at most HH, HH and 2m2m passes of their loops.

Proof. Base. No slot is full and all bits are 0, so the invariant holds and DD is empty.

Search. By Lemma 1, lines L17 to L20 return the value of kk if kk is a key and NIL otherwise, after at most HH passes, and change nothing.

Delete. By Lemma 1, lines L21 to L23 find the slot q=(i+d) mod mq = (i + d) \bmod m of kk if kk is a key. Line L24 empties it, clears bit dd of hop[i]\mathrm{hop}[i] and lowers nn. (P1) holds for the other keys, which did not move. For (P2), only bits that name slot qq change truth; the one that was 1 was bit dd of the home ii of kk, which is now 0, and the others were 0 and stay 0. (P3) holds, and DD loses exactly the pair of kk. If kk is not a key, nothing changes.

Insert. By Lemma 1, lines L1 to L3 find kk if it is a key, and then only its value changes. Otherwise kk is absent. Each call of HOP-PLACE at line L4 that returns FAIL leaves the invariant and DD, by Lemma 3. Then RESIZE(T,2m)(T, 2m) saves the list PP of the pairs, which are ∣D∣=n|D| = n pairs with distinct keys. Each attempt of lines L27 and L28 starts from an empty table of m′≥mm' \ge m slots, in which the invariant holds with DD empty, and calls HOP-PLACE on the pairs of PP, none of which is in the table yet, since the keys of PP are distinct. By Lemma 3, after an attempt in which all calls return OK, the table holds exactly the pairs of PP and (P1) and (P2) hold; nn was not changed and equals ∣P∣|P|, so (P3) holds. Since H≤m≤m′H \le m \le m', the condition H≤m′H \le m' holds for each new size. So when RESIZE returns, the invariant holds and DD is as before. When HOP-PLACE at line L4 returns OK, Lemma 3 shows that the table holds the old pairs and (k,v)(k, v), and line L5 raises nn, so (P3) holds and DD gains exactly (k,v)(k, v).

By induction on the number of calls, the claims hold after each call. ■\blacksquare

The theorem assumes that RESIZE returns. It does when, for some size m′m', all the placements succeed; this fails forever when more than HH keys have the same value h(k)h(k), as the page explains. The Python version stops with an error in that case and when the table would pass 2202^{20} slots, which keeps the invariant, since a failed HOP-PLACE does.

Back to Hopscotch hashing