Solution. (Open Addressing) Every key k∈U is assigned not just one hash value in {0,1,…,m−1}, but rather a permutation of {0,1,…,m−1} called a probing sequence:
h:U×{0,1,…,m−1}→{0,1,…,m−1} such that k has probing sequence (h(k,0),h(k,1),…,h(k,m−1)).
We implement our desired operations as follows:
Insert(k,v): Find the smallest p such that T[h(k,p)]=Null or Deleted, then set T[h(k,p)]=(k,v).
Search(k): Find the smallest p such that T[h(k,p)].Key=k, then return T[h(k,p)].Value.
Delete(k): Find the smallest p such that T[h(k,p)].Key=k, then set T[h(k,p)]=Deleted.
Remark. Some CYU questions:
Why do we need a Deleted type? What goes wrong if we set T[h(k,p)]=Null when deleting?
What happens when α=mn approaches 1, or exceeds 1?
Claim. Suppose that every key receives an independently generated uniformly random sequence.
Then the expected number of p we will need to check is O(1−α1), where α:=mn<1.
Proof: Handwaving… a guess for p succeeds with probability 1−α, so the EV # of tries is 1−α1. ■
The rigorous proof goes hE[X]=j=1∑mj⋅hPr[X=j]=j=1∑mhPr[X≥j]≤j=1∑mαj−1≤1−α1, hooray. ■
Even more formally, we need to justify hPr[X≥j]≤αj−1, so we should be writing something like the following, where Ei is the event that the ith probe finds an occupied slot:
And the ℓth term in this product is upper-bounded by m−ℓ+1n−ℓ+1≤mn=α, as desired. ■
Generating probing sequences uniformly at random is hard, so in practice, we implement like so.
Solution. (Probing Methods) Pick any two hash functions h1,h2:U→{0,1,…,m−1}. We can use:
Linear Probing. Take h(k,p):=(h1(k)+p)%m.
Double Hashing Probing. Take h(k,p):=(h1(k)+p⋅h2(k))%m. (This is a permutation only when gcd(h2(k),m)=1.)
Now for something new. Let's try to implement…
Problem. (Static Dictionaries & Perfect Hashing) We are given {(k1,v1),…,(kn,vn)} up front, after which point only Search(k) may be called. We would like the following:
In particular, we require perfect hashing: no collisions occur at all!
Here's a naive solution that passes all constraints except the space requirement.
Solution. (Suboptimal-Space Solution) Consider a universal hash family H, and take m≥n2.
Then pick h∈H, and use ki↦h(ki). If the first h∈H fails, pick another h∼H until success.
Proof: This works in expected O(n) pre-processing time because h∼HPr[no collisions]≥21 by Union Bound—try it yourself! (When does universality of H get used?) ■
Here's the smart solution by Fredman-Komlos-Szemeredi (1984) that passes the space requirement.
Solution. (Optimal-Space Solution) Sample a hash function h1∈H1 that goes h1:U→{0,1,…,n−1}.
Suppose Nℓ:=∣h1−1(ℓ)∩{k1,…,kn}∣ for all ℓ∈{0,1,…,n−1}.
Use the suboptimal-space solution to fit Nℓ key-value pairs in the ℓth bucket.
Using O(Nℓ) time and O(Nℓ2) space, give the ℓth bucket a sampled hash function h2,ℓ∈H2,ℓ that goes h2,ℓ:h1−1(ℓ)→{0,1,…,Nℓ2−1}.
Our data structure is then a two-dimensional array: n buckets, the ℓth of which has Nℓ2 slots to hash into.
Proof: It's easy to see that this has O(1) search time, and that we only need O(n) pre-processing time once h1 is chosen.
The hard part is achieving O(n) space. If we choose h1 unluckily, we won't actually get O(n) space! However,
Claim.h∼H1Pr[(ℓ=0∑n−1Nℓ2)>4n]<21.
To prove this, note that by Markov's inequality, it suffices to show h∼H1E[∑ℓ=0n−1Nℓ2]<2n. This takes some clever work: