Chapter 11 — Hash Tables
CLRS, fourth edition · Lean 4 formalization
The proofs below use the models and assumptions described in the scope and implementation notes.
Imports
import Mathlib11.1. Direct-Address Tables
This section models a direct-address table as a total function from natural keys to optional values. This is the mathematical core of the CLRS operations: search reads the key slot, insert overwrites that slot, and delete clears it.
Main results:
-
Theorem
search_insert_same: searching the inserted key returns the new value. -
Theorem
search_insert_other: inserting at one key leaves other keys unchanged. -
Theorem
search_delete_same: deleting a key clears that key.
Status: proved for the functional direct-address model.
Deferred refinements: array bounds and RAM costs.
namespace CLRSnamespace Chapter11Direct-address table model
A direct-address table maps each natural key to an optional value.
abbrev DirectAddressTable (V : Type u) := Nat → Option VThe empty direct-address table.
def emptyDirectAddressTable : DirectAddressTable V :=
fun _ => noneSearch a direct-address table by reading the corresponding slot.
def directSearch (T : DirectAddressTable V) (key : Nat) : Option V :=
T key
Insert overwrites the slot at key.
def directInsert (key : Nat) (value : V)
(T : DirectAddressTable V) : DirectAddressTable V :=
fun j => if j = key then some value else T j
Delete clears the slot at key.
def directDelete (key : Nat)
(T : DirectAddressTable V) : DirectAddressTable V :=
fun j => if j = key then none else T jOperation correctness
Searching the inserted key returns the inserted value.
theorem search_insert_same (key : Nat) (value : V)
(T : DirectAddressTable V) :
directSearch (directInsert key value T) key = some value := by
simp [directSearch, directInsert]Inserting at one key leaves every other key unchanged.
theorem search_insert_other {key other : Nat} (h : other ≠ key)
(value : V) (T : DirectAddressTable V) :
directSearch (directInsert key value T) other = directSearch T other := by
simp [directSearch, directInsert, h]
Deleting a key makes search at that key return none.
theorem search_delete_same (key : Nat) (T : DirectAddressTable V) :
directSearch (directDelete key T) key = none := by
simp [directSearch, directDelete]Deleting one key leaves every other key unchanged.
theorem search_delete_other {key other : Nat} (h : other ≠ key)
(T : DirectAddressTable V) :
directSearch (directDelete key T) other = directSearch T other := by
simp [directSearch, directDelete, h]
Searching an empty direct-address table returns none.
theorem search_empty (key : Nat) :
directSearch (emptyDirectAddressTable : DirectAddressTable V) key = none := by
rflend Chapter11end CLRSImports
import Mathlib
import CLRSLean.Probability.FiniteExpectation11.2. Chained Hash Tables
This section gives a deterministic correctness layer for chained hash tables. The table is a function from bucket indices to lists of keys. The hash function decides the bucket, insertion conses the key onto that bucket, and deletion filters the key from that same bucket.
Main results:
-
Theorem
bucket_hashInsert_same: the inserted key appears in its hash bucket. -
Theorem
bucket_hashInsert_other: buckets with a different index are unchanged. -
Theorem
hashSearch_hashInsert_self: after insertion, searching for the inserted key succeeds. -
Theorem
hashSearch_hashInsert_iff: after insertion, searching for any query succeeds exactly when it is the inserted key or was already present. -
Theorem
hashSearch_hashDelete_self: after deletion, searching for the deleted key fails. -
Theorem
hashSearch_hashDelete_iff: after deletion, searching for any query succeeds exactly when it is different from the deleted key and was already present. -
Theorem
expectedSearchChainLength_eq_loadFactor: in the finite-uniform bucket model, expected chain length is exactly the load factor. -
Theorem
expectedUnsuccessfulSearchCost_finiteHashInsert: inserting one key increases expected unsuccessful-search cost by1/m. -
Theorem
expectedRandomChainLength_eq_loadFactor: under SUHA (keys hashed independently and uniformly), the expected chain length at any fixed bucket is the load factorα = n/m, as a genuine expectation over the input distributionFin n → Fin m. -
Theorem
expectedRandomUnsuccessfulSearchCost: the expected unsuccessful search cost is1 + α, as a genuine expectation. -
Theorem
pairCollisionProb: under SUHA, two distinct keys collide with probability exactly1/m, as a genuine expectation. -
Theorem
expectedRandomSuccessfulSearchCost: the expected successful-search cost is1 + (n-1)/(2m)(CLRS Theorem 11.2), as a double expectation over the uniform query key and the SUHA input distribution. -
Definition
IsUniversaland Theoremsuniversal_expected_collisions/universal_expected_search_cost: a random hash-function model, where a universal family gives expected collisions≤ αand search cost≤ 1 + α(CLRS Theorem 11.3).
Status: proved for deterministic correctness, finite-uniform expected cost, the
SUHA true-expectation chain-length / unsuccessful- / successful-search analysis,
and a universal random-hash-function collision-bound model.
Deferred refinements: RAM / probe-count operational semantics.
namespace CLRSnamespace Chapter11Chained table model
A chained hash table maps bucket indices to lists of stored keys.
abbrev ChainedHashTable (K : Type u) := Nat → List KInsert a key into the bucket selected by the hash function.
def hashInsert (h : K → Nat) (x : K)
(T : ChainedHashTable K) : ChainedHashTable K :=
fun i => if i = h x then x :: T i else T iDelete every copy of a key from the bucket selected by the hash function. This is the deterministic functional analogue of CLRS chained-hash deletion; pointer updates inside a linked list are intentionally outside this model.
def hashDelete [DecidableEq K] (h : K → Nat) (x : K)
(T : ChainedHashTable K) : ChainedHashTable K :=
fun i => if i = h x then (T i).filter fun y => y != x else T iSearch for a key in the bucket selected by its hash value.
def hashSearch (h : K → Nat) (T : ChainedHashTable K) (x : K) : Prop :=
x ∈ T (h x)Deterministic correctness
The inserted key appears in its own hash bucket.
theorem bucket_hashInsert_same (h : K → Nat) (T : ChainedHashTable K) (x : K) :
x ∈ hashInsert h x T (h x) := by
simp [hashInsert]A bucket with a different index is unchanged by insertion.
theorem bucket_hashInsert_other {h : K → Nat} {T : ChainedHashTable K}
{x : K} {i : Nat} (hi : i ≠ h x) :
hashInsert h x T i = T i := by
simp [hashInsert, hi]The deleted key no longer appears in its hash bucket.
theorem bucket_hashDelete_same [DecidableEq K]
(h : K → Nat) (T : ChainedHashTable K) (x : K) :
x ∉ hashDelete h x T (h x) := by
simp [hashDelete]A bucket with a different index is unchanged by deletion.
theorem bucket_hashDelete_other [DecidableEq K]
{h : K → Nat} {T : ChainedHashTable K} {x : K} {i : Nat}
(hi : i ≠ h x) :
hashDelete h x T i = T i := by
simp [hashDelete, hi]After inserting a key, searching for that key succeeds.
theorem hashSearch_hashInsert_self (h : K → Nat)
(T : ChainedHashTable K) (x : K) :
hashSearch h (hashInsert h x T) x := by
exact bucket_hashInsert_same h T xSearching after insertion succeeds exactly when the query is the inserted key or the query already appeared in its own hash bucket.
theorem hashSearch_hashInsert_iff (h : K → Nat)
(T : ChainedHashTable K) (x y : K) :
hashSearch h (hashInsert h y T) x ↔ x = y ∨ hashSearch h T x := by
by_cases hxy : h x = h y
· simp [hashSearch, hashInsert, hxy]
· have hxne : x ≠ y := by
intro hkey
exact hxy (by rw [hkey])
simp [hashSearch, hashInsert, hxy, hxne]After deleting a key, searching for that key fails.
theorem hashSearch_hashDelete_self [DecidableEq K] (h : K → Nat)
(T : ChainedHashTable K) (x : K) :
¬ hashSearch h (hashDelete h x T) x := by
exact bucket_hashDelete_same h T xSearching after deletion succeeds exactly when the query is not the deleted key and the query was already present in its own hash bucket.
theorem hashSearch_hashDelete_iff [DecidableEq K] (h : K → Nat)
(T : ChainedHashTable K) (x y : K) :
hashSearch h (hashDelete h y T) x ↔ x ≠ y ∧ hashSearch h T x := by
by_cases hxy : h x = h y
· simp [hashSearch, hashDelete, hxy, and_comm]
· have hxne : x ≠ y := by
intro hkey
exact hxy (by rw [hkey])
simp [hashSearch, hashDelete, hxy, hxne]Finite-uniform hashing interface
A finite chained hash table with exactly m buckets.
abbrev FiniteChainedHashTable (m : Nat) (K : Type u) := Fin m → List K
A real-valued 0/1 indicator for finite probability calculations.
def probabilityIndicator (P : Prop) [Decidable P] : ℝ :=
if P then 1 else 0
Uniform average over the bucket set Fin m.
noncomputable def uniformAverageFin {m : Nat} (X : Fin m → ℝ) : ℝ :=
(∑ i : Fin m, X i) / (m : ℝ)
The finite-uniform bucket average is the shared
CLRS.Probability.fintypeExpect toolkit specialised to Fin m. This
bridge lets the algebraic lemmas below reuse the toolkit instead of re-deriving
them.
theorem uniformAverageFin_eq_fintypeExpect {m : Nat} (X : Fin m → ℝ) :
uniformAverageFin X = CLRS.Probability.fintypeExpect X := by
simp [uniformAverageFin, CLRS.Probability.fintypeExpect, Fintype.card_fin]Uniform averages are additive.
theorem uniformAverageFin_add {m : Nat} (X Y : Fin m → ℝ) :
uniformAverageFin (fun i => X i + Y i) =
uniformAverageFin X + uniformAverageFin Y := by
simp only [uniformAverageFin_eq_fintypeExpect]
exact CLRS.Probability.fintypeExpect_add X YA uniform average of nonnegative quantities is nonnegative.
theorem uniformAverageFin_nonneg {m : Nat} {X : Fin m → ℝ}
(hX : ∀ i, 0 ≤ X i) :
0 ≤ uniformAverageFin X := by
rw [uniformAverageFin_eq_fintypeExpect]
exact CLRS.Probability.fintypeExpect_nonneg hX
A singleton bucket has probability 1/m under the uniform bucket model.
theorem uniformAverageFin_indicator_singleton {m : Nat} (j : Fin m) :
uniformAverageFin (fun i => probabilityIndicator (i = j)) = 1 / (m : ℝ) := by
rw [uniformAverageFin_eq_fintypeExpect,
show (fun i : Fin m => probabilityIndicator (i = j))
= (fun i => CLRS.Probability.indicator (i = j)) from rfl,
CLRS.Probability.fintypeExpect_indicator_singleton, Fintype.card_fin]Insert into a finite-bucket chained hash table.
def finiteHashInsert {m : Nat} (h : K → Fin m) (x : K)
(T : FiniteChainedHashTable m K) : FiniteChainedHashTable m K :=
fun i => if i = h x then x :: T i else T iSearch in a finite-bucket chained hash table.
def finiteHashSearch {m : Nat} (h : K → Fin m)
(T : FiniteChainedHashTable m K) (x : K) : Prop :=
x ∈ T (h x)The finite-bucket load factor: stored keys divided by bucket count.
noncomputable def finiteHashLoadFactor {m : Nat}
(T : FiniteChainedHashTable m K) : ℝ :=
(∑ i : Fin m, ((T i).length : ℝ)) / (m : ℝ)Load factor is nonnegative.
theorem finiteHashLoadFactor_nonneg {m : Nat}
(T : FiniteChainedHashTable m K) :
0 ≤ finiteHashLoadFactor T := by
unfold finiteHashLoadFactor
refine div_nonneg ?_ ?_
· exact Finset.sum_nonneg (fun i _hi => by exact_mod_cast Nat.zero_le (T i).length)
· exact_mod_cast Nat.zero_le mExpected chain length for an unsuccessful search when the searched bucket is uniform over all buckets.
noncomputable def expectedSearchChainLength {m : Nat}
(T : FiniteChainedHashTable m K) : ℝ :=
uniformAverageFin (fun i => ((T i).length : ℝ))Expected unsuccessful-search cost in the current abstraction: one bucket access plus the expected chain length.
noncomputable def expectedUnsuccessfulSearchCost {m : Nat}
(T : FiniteChainedHashTable m K) : ℝ :=
1 + expectedSearchChainLength TUnder uniform hashing over buckets, expected chain length is exactly the load factor.
theorem expectedSearchChainLength_eq_loadFactor {m : Nat}
(T : FiniteChainedHashTable m K) :
expectedSearchChainLength T = finiteHashLoadFactor T := by
rflExpected chain length is nonnegative in the finite-uniform bucket model.
theorem expectedSearchChainLength_nonneg {m : Nat}
(T : FiniteChainedHashTable m K) :
0 ≤ expectedSearchChainLength T := by
rw [expectedSearchChainLength_eq_loadFactor]
exact finiteHashLoadFactor_nonneg T
Under uniform hashing over buckets, unsuccessful search has cost
1 + load factor in the current finite-bucket abstraction.
theorem expectedUnsuccessfulSearchCost_eq_one_plus_loadFactor {m : Nat}
(T : FiniteChainedHashTable m K) :
expectedUnsuccessfulSearchCost T = 1 + finiteHashLoadFactor T := by
rflExpected unsuccessful-search cost is at least the initial bucket access.
theorem expectedUnsuccessfulSearchCost_ge_one {m : Nat}
(T : FiniteChainedHashTable m K) :
1 ≤ expectedUnsuccessfulSearchCost T := by
unfold expectedUnsuccessfulSearchCost
have hnonneg := expectedSearchChainLength_nonneg T
linarithInserting one key into a finite chained table increases total chain length by one.
theorem totalBucketLength_finiteHashInsert {m : Nat} (h : K → Fin m)
(T : FiniteChainedHashTable m K) (x : K) :
(∑ i : Fin m, ((finiteHashInsert h x T i).length : ℝ)) =
(∑ i : Fin m, ((T i).length : ℝ)) + 1 := by
classical
have hpoint : ∀ i : Fin m,
((finiteHashInsert h x T i).length : ℝ) =
((T i).length : ℝ) + probabilityIndicator (i = h x) := by
intro i
by_cases hi : i = h x
· simp [finiteHashInsert, probabilityIndicator, hi]
· simp [finiteHashInsert, probabilityIndicator, hi]
have hindicator :
(∑ i : Fin m, probabilityIndicator (i = h x)) = (1 : ℝ) := by
rw [Finset.sum_eq_single (h x)]
· simp [probabilityIndicator]
· intro b _hb hbne
simp [probabilityIndicator, hbne]
· intro hmissing
exact (hmissing (Finset.mem_univ (h x))).elim
calc
(∑ i : Fin m, ((finiteHashInsert h x T i).length : ℝ))
= ∑ i : Fin m, (((T i).length : ℝ) + probabilityIndicator (i = h x)) := by
exact Finset.sum_congr rfl (fun i _hi => hpoint i)
_ = (∑ i : Fin m, ((T i).length : ℝ)) +
∑ i : Fin m, probabilityIndicator (i = h x) := by
rw [Finset.sum_add_distrib]
_ = (∑ i : Fin m, ((T i).length : ℝ)) + 1 := by
rw [hindicator]
Inserting one key increases the expected chain length by 1/m in the
finite-uniform bucket model.
theorem expectedSearchChainLength_finiteHashInsert {m : Nat} (h : K → Fin m)
(T : FiniteChainedHashTable m K) (x : K) :
expectedSearchChainLength (finiteHashInsert h x T) =
expectedSearchChainLength T + 1 / (m : ℝ) := by
simp [expectedSearchChainLength, uniformAverageFin,
totalBucketLength_finiteHashInsert, add_div]
Inserting one key increases the finite-bucket load factor by 1/m.
theorem finiteHashLoadFactor_finiteHashInsert {m : Nat} (h : K → Fin m)
(T : FiniteChainedHashTable m K) (x : K) :
finiteHashLoadFactor (finiteHashInsert h x T) =
finiteHashLoadFactor T + 1 / (m : ℝ) := by
simp [finiteHashLoadFactor, totalBucketLength_finiteHashInsert, add_div]
Inserting one key increases expected unsuccessful-search cost by 1/m in
the finite-uniform bucket model.
theorem expectedUnsuccessfulSearchCost_finiteHashInsert {m : Nat} (h : K → Fin m)
(T : FiniteChainedHashTable m K) (x : K) :
expectedUnsuccessfulSearchCost (finiteHashInsert h x T) =
expectedUnsuccessfulSearchCost T + 1 / (m : ℝ) := by
rw [expectedUnsuccessfulSearchCost, expectedSearchChainLength_finiteHashInsert,
expectedUnsuccessfulSearchCost]
ringExpected search cost as a true expectation (SUHA)
The finite-uniform layer above is definitional. We now derive the CLRS
chained-hash costs as genuine expectations under the simple uniform hashing
assumption: n keys are hashed independently and uniformly into
m buckets. The sample space is the explicit independent uniform
distribution Fin n → Fin m (the bucket each key hashes to), and the load
factor is α = n/m.
open CLRS.Probability
The load factor α = n/m of n keys in m buckets.
noncomputable def loadFactor (m n : Nat) : ℝ := (n : ℝ) / (m : ℝ)
Split a hash assignment a : Fin n → Fin m into the bucket of one key i
and the assignment of the remaining keys. This is the product decomposition
witnessing that coordinate i is independent of the rest.
noncomputable def hashSplit {m n : Nat} (i : Fin n) :
(Fin n → Fin m) ≃ Fin m × ({x : Fin n // x ≠ i} → Fin m) where
toFun a := (a i, fun x => a x.val)
invFun q := fun x => if hx : x = i then q.1 else q.2 ⟨x, hx⟩
left_inv a := by
funext x; by_cases hx : x = i
· subst hx; simp
· simp [hx]
right_inv q := by
obtain ⟨b, rest⟩ := q
simp only [Prod.mk.injEq]
refine ⟨by simp, ?_⟩
funext x; obtain ⟨xv, hxi⟩ := x; simp [hxi]
Marginalisation: the expectation of a function of a single hash coordinate
equals the expectation over the single-bucket space Fin m.
theorem fintypeExpect_hashCoord {m n : Nat} (i : Fin n) (hm : 0 < m) (f : Fin m → ℝ) :
fintypeExpect (fun a : Fin n → Fin m => f (a i)) = fintypeExpect f := by
haveI : Nonempty (Fin m) := ⟨⟨0, hm⟩⟩
have hcard : Fintype.card ({x : Fin n // x ≠ i} → Fin m) ≠ 0 := Fintype.card_ne_zero
have he := fintypeExpect_equiv (hashSplit (m := m) i)
(fun q : Fin m × ({x : Fin n // x ≠ i} → Fin m) => f q.1)
simp only [hashSplit, Equiv.coe_fn_mk] at he
rw [he]
exact fintypeExpect_fst hcard f
A single key hashes to a fixed bucket q with probability 1/m.
theorem singleBucketProb {m n : Nat} (i : Fin n) (q : Fin m) (hm : 0 < m) :
fintypeExpect (fun a : Fin n → Fin m => indicator (a i = q)) = 1 / (m : ℝ) := by
rw [fintypeExpect_hashCoord i hm (fun c => indicator (c = q)),
fintypeExpect_indicator_singleton, Fintype.card_fin]
The length of the chain at bucket q under a hash assignment a: the number
of keys that hash to q.
noncomputable def randomChainLength {m n : Nat} (a : Fin n → Fin m) (q : Fin m) : ℝ :=
∑ i : Fin n, indicator (a i = q)
Expected chain length = load factor (true expectation). Under SUHA, the
expected number of keys hashing to any fixed bucket q is exactly the load
factor α = n/m (CLRS Theorem 11.1, unsuccessful-search chain length).
theorem expectedRandomChainLength_eq_loadFactor {m n : Nat} (q : Fin m) (hm : 0 < m) :
fintypeExpect (fun a : Fin n → Fin m => randomChainLength a q) = loadFactor m n := by
unfold randomChainLength loadFactor
rw [fintypeExpect_sum]
simp only [singleBucketProb _ q hm]
rw [Finset.sum_const, Finset.card_univ, Fintype.card_fin, nsmul_eq_mul]
ring
Expected unsuccessful-search cost = 1 + α (true expectation). One bucket
access plus the expected chain length, as a genuine expectation over the SUHA
input distribution (CLRS Theorem 11.1).
theorem expectedRandomUnsuccessfulSearchCost {m n : Nat} (q : Fin m) (hm : 0 < m) :
fintypeExpect (fun a : Fin n → Fin m => 1 + randomChainLength a q) =
1 + loadFactor m n := by
haveI : Nonempty (Fin m) := ⟨⟨0, hm⟩⟩
have hcard : Fintype.card (Fin n → Fin m) ≠ 0 := Fintype.card_ne_zero
have key : (fun a : Fin n → Fin m => 1 + randomChainLength a q)
= (fun a => (fun _ : Fin n → Fin m => (1 : ℝ)) a
+ (fun a => randomChainLength a q) a) := rfl
rw [key, fintypeExpect_add, fintypeExpect_const hcard,
expectedRandomChainLength_eq_loadFactor q hm]Successful search as a true expectation (SUHA)
CLRS Theorem 11.2 analyses a successful search: the query key is one of the
n stored keys, chosen uniformly, and the number of probes is one (for the key
itself) plus the number of keys that precede it in its chain. Because new keys
are prepended, the keys ahead of k_i are exactly those inserted after it,
i.e. the later-indexed keys k_j with j > i that hash to the same bucket.
Averaging over both the uniformly random query key and the SUHA input
distribution Fin n → Fin m gives the expected cost 1 + α/2 - α/(2n), which we
prove here in the exact form 1 + (n-1)/(2m).
Scalar factors pull out of fintypeExpect: the finite-uniform expectation is
linear in a constant multiplier.
theorem fintypeExpect_const_mul {Ω : Type} [Fintype Ω] [DecidableEq Ω]
(c : ℝ) (X : Ω → ℝ) :
fintypeExpect (fun ω => c * X ω) = c * fintypeExpect X := by
unfold fintypeExpect
rw [← Finset.mul_sum, mul_div_assoc]
Split a hash assignment a : Fin n → Fin m into the buckets of two distinct
keys i ≠ j together with the assignment of the remaining keys. This witnesses
that the pair of coordinates (i, j) is independent of the rest.
noncomputable def hashSplitPair {m n : Nat} (i j : Fin n) (hij : i ≠ j) :
(Fin n → Fin m) ≃
(Fin m × Fin m) × ({x : Fin n // x ≠ i ∧ x ≠ j} → Fin m) where
toFun a := ((a i, a j), fun x => a x.val)
invFun q := fun x =>
if hx : x = i then q.1.1
else if hy : x = j then q.1.2
else q.2 ⟨x, ⟨hx, hy⟩⟩
left_inv a := by
funext x
by_cases hx : x = i
· subst hx; simp
· by_cases hy : x = j
· subst hy; simp [hx]
· simp [hx, hy]
right_inv q := by
obtain ⟨⟨b1, b2⟩, rest⟩ := q
simp only [Prod.mk.injEq]
refine ⟨⟨?_, ?_⟩, ?_⟩
· simp
· have hji : ¬ (j = i) := fun h => hij h.symm
simp [hji]
· funext x; obtain ⟨xv, hxi, hxj⟩ := x
simp [hxi, hxj]
The uniform probability that the two coordinates of a pair over Fin m agree
is 1/m: the diagonal of Fin m × Fin m has m of the m² points.
theorem fintypeExpect_prod_diag {m : Nat} (hm : 0 < m) :
fintypeExpect (fun q : Fin m × Fin m => indicator (q.1 = q.2)) = 1 / (m : ℝ) := by
have hm' : (m : ℝ) ≠ 0 := by exact_mod_cast hm.ne'
have hnum : (∑ q : Fin m × Fin m, indicator (q.1 = q.2)) = (m : ℝ) := by
unfold indicator
rw [Fintype.sum_prod_type]
have hinner : ∀ a : Fin m, (∑ b : Fin m, (if a = b then (1 : ℝ) else 0)) = 1 := by
intro a; simp
rw [Finset.sum_congr rfl (fun a _ => hinner a), Finset.sum_const,
Finset.card_univ, Fintype.card_fin, nsmul_eq_mul, mul_one]
unfold fintypeExpect
rw [hnum, Fintype.card_prod, Fintype.card_fin]
push_cast
rw [div_mul_eq_div_div, div_self hm']
Pairwise collision probability = 1/m (true expectation). Under SUHA, two
distinct keys i ≠ j hash to the same bucket with probability exactly 1/m,
as a genuine expectation over the input distribution Fin n → Fin m (CLRS
Corollary/analysis underlying Theorems 11.1-11.2, E[X_ij] = 1/m).
theorem pairCollisionProb {m n : Nat} (i j : Fin n) (hij : i ≠ j) (hm : 0 < m) :
fintypeExpect (fun a : Fin n → Fin m => indicator (a i = a j)) = 1 / (m : ℝ) := by
haveI : Nonempty (Fin m) := ⟨⟨0, hm⟩⟩
have hcard :
Fintype.card ({x : Fin n // x ≠ i ∧ x ≠ j} → Fin m) ≠ 0 := Fintype.card_ne_zero
have he := fintypeExpect_equiv (hashSplitPair (m := m) i j hij)
(fun p : (Fin m × Fin m) × ({x : Fin n // x ≠ i ∧ x ≠ j} → Fin m) =>
indicator (p.1.1 = p.1.2))
simp only [hashSplitPair, Equiv.coe_fn_mk] at he
have h2 : fintypeExpect
(fun p : (Fin m × Fin m) × ({x : Fin n // x ≠ i ∧ x ≠ j} → Fin m) =>
indicator (p.1.1 = p.1.2)) = 1 / (m : ℝ) := by
rw [← fintypeExpect_prod_diag hm]
exact fintypeExpect_fst hcard (fun q : Fin m × Fin m => indicator (q.1 = q.2))
exact he.trans h2
The number of ordered pairs i < j in Fin n, counted in ℝ, is
n(n-1)/2. This is the Gauss triangle count obtained from trichotomy and the
symmetry of the strict order.
theorem sum_upper_triangle (n : Nat) :
(∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 : ℝ) else 0))
= (n : ℝ) * ((n : ℝ) - 1) / 2 := by
have hpt : ∀ i j : Fin n,
(if i < j then (1 : ℝ) else 0) + (if j < i then (1 : ℝ) else 0)
+ (if i = j then (1 : ℝ) else 0) = 1 := by
intro i j
rcases lt_trichotomy i j with h | h | h
· have h1 : ¬ j < i := lt_asymm h
have h2 : ¬ i = j := ne_of_lt h
simp [h, h1, h2]
· subst h; simp
· have h1 : ¬ i < j := lt_asymm h
have h2 : ¬ i = j := fun he => (ne_of_lt h) he.symm
simp [h, h1, h2]
have hUL : (∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 : ℝ) else 0))
= ∑ i : Fin n, ∑ j : Fin n, (if j < i then (1 : ℝ) else 0) := Finset.sum_comm
have hD : (∑ i : Fin n, ∑ j : Fin n, (if i = j then (1 : ℝ) else 0)) = (n : ℝ) := by
have hone : ∀ i : Fin n, (∑ j : Fin n, (if i = j then (1 : ℝ) else 0)) = 1 := by
intro i; simp
rw [Finset.sum_congr rfl (fun i _ => hone i), Finset.sum_const,
Finset.card_univ, Fintype.card_fin, nsmul_eq_mul, mul_one]
have hAll : (∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 : ℝ) else 0))
+ (∑ i : Fin n, ∑ j : Fin n, (if j < i then (1 : ℝ) else 0))
+ (∑ i : Fin n, ∑ j : Fin n, (if i = j then (1 : ℝ) else 0))
= (n : ℝ) * (n : ℝ) := by
rw [← Finset.sum_add_distrib, ← Finset.sum_add_distrib]
have hstep : (∑ i : Fin n, ((∑ j : Fin n, (if i < j then (1 : ℝ) else 0))
+ (∑ j : Fin n, (if j < i then (1 : ℝ) else 0))
+ ∑ j : Fin n, (if i = j then (1 : ℝ) else 0)))
= ∑ _i : Fin n, (n : ℝ) := by
refine Finset.sum_congr rfl (fun i _ => ?_)
rw [← Finset.sum_add_distrib, ← Finset.sum_add_distrib,
Finset.sum_congr rfl (fun j _ => hpt i j), Finset.sum_const,
Finset.card_univ, Fintype.card_fin, nsmul_eq_mul, mul_one]
rw [hstep, Finset.sum_const, Finset.card_univ, Fintype.card_fin, nsmul_eq_mul]
rw [← hUL, hD] at hAll
have h2 : (∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 : ℝ) else 0))
= ((n : ℝ) * (n : ℝ) - (n : ℝ)) / 2 := by linarith
rw [h2]; ring
Cost of a successful search for the i-th inserted key under a hash
assignment a: one probe for k_i itself, plus one probe for every
later-inserted key k_j (index j > i) that hashes to the same bucket (CLRS
proof of Theorem 11.2, keys prepended so those ahead of k_i are exactly the
later ones).
noncomputable def successfulSearchKeyCost {m n : Nat}
(a : Fin n → Fin m) (i : Fin n) : ℝ :=
1 + ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0)
Average successful-search cost over the n stored keys, for a fixed hash
assignment a (the query key is uniform over the stored keys).
noncomputable def averageSuccessfulSearchCost {m n : Nat}
(a : Fin n → Fin m) : ℝ :=
(1 / (n : ℝ)) * ∑ i : Fin n, successfulSearchKeyCost a i
Expected successful-search cost = 1 + (n-1)/(2m) (true expectation). This
is CLRS Theorem 11.2 (Θ(1 + α), exact form 1 + α/2 - α/(2n)), proved as a
genuine double expectation: over the uniformly random query key and over the
SUHA input distribution Fin n → Fin m.
theorem expectedRandomSuccessfulSearchCost {m n : Nat} (hm : 0 < m) (hn : 0 < n) :
fintypeExpect (fun a : Fin n → Fin m => averageSuccessfulSearchCost a)
= 1 + ((n : ℝ) - 1) / (2 * (m : ℝ)) := by
haveI : Nonempty (Fin m) := ⟨⟨0, hm⟩⟩
have hmcard : Fintype.card (Fin n → Fin m) ≠ 0 := Fintype.card_ne_zero
have hn' : (n : ℝ) ≠ 0 := by exact_mod_cast hn.ne'
have hm' : (m : ℝ) ≠ 0 := by exact_mod_cast hm.ne'
have hE : fintypeExpect (fun a : Fin n → Fin m =>
∑ i : Fin n, ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0))
= ∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 / (m : ℝ)) else 0) := by
rw [fintypeExpect_sum Finset.univ (fun (i : Fin n) (a : Fin n → Fin m) =>
∑ j : Fin n, (if i < j then indicator (a i = a j) else 0))]
refine Finset.sum_congr rfl (fun i _ => ?_)
rw [fintypeExpect_sum Finset.univ (fun (j : Fin n) (a : Fin n → Fin m) =>
if i < j then indicator (a i = a j) else 0)]
refine Finset.sum_congr rfl (fun j _ => ?_)
by_cases hlt : i < j
· simp only [if_pos hlt]
exact pairCollisionProb i j (ne_of_lt hlt) hm
· simp only [if_neg hlt]
exact fintypeExpect_const hmcard 0
have hpair : (∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 / (m : ℝ)) else 0))
= (1 / (m : ℝ)) * ((n : ℝ) * ((n : ℝ) - 1) / 2) := by
rw [← sum_upper_triangle n, Finset.mul_sum]
refine Finset.sum_congr rfl (fun i _ => ?_)
rw [Finset.mul_sum]
refine Finset.sum_congr rfl (fun j _ => ?_)
by_cases h : i < j <;> simp [h]
have hG : fintypeExpect (fun a : Fin n → Fin m =>
∑ i : Fin n, successfulSearchKeyCost a i)
= (n : ℝ) + (1 / (m : ℝ)) * ((n : ℝ) * ((n : ℝ) - 1) / 2) := by
have hGkey : (fun a : Fin n → Fin m => ∑ i : Fin n, successfulSearchKeyCost a i)
= (fun a => (fun _ : Fin n → Fin m => (n : ℝ)) a
+ (fun a => ∑ i : Fin n, ∑ j : Fin n,
(if i < j then indicator (a i = a j) else 0)) a) := by
funext a
show (∑ i : Fin n, successfulSearchKeyCost a i)
= (n : ℝ) + ∑ i : Fin n, ∑ j : Fin n,
(if i < j then indicator (a i = a j) else 0)
unfold successfulSearchKeyCost
rw [Finset.sum_add_distrib, Finset.sum_const, Finset.card_univ, Fintype.card_fin,
nsmul_eq_mul, mul_one]
rw [hGkey, fintypeExpect_add, fintypeExpect_const hmcard, hE, hpair]
have key : (fun a : Fin n → Fin m => averageSuccessfulSearchCost a)
= (fun a => (1 / (n : ℝ))
* (fun a => ∑ i : Fin n, successfulSearchKeyCost a i) a) := rfl
rw [key, fintypeExpect_const_mul, hG]
field_simpRandom hash-function model (universal hashing)
The analysis above randomises key placement: keys hash independently and
uniformly. Universal hashing instead fixes the keys and randomises the hash
function, drawn uniformly from a family H : ι → (K → Fin m). The family is
universal when any two distinct keys collide with probability at most 1/m
(CLRS Definition of a universal family, equation (11.4)). From this hypothesis
alone we recover the expected collision bound α = n/m and the expected search
cost 1 + α (CLRS Theorem 11.3).
A finite family of hash functions H : ι → (K → Fin m) is universal if
any two distinct keys collide under a uniformly random member of the family with
probability at most 1/m (CLRS equation (11.4)).
def IsUniversal {ι K : Type} [Fintype ι] [DecidableEq ι] {m : Nat}
(H : ι → (K → Fin m)) : Prop :=
∀ x y : K, x ≠ y →
fintypeExpect (fun t : ι => indicator (H t x = H t y)) ≤ 1 / (m : ℝ)
Expected collisions under universal hashing ≤ α = n/m. Fix a query key
x and n stored keys k i, all distinct from x. Under a uniformly random
member of a universal family, the expected number of stored keys colliding with
x is at most the load factor n/m (CLRS Theorem 11.3).
theorem universal_expected_collisions {ι K : Type} [Fintype ι] [DecidableEq ι]
{m n : Nat} (H : ι → (K → Fin m)) (hU : IsUniversal H)
(x : K) (k : Fin n → K) (hk : ∀ i, k i ≠ x) :
fintypeExpect (fun t : ι => ∑ i : Fin n, indicator (H t x = H t (k i)))
≤ (n : ℝ) / (m : ℝ) := by
rw [fintypeExpect_sum Finset.univ
(fun (i : Fin n) (t : ι) => indicator (H t x = H t (k i)))]
calc ∑ i : Fin n, fintypeExpect (fun t : ι => indicator (H t x = H t (k i)))
≤ ∑ _i : Fin n, (1 / (m : ℝ)) :=
Finset.sum_le_sum (fun i _ => hU x (k i) (Ne.symm (hk i)))
_ = (n : ℝ) / (m : ℝ) := by
rw [Finset.sum_const, Finset.card_univ, Fintype.card_fin, nsmul_eq_mul]
ring
Expected successful/unsuccessful search cost under universal hashing
≤ 1 + α. One probe plus the expected number of colliding stored keys
(CLRS Theorem 11.3, the universal-hashing analogue of Theorems 11.1-11.2).
theorem universal_expected_search_cost {ι K : Type} [Fintype ι] [DecidableEq ι]
[Nonempty ι] {m n : Nat} (H : ι → (K → Fin m)) (hU : IsUniversal H)
(x : K) (k : Fin n → K) (hk : ∀ i, k i ≠ x) :
fintypeExpect (fun t : ι => 1 + ∑ i : Fin n, indicator (H t x = H t (k i)))
≤ 1 + (n : ℝ) / (m : ℝ) := by
have hcard : Fintype.card ι ≠ 0 := Fintype.card_ne_zero
have key : (fun t : ι => 1 + ∑ i : Fin n, indicator (H t x = H t (k i)))
= (fun t => (fun _ : ι => (1 : ℝ)) t
+ (fun t => ∑ i : Fin n, indicator (H t x = H t (k i))) t) := rfl
rw [key, fintypeExpect_add, fintypeExpect_const hcard]
have hbound := universal_expected_collisions H hU x k hk
linarithend Chapter11end CLRSDefinitions and proofs
CLRSLean.Probability.FiniteExpectation
Generic Fintype wrapper.
noncomputable def fintypeExpect {Ω : Type} [Fintype Ω] [DecidableEq Ω] (X : Ω → ℝ) : ℝ :=
(∑ ω : Ω, X ω) / (Fintype.card Ω : ℝ)Imports
import Mathlib
import CLRSLean.FourthEdition.Chapter_11.Section_11_2_Chained_Hash_Tables11.3. Hash Functions
Section 11.2 defines the predicate IsUniversal (CLRS equation (11.4)) and
proves, from that hypothesis alone, the universal-hashing costs
universal_expected_collisions and universal_expected_search_cost.
What was missing is an actual family that satisfies IsUniversal: until
now universality was an unwitnessed assumption. This section closes that loop by
constructing a concrete universal family and, additionally, records the two
deterministic heuristics of CLRS §11.3.1-11.3.2.
Main results:
-
Definition
divisionHashand lemmadivisionHash_lt: the division methodh(k) = k mod mwith its range bound (CLRS §11.3.1). -
Definition
multiplicationHashand lemmamultiplicationHash_lt: the multiplication methodh(k) = floor (m * frac (k * A))with its range bound (CLRS §11.3.2). -
Definition
affineHash: the prime-field affine familyh_{a,b}(k) = a * k + bover the fieldZMod p(pprime). This is the number-theoretic dot-product construction of CLRS §11.3.3 in the exact casem = p. -
Theorem
affineHash_isUniversal: the affine family satisfiesIsUniversal(CLRS Theorem 11.5). Two distinct keys collide exactly when the multiplierais zero, an event of probability1/p = 1/m, so the collision probability is at most1/m. This provides the first concrete witness discharging theIsUniversalhypothesis. -
Theorems
affineHash_expected_collisionsandaffineHash_expected_search_cost: the §11.2 universal-hashing bounds instantiated on the concrete family, i.e. expected collisions≤ n/mand expected search cost≤ 1 + n/mwith noIsUniversalhypothesis left open. -
Definition
affineHashModand theoremaffineHashMod_isUniversal: the full CLRS Theorem 11.5 family for arbitrarym ≤ p, using the outer reduction modulom.
Status: proved. Both the exact m = p family and the general
m ≤ p form exist and instantiate the downstream §11.2 bounds; the
deterministic heuristics are recorded with range lemmas.
Notation conventions used in this section:
-
p: a prime modulus, soZMod pis a finite field ofpelements -
m: the table size (m = pfor the affine family) -
a,b: the slope and intercept of an affine hashh_{a,b}(k) = a * k + b -
A: the multiplicative constant in(0, 1)of the multiplication method
Current gaps: none for the mathematical statement of CLRS Theorem 11.5. Operational RAM/probe accounting remains outside this section's advertised boundary.
namespace CLRSnamespace Chapter11open CLRS.ProbabilityDeterministic hashing heuristics (CLRS §11.3.1-11.3.2)
The division method (CLRS §11.3.1): map a natural-number key to the remainder
k mod m. This is a deterministic hash into {0, …, m-1}.
def divisionHash (m k : ℕ) : ℕ := k % m
The division method lands in the bucket range {0, …, m-1}.
theorem divisionHash_lt (m k : ℕ) (hm : 0 < m) : divisionHash m k < m :=
Nat.mod_lt _ hm
The multiplication method (CLRS §11.3.2): with a real constant A, take the
fractional part of k * A, scale by m, and take the floor. This is a
deterministic hash into {0, …, m-1}.
noncomputable def multiplicationHash (m : ℕ) (A : ℝ) (k : ℕ) : ℕ :=
⌊(m : ℝ) * Int.fract ((k : ℝ) * A)⌋.toNat
The multiplication method lands in the bucket range {0, …, m-1}.
theorem multiplicationHash_lt (m : ℕ) (A : ℝ) (k : ℕ) (hm : 0 < m) :
multiplicationHash m A k < m := by
unfold multiplicationHash
have hfract_lt : Int.fract ((k : ℝ) * A) < 1 := Int.fract_lt_one _
have hmpos : (0 : ℝ) < (m : ℝ) := by exact_mod_cast hm
have hlt : (m : ℝ) * Int.fract ((k : ℝ) * A) < (m : ℝ) := by
calc (m : ℝ) * Int.fract ((k : ℝ) * A)
< (m : ℝ) * 1 := by exact mul_lt_mul_of_pos_left hfract_lt hmpos
_ = (m : ℝ) := by ring
have h1 : ⌊(m : ℝ) * Int.fract ((k : ℝ) * A)⌋ < (m : ℤ) := by
rw [Int.floor_lt]; push_cast; exact hlt
omegaA concrete universal family (CLRS Theorem 11.5)
We construct the prime-field affine family h_{a,b}(k) = a * k + b over the
finite field ZMod p. With m = p this is exactly the number-theoretic
construction of CLRS §11.3.3. The family is indexed by all pairs
(a, b) ∈ ZMod p × ZMod p, drawn uniformly.
Universality is genuinely number-theoretic: for distinct keys x ≠ y, the two
hash values agree iff a * (x - y) = 0. In a field this forces a = 0
(since x - y ≠ 0 is a unit), an event of probability exactly 1/p.
The canonical representative of a residue in ZMod p as an element of
Fin p, using the value map ZMod.val. This lets the affine family land in the
Fin m codomain required by IsUniversal.
def toFin {p : ℕ} [NeZero p] (a : ZMod p) : Fin p := ⟨a.val, ZMod.val_lt a⟩
The representative map ZMod p → Fin p is injective, because ZMod.val is.
theorem toFin_injective {p : ℕ} [NeZero p] : Function.Injective (toFin (p := p)) := by
intro a b hab
have hval : a.val = b.val := congrArg Fin.val hab
exact ZMod.val_injective p hval
The prime-field affine hash family (CLRS §11.3.3, Theorem 11.5, exact
m = p case): h_{a,b}(k) = a * k + b, computed in the field ZMod p and
represented in Fin p. The index (a, b) ranges over ZMod p × ZMod p.
def affineHash (p : ℕ) [NeZero p] (t : ZMod p × ZMod p) (k : ZMod p) : Fin p :=
toFin (t.1 * k + t.2)
Theorem (CLRS Theorem 11.5). The prime-field affine family is universal: for
any two distinct keys, a uniformly random member collides on them with
probability at most 1/m (here m = p).
This is the first concrete witness of the IsUniversal predicate from
Section 11.2, so the collision and search-cost bounds there are no longer
conditional on an unproven hypothesis.
theorem affineHash_isUniversal (p : ℕ) [NeZero p] (hp : p.Prime) :
IsUniversal (affineHash p) := by
haveI : Fact p.Prime := ⟨hp⟩
intro x y hxy
-- The collision event `h x = h y` is exactly the event `a = 0`.
have hiff : ∀ t : ZMod p × ZMod p,
(affineHash p t x = affineHash p t y) ↔ (t.1 = 0) := by
intro t
constructor
· intro h
have h2 : t.1 * x + t.2 = t.1 * y + t.2 := toFin_injective h
have h3 : t.1 * x = t.1 * y := add_right_cancel h2
have h4 : t.1 * (x - y) = 0 := by rw [mul_sub, h3, sub_self]
rcases mul_eq_zero.mp h4 with h5 | h5
· exact h5
· exact absurd (sub_eq_zero.mp h5) hxy
· intro h
show affineHash p t x = affineHash p t y
unfold affineHash
rw [h]; simp
-- Rewrite the collision indicator as the indicator of `a = 0`.
have hfun :
(fun t : ZMod p × ZMod p => indicator (affineHash p t x = affineHash p t y))
= (fun t : ZMod p × ZMod p => indicator (t.1 = 0)) := by
funext t
by_cases h : t.1 = 0
· simp [indicator, (hiff t).mpr h, h]
· have hne : ¬ (affineHash p t x = affineHash p t y) := fun hc => h ((hiff t).mp hc)
simp [indicator, hne, h]
rw [hfun]
-- The probability that the first coordinate is `0` is `1/card = 1/p`.
have hcardZ : Fintype.card (ZMod p) ≠ 0 := Fintype.card_ne_zero
have hmarg := fintypeExpect_fst (Ω₁ := ZMod p) (Ω₂ := ZMod p) hcardZ
(fun a => indicator (a = 0))
have hrw : (fun t : ZMod p × ZMod p => indicator (t.1 = 0))
= (fun t : ZMod p × ZMod p => (fun a : ZMod p => indicator (a = 0)) t.1) := rfl
rw [hrw, hmarg, fintypeExpect_indicator_singleton, ZMod.card]
Corollary (concrete universal collision bound). Instantiating
universal_expected_collisions on the affine family: for a query key x
and n stored keys all distinct from x, the expected number of collisions
under a uniformly random affine hash is at most the load factor n/m
(m = p), with no IsUniversal hypothesis left open.
theorem affineHash_expected_collisions (p : ℕ) [NeZero p] (hp : p.Prime) {n : ℕ}
(x : ZMod p) (k : Fin n → ZMod p) (hk : ∀ i, k i ≠ x) :
fintypeExpect (fun t : ZMod p × ZMod p =>
∑ i : Fin n, indicator (affineHash p t x = affineHash p t (k i)))
≤ (n : ℝ) / (p : ℝ) :=
universal_expected_collisions (affineHash p) (affineHash_isUniversal p hp) x k hk
Corollary (concrete universal search-cost bound). Instantiating
universal_expected_search_cost on the affine family: the expected
search cost (one probe plus expected collisions) is at most 1 + n/m
(m = p), with no IsUniversal hypothesis left open (CLRS Theorem 11.3).
theorem affineHash_expected_search_cost (p : ℕ) [NeZero p] (hp : p.Prime) {n : ℕ}
(x : ZMod p) (k : Fin n → ZMod p) (hk : ∀ i, k i ≠ x) :
fintypeExpect (fun t : ZMod p × ZMod p =>
1 + ∑ i : Fin n, indicator (affineHash p t x = affineHash p t (k i)))
≤ 1 + (n : ℝ) / (p : ℝ) := by
haveI : Nonempty (ZMod p × ZMod p) := ⟨(0, 0)⟩
exact universal_expected_search_cost (affineHash p) (affineHash_isUniversal p hp) x k hk
The general mod-m affine family (CLRS Theorem 11.5, full form)
The exact m = p construction above reduces each hash value into Fin p. CLRS
Theorem 11.5 instead uses a larger prime modulus p and an outer reduction
modulo the table size m ≤ p: h_{a,b}(k) = ((a·k + b) mod p) mod m. This
subsection formalises that general family and proves it universal.
The proof is the standard counting argument of CLRS. For distinct keys x ≠ y
and a nonzero multiplier a, the field values r = a·x + b and s = a·y + b
differ (else a·(x - y) = 0 in the field ZMod p); the assignment
(a,b) ↦ (r,s) is injective. A collision modulo m therefore corresponds to a
pair of distinct residues r ≠ s with r ≡ s (mod m). The number of such
residue pairs in {0,…,p-1}² is at most p·(p-1)/m, which gives the collision
probability bound 1/m.
The number of s < p strictly greater than r and congruent to r modulo
m is at most (p - 1 - r) / m.
lemma count_congruent_gt_le (p m r : ℕ) (hm : 0 < m) (hr : r < p) :
(((Finset.range p).filter (fun s : ℕ => r < s ∧ s % m = r % m)).card : ℕ)
≤ (p - 1 - r) / m := by
classical
let f : ℕ → ℕ := fun s => (s - r) / m
let S : Finset ℕ := (Finset.range p).filter (fun s : ℕ => r < s ∧ s % m = r % m)
let T : Finset ℕ := Finset.Icc 1 ((p - 1 - r) / m)
have hMt : Set.MapsTo f (S : Set ℕ) (T : Set ℕ) := by
intro s hs
rcases Finset.mem_filter.mp hs with ⟨hsp, hscond⟩
rcases hscond with ⟨hrs, hmod⟩
have hslt : s < p := Finset.mem_range.mp hsp
have hme : s ≡ r [MOD m] := by
change s % m = r % m
exact hmod
have hle : r ≤ s := le_of_lt hrs
have hdvd : m ∣ s - r := (Nat.modEq_iff_dvd' hle).mp hme.symm
have hge : m ≤ s - r := by
obtain ⟨q, hq⟩ := hdvd
have hne : s - r ≠ 0 := by omega
have hqpos : 1 ≤ q := by
by_contra hqn
have hq0 : q = 0 := by omega
subst q
simp at hq
exact hne hq
nlinarith
have hpos : 1 ≤ (s - r) / m := (Nat.le_div_iff_mul_le hm).mpr (by simpa using hge)
have hsub : s - r ≤ p - 1 - r := by
have hsp1 : s ≤ p - 1 := by omega
exact Nat.sub_le_sub_right hsp1 r
have hle2 : (s - r) / m ≤ (p - 1 - r) / m := Nat.div_le_div_right hsub
simpa [f, T] using (Finset.mem_Icc.mpr ⟨hpos, hle2⟩)
have hInj : (S : Set ℕ).InjOn f := by
intro s hs t ht hf
have hmes : s ≡ r [MOD m] := by
have hmod : s % m = r % m := (Finset.mem_filter.mp hs).2.2
change s % m = r % m
exact hmod
have hmet : t ≡ r [MOD m] := by
have hmod : t % m = r % m := (Finset.mem_filter.mp ht).2.2
change t % m = r % m
exact hmod
have hsle : r ≤ s := le_of_lt (Finset.mem_filter.mp hs).2.1
have htle : r ≤ t := le_of_lt (Finset.mem_filter.mp ht).2.1
have hdvdS : m ∣ s - r := (Nat.modEq_iff_dvd' hsle).mp hmes.symm
have hdvdT : m ∣ t - r := (Nat.modEq_iff_dvd' htle).mp hmet.symm
have hcal : s - r = t - r := by
calc s - r = (s - r) / m * m := by rw [Nat.div_mul_cancel hdvdS]
_ = (t - r) / m * m := by
change f s * m = f t * m
rw [hf]
_ = t - r := by rw [Nat.div_mul_cancel hdvdT]
omega
have hle := Finset.card_le_card_of_injOn f hMt hInj
have hT : T.card = (p - 1 - r) / m := by
simp [T]
rwa [hT] at hle
The number of s < p strictly less than r and congruent to r modulo
m is at most r / m.
lemma count_congruent_lt_le (p m r : ℕ) (hm : 0 < m) (hr : r < p) :
(((Finset.range p).filter (fun s : ℕ => s < r ∧ s % m = r % m)).card : ℕ)
≤ r / m := by
classical
let f : ℕ → ℕ := fun s => (r - s) / m
let S : Finset ℕ := (Finset.range p).filter (fun s : ℕ => s < r ∧ s % m = r % m)
let T : Finset ℕ := Finset.Icc 1 (r / m)
have hMt : Set.MapsTo f (S : Set ℕ) (T : Set ℕ) := by
intro s hs
rcases Finset.mem_filter.mp hs with ⟨hsp, hscond⟩
rcases hscond with ⟨hsr, hmod⟩
have hme : s ≡ r [MOD m] := by
change s % m = r % m
exact hmod
have hle : s ≤ r := le_of_lt hsr
have hdvd : m ∣ r - s := (Nat.modEq_iff_dvd' hle).mp hme
have hge : m ≤ r - s := by
obtain ⟨q, hq⟩ := hdvd
have hne : r - s ≠ 0 := by omega
have hqpos : 1 ≤ q := by
by_contra hqn
have hq0 : q = 0 := by omega
subst q
simp at hq
exact hne hq
nlinarith
have hpos : 1 ≤ (r - s) / m := (Nat.le_div_iff_mul_le hm).mpr (by simpa using hge)
have hle2 : (r - s) / m ≤ r / m := Nat.div_le_div_right (Nat.sub_le r s)
simpa [f, T] using (Finset.mem_Icc.mpr ⟨hpos, hle2⟩)
have hInj : (S : Set ℕ).InjOn f := by
intro s hs t ht hf
have hmes : s ≡ r [MOD m] := by
have hmod : s % m = r % m := (Finset.mem_filter.mp hs).2.2
change s % m = r % m
exact hmod
have hmet : t ≡ r [MOD m] := by
have hmod : t % m = r % m := (Finset.mem_filter.mp ht).2.2
change t % m = r % m
exact hmod
have hsle : s ≤ r := le_of_lt (Finset.mem_filter.mp hs).2.1
have htle : t ≤ r := le_of_lt (Finset.mem_filter.mp ht).2.1
have hdvdS : m ∣ r - s := (Nat.modEq_iff_dvd' hsle).mp hmes
have hdvdT : m ∣ r - t := (Nat.modEq_iff_dvd' htle).mp hmet
have hcal : r - s = r - t := by
calc r - s = (r - s) / m * m := by rw [Nat.div_mul_cancel hdvdS]
_ = (r - t) / m * m := by
change f s * m = f t * m
rw [hf]
_ = r - t := by rw [Nat.div_mul_cancel hdvdT]
omega
have hle := Finset.card_le_card_of_injOn f hMt hInj
have hT : T.card = r / m := by
simp [T]
rwa [hT] at hle
The number of pairs (r, s) with r < p, s < p, r ≠ s, and
r % m = s % m is at most p·(p-1)/m.
lemma congruentPair_count_le (p m : ℕ) (hm : 0 < m) :
(((Finset.range p).product (Finset.range p)).filter
(fun rs : ℕ × ℕ => rs.1 ≠ rs.2 ∧ rs.1 % m = rs.2 % m)).card
≤ (p : ℝ) * (p - 1 : ℝ) / (m : ℝ) := by
classical
let S : ℕ → Finset ℕ := fun r =>
(Finset.range p).filter (fun s : ℕ => s ≠ r ∧ s % m = r % m)
have hper : ∀ r ∈ Finset.range p, ((S r).card : ℝ) ≤ (p - 1 : ℝ) / (m : ℝ) := by
intro r hr
have hrp : r < p := Finset.mem_range.mp hr
let Sneg : Finset ℕ := (Finset.range p).filter (fun s : ℕ => s < r ∧ s % m = r % m)
let Spos : Finset ℕ := (Finset.range p).filter (fun s : ℕ => r < s ∧ s % m = r % m)
have hsplit : S r = Sneg ∪ Spos := by
ext s
constructor
· intro hs
rcases Finset.mem_filter.mp hs with ⟨hsp, hne, hmod⟩
have hslt : s < r ∨ r < s := by omega
rw [Finset.mem_union]
rcases hslt with hslt | hslt
· exact Or.inl (Finset.mem_filter.mpr ⟨hsp, ⟨hslt, hmod⟩⟩)
· exact Or.inr (Finset.mem_filter.mpr ⟨hsp, ⟨hslt, hmod⟩⟩)
· intro hs
rw [Finset.mem_union] at hs
rcases hs with hs | hs
· rcases Finset.mem_filter.mp hs with ⟨hsp, hsr, hmod⟩
exact Finset.mem_filter.mpr ⟨hsp, ⟨ne_of_lt hsr, hmod⟩⟩
· rcases Finset.mem_filter.mp hs with ⟨hsp, hsr, hmod⟩
exact Finset.mem_filter.mpr ⟨hsp, ⟨ne_of_gt hsr, hmod⟩⟩
have hd : Disjoint Sneg Spos := by
rw [Finset.disjoint_left]
intro s hsneg hspos
simp [Sneg, Spos] at hsneg hspos
omega
have hcard : (S r).card = Sneg.card + Spos.card := by
rw [hsplit, Finset.card_union_of_disjoint hd]
have hb1 : (Spos.card : ℝ) ≤ (p - 1 - r : ℝ) / (m : ℝ) := by
calc
(Spos.card : ℝ) ≤ (((p - 1 - r) / m : ℕ) : ℝ) := by
exact_mod_cast (count_congruent_gt_le p m r hm hrp)
_ ≤ (p - 1 - r : ℝ) / (m : ℝ) := by
have hnum : ((p - 1 - r : ℕ) : ℝ) = (p - 1 - r : ℝ) := by
rw [Nat.cast_sub (by omega : r ≤ p - 1)]
rw [Nat.cast_sub (by omega : 1 ≤ p), Nat.cast_one]
rw [← hnum]
exact Nat.cast_div_le
have hb2 : (Sneg.card : ℝ) ≤ (r : ℝ) / (m : ℝ) := by
calc
(Sneg.card : ℝ) ≤ ((r / m : ℕ) : ℝ) := by
exact_mod_cast (count_congruent_lt_le p m r hm hrp)
_ ≤ (r : ℝ) / (m : ℝ) := Nat.cast_div_le
have hdiv : (r : ℝ) / (m : ℝ) + (p - 1 - r : ℝ) / (m : ℝ) = (p - 1 : ℝ) / (m : ℝ) := by
rw [← add_div]
ring
rw [hcard, Nat.cast_add]
calc (Sneg.card : ℝ) + (Spos.card : ℝ)
≤ (r : ℝ) / (m : ℝ) + (p - 1 - r : ℝ) / (m : ℝ) := add_le_add hb2 hb1
_ = (p - 1 : ℝ) / (m : ℝ) := hdiv
have hPsubset : (((Finset.range p).product (Finset.range p)).filter
(fun rs : ℕ × ℕ => rs.1 ≠ rs.2 ∧ rs.1 % m = rs.2 % m))
⊆ (Finset.range p).biUnion (fun r : ℕ => ({r} : Finset ℕ).product (S r)) := by
intro rs hrs
rcases Finset.mem_filter.mp hrs with ⟨hrsprod, hne, hmod⟩
rw [Finset.product_eq_sprod, Finset.mem_product] at hrsprod
rcases hrsprod with ⟨hrp, hsp⟩
rw [Finset.mem_biUnion]
refine ⟨rs.1, hrp, ?_⟩
rw [Finset.product_eq_sprod, Finset.mem_product]
refine ⟨Finset.mem_singleton.mpr rfl, ?_⟩
rw [Finset.mem_filter]
exact ⟨hsp, hne.symm, hmod.symm⟩
have hcb := Finset.card_biUnion_le (s := Finset.range p)
(t := fun r : ℕ => ({r} : Finset ℕ).product (S r))
calc
((((Finset.range p).product (Finset.range p)).filter
(fun rs : ℕ × ℕ => rs.1 ≠ rs.2 ∧ rs.1 % m = rs.2 % m)).card : ℝ)
≤ (((Finset.range p).biUnion (fun r : ℕ => ({r} : Finset ℕ).product (S r))).card : ℝ) := by
exact_mod_cast (Finset.card_le_card hPsubset)
_ ≤ (∑ r ∈ (Finset.range p : Finset ℕ), ((({r} : Finset ℕ).product (S r)).card : ℝ)) := by
exact_mod_cast hcb
_ = (∑ r ∈ (Finset.range p : Finset ℕ), ((S r).card : ℝ)) := by
apply Finset.sum_congr rfl
intro r hr
simp [Finset.card_product]
_ ≤ (∑ r ∈ (Finset.range p : Finset ℕ), (p - 1 : ℝ) / (m : ℝ)) := by
apply Finset.sum_le_sum
intro r hr
exact hper r hr
_ = (p : ℝ) * (p - 1 : ℝ) / (m : ℝ) := by
rw [Finset.sum_const, Finset.card_range, nsmul_eq_mul]
ring
The general mod-m affine family of CLRS Theorem 11.5:
h_{a,b}(k) = ((a·k + b) mod p) mod m, computed in the field ZMod p and then
reduced modulo the table size m. The index (a,b) ranges over ZMod p ×
ZMod p with a ≠ 0; the codomain is Fin m.
def affineHashMod (p m : ℕ) [NeZero p] [NeZero m]
(t : {a : ZMod p // a ≠ 0} × ZMod p) (k : ZMod p) : Fin m :=
⟨ZMod.val (t.1.1 * k + t.2) % m, Nat.mod_lt _ (NeZero.pos m)⟩
Theorem (CLRS Theorem 11.5, full mod-m form). The general affine family
h_{a,b}(k) = ((a·k + b) mod p) mod m (a ≠ 0) is universal: any two
distinct keys collide under a uniformly random member with probability at most
1/m. This refines the exact m = p construction
affineHash_isUniversal to arbitrary table sizes m ≤ p.
theorem affineHashMod_isUniversal (p m : ℕ) [NeZero p] [NeZero m] (hp : p.Prime) :
IsUniversal (affineHashMod p m) := by
haveI : Fact p.Prime := ⟨hp⟩
intro x y hxy
let B : Finset ({a : ZMod p // a ≠ 0} × ZMod p) :=
(Finset.univ : Finset ({a : ZMod p // a ≠ 0} × ZMod p)).filter
(fun t => affineHashMod p m t x = affineHashMod p m t y)
have hExpect :
fintypeExpect (fun t : {a : ZMod p // a ≠ 0} × ZMod p =>
indicator (affineHashMod p m t x = affineHashMod p m t y))
= (B.card : ℝ) / (Fintype.card ({a : ZMod p // a ≠ 0} × ZMod p) : ℝ) := by
unfold fintypeExpect indicator
have hsum : (∑ t : {a : ZMod p // a ≠ 0} × ZMod p,
(if affineHashMod p m t x = affineHashMod p m t y then (1 : ℝ) else 0))
= (B.card : ℝ) := by
simp [B]
rw [hsum]
rw [hExpect]
let φ : ({a : ZMod p // a ≠ 0} × ZMod p) → ℕ × ℕ := fun t =>
(ZMod.val (t.1.1 * x + t.2), ZMod.val (t.1.1 * y + t.2))
have hφinj : (B : Set ({a : ZMod p // a ≠ 0} × ZMod p)).InjOn φ := by
intro t1 ht1 t2 ht2 hφ
rcases t1 with ⟨a1, b1⟩
rcases t2 with ⟨a2, b2⟩
have hfst : ZMod.val (a1.1 * x + b1) = ZMod.val (a2.1 * x + b2) :=
congrArg Prod.fst hφ
have hsnd : ZMod.val (a1.1 * y + b1) = ZMod.val (a2.1 * y + b2) :=
congrArg Prod.snd hφ
have h1 : a1.1 * x + b1 = a2.1 * x + b2 := ZMod.val_injective p hfst
have h2 : a1.1 * y + b1 = a2.1 * y + b2 := ZMod.val_injective p hsnd
have hxy0 : x - y ≠ 0 := sub_ne_zero.mpr hxy
have hdiff : a1.1 - a2.1 = 0 := by
have hsub : a1.1 * (x - y) = a2.1 * (x - y) := by
calc a1.1 * (x - y) = (a1.1 * x + b1) - (a1.1 * y + b1) := by ring
_ = (a2.1 * x + b2) - (a2.1 * y + b2) := by rw [h1, h2]
_ = a2.1 * (x - y) := by ring
have hmul : (a1.1 - a2.1) * (x - y) = 0 := by
calc (a1.1 - a2.1) * (x - y) = a1.1 * (x - y) - a2.1 * (x - y) := by ring
_ = 0 := by rw [hsub, sub_self]
exact (mul_eq_zero.mp hmul).resolve_right hxy0
have ha : a1.1 = a2.1 := sub_eq_zero.mp hdiff
have hb : b1 = b2 := by
have hx : a1.1 * x + b1 = a1.1 * x + b2 := by
rwa [← ha] at h1
exact add_left_cancel hx
exact Prod.ext (Subtype.ext ha) hb
let C : Finset (ℕ × ℕ) :=
(Finset.range p).product (Finset.range p) |>.filter
(fun rs : ℕ × ℕ => rs.1 ≠ rs.2 ∧ rs.1 % m = rs.2 % m)
have hIm : B.image φ ⊆ C := by
intro rs hrs
rcases (Finset.mem_image.mp hrs) with ⟨t, htB, hφt⟩
rcases t with ⟨a, b⟩
rw [← hφt]
change (ZMod.val (a.1 * x + b), ZMod.val (a.1 * y + b)) ∈ C
have hcoll : affineHashMod p m ⟨a, b⟩ x = affineHashMod p m ⟨a, b⟩ y :=
(Finset.mem_filter.mp htB).2
simp [C]
constructor
· constructor
· exact ZMod.val_lt (a.1 * x + b)
· exact ZMod.val_lt (a.1 * y + b)
· constructor
· intro hval
have h : a.1 * x + b = a.1 * y + b := ZMod.val_injective p hval
have h' : a.1 * (x - y) = 0 := by
calc a.1 * (x - y) = (a.1 * x + b) - (a.1 * y + b) := by ring
_ = 0 := by rw [h, sub_self]
have hxy0 : x - y ≠ 0 := sub_ne_zero.mpr hxy
exact a.2 ((mul_eq_zero.mp h').resolve_right hxy0)
· have hfin : (⟨ZMod.val (a.1 * x + b) % m, Nat.mod_lt _ (NeZero.pos m)⟩ : Fin m)
= ⟨ZMod.val (a.1 * y + b) % m, Nat.mod_lt _ (NeZero.pos m)⟩ := by
simpa [affineHashMod] using hcoll
exact congrArg Fin.val hfin
have hcardIm : (B.image φ).card = B.card := Finset.card_image_of_injOn hφinj
have hleC : B.card ≤ C.card := by
rw [← hcardIm]
exact Finset.card_le_card hIm
have hBcard : (B.card : ℝ) ≤ (p : ℝ) * (p - 1 : ℝ) / (m : ℝ) := by
have hCbound : (C.card : ℝ) ≤ (p : ℝ) * (p - 1 : ℝ) / (m : ℝ) :=
congruentPair_count_le p m (NeZero.pos m)
exact le_trans (by exact_mod_cast hleC) hCbound
have hIcard : (Fintype.card ({a : ZMod p // a ≠ 0} × ZMod p) : ℝ) = (p : ℝ) * (p - 1 : ℝ) := by
rw [Fintype.card_prod]
have hne0 : Fintype.card {a : ZMod p // a = 0} = 1 := by
rw [Fintype.card_subtype_eq]
have h1 : Fintype.card {a : ZMod p // a ≠ 0} = p - 1 := by
have hc := Fintype.card_subtype_compl (p := fun a : ZMod p => a = 0)
rw [hne0, ZMod.card] at hc
simpa [ne_eq, eq_comm] using hc
rw [h1, ZMod.card]
rw [Nat.cast_mul]
rw [Nat.cast_sub hp.one_le]
ring
have hmne : (m : ℝ) ≠ 0 := by exact_mod_cast (NeZero.ne m)
have hIpos : 0 < (p : ℝ) * (p - 1 : ℝ) := by
have hp2 : (2 : ℝ) ≤ (p : ℝ) := by exact_mod_cast hp.two_le
nlinarith
have hdiv : (B.card : ℝ) / ((p : ℝ) * (p - 1 : ℝ)) ≤ 1 / (m : ℝ) := by
rw [div_le_iff₀ hIpos]
calc (B.card : ℝ) ≤ (p : ℝ) * (p - 1 : ℝ) / (m : ℝ) := hBcard
_ = (1 : ℝ) / (m : ℝ) * ((p : ℝ) * (p - 1 : ℝ)) := by
field_simp [hmne]
rw [hIcard]
exact hdivend Chapter11end CLRSImports
import Mathlib11.4. Open Addressing
Open addressing stores every key directly in the table Fin m → Option K (no
chains). Each key k has a probe sequence ⟨h(k,0), h(k,1), …, h(k,m-1)⟩, a
permutation of the slots; a search or insertion walks the probe order until it
finds the key (success), an empty slot (stop), or exhausts the table.
This section formalises three layers.
Main results:
-
Functional model (CLRS §11.4 operational layer).
-
openInsert/openSearch: insert into the first empty slot along the probe order; search until the key or the first empty slot. -
Theorem
openSearch_eq_false_of_absent: a key that is nowhere in the table is not found (absent key not found). -
Theorem
openSearch_openInsert: after inserting a key along a duplicate-free probe order that has an empty slot, a search finds it (inserted key is found).
-
-
Probe schemes (CLRS §11.4, equations (11.5)-(11.7)).
-
linearProbe,quadraticProbe,doubleHashProbeoverZMod m. -
Theorem
linearProbe_bijective: linear probing enumerates every slot. -
Theorem
doubleHashProbe_bijective: double hashing enumerates every slot when the second hash is a unit (coprime tom, CLRS requirement). -
Theorem
quadraticProbe_zero: quadratic probing starts at the base slot.
-
-
Expected-probe bounds under uniform hashing (CLRS Theorems 11.6-11.8).
-
probeTail: the uniform-hashing probability that the firstiprobes of an unsuccessful search all hit occupied slots, the without-replacement product∏_{j<i} (n-j)/(m-j)(CLRS §11.4). -
Theorem
probeTail_le_pow: each such probability is at mostα^i, the per-factor bound(n-j)/(m-j) ≤ n/mthat CLRS uses. -
Theorem
expectedUnsuccessfulProbes_le: expected unsuccessful-search probes≤ 1/(1-α)(CLRS Theorem 11.6), as the tail-sum∑_i probeTail. -
Theorem
expectedInsertionProbes_le: the same1/(1-α)bound for an insertion (CLRS Corollary 11.7). -
Theorem
expectedSuccessfulProbes_le: expected successful-search probes≤ (1/α) * ∑_{j<n} 1/(m-j) = (1/α)(H_m - H_{m-n}), the harmonic form of CLRS Theorem 11.8. -
Theorem
expectedSuccessfulProbes_le_ln: expected successful-search probes≤ (1/α) * ln(1/(1-α)), the logarithmic (closed-form) version of CLRS Theorem 11.8.
-
Status: proved for the functional model, the probe schemes, and the
uniform-hashing expected-probe bounds.
Notation conventions used in this section:
-
m: the number of table slots -
n: the number of stored keys;α = n/mis the load factor (openLoadFactor) -
K: the key type; a slot isOption K(none= empty) -
probeTail m n i:P[first i probes all occupied], the uniform-hashing tail -
H_k: thek-th harmonic number∑_{r=1}^{k} 1/r
The companion UniformProbe development derives the without-replacement
tails from an explicit uniform permutation sample space and identifies their
tail sum with the actual first-empty-slot probe count. The upper bounds retain
their non-full-load hypotheses; the logarithmic successful-search bound requires
0 < n < m. Successful expectation averages insertion-time occupancies.
RAM and cache costs remain outside this model.
namespace CLRSnamespace Chapter11Functional open-addressing model
The table maps each of the m slots to Option K (none = empty). A probe
order is a List of slots (the slot type is left abstract; the probe schemes
below instantiate it at ZMod m). scanFind and scanInsertPos walk a
probe order once.
Walk a probe order looking for k, stopping at the first empty slot: return
true if a slot holding k is reached before any empty slot (open-addressing
search semantics).
def scanFind {S K : Type*} [DecidableEq K] (T : S → Option K) (k : K) :
List S → Bool
| [] => false
| s :: rest =>
if T s = some k then true
else if T s = none then false
else scanFind T k rest
Walk a probe order returning the first empty slot, or none if the order has
no empty slot (table full along this probe order).
def scanInsertPos {S K : Type*} [DecidableEq K] (T : S → Option K) :
List S → Option S
| [] => none
| s :: rest => if T s = none then some s else scanInsertPos T restOpen-addressing search along a probe order.
def openSearch {S K : Type*} [DecidableEq K] (T : S → Option K)
(order : List S) (k : K) : Bool :=
scanFind T k order
Open-addressing insert along a probe order: place the key in the first empty
slot. If the probe order has no empty slot the table is returned unchanged (the
table-full junk value; totality over Option-slot tables).
def openInsert {S K : Type*} [DecidableEq S] [DecidableEq K] (T : S → Option K)
(order : List S) (k : K) : S → Option K :=
match scanInsertPos T order with
| some s => Function.update T s (some k)
| none => TModel correctness (CLRS §11.4 insert/search behaviour)
If a key occupies no slot, an open-addressing search along any probe order fails.
theorem scanFind_absent {S K : Type*} [DecidableEq K] (T : S → Option K) (k : K)
(h : ∀ s, T s ≠ some k) (order : List S) : scanFind T k order = false := by
induction order with
| nil => rfl
| cons s rest ih =>
simp only [scanFind, if_neg (h s)]
by_cases he : T s = none
· simp [he]
· simp only [if_neg he]; exact ih
The first empty slot reported by scanInsertPos is a member of the probe
order.
theorem scanInsertPos_mem {S K : Type*} [DecidableEq K] (T : S → Option K)
(order : List S) (s : S) (h : scanInsertPos T order = some s) : s ∈ order := by
induction order with
| nil => simp [scanInsertPos] at h
| cons a rest ih =>
simp only [scanInsertPos] at h
by_cases ha : T a = none
· rw [if_pos ha, Option.some_inj] at h
subst h; simp
· rw [if_neg ha] at h
exact List.mem_cons_of_mem _ (ih h)
If the probe order has an empty slot, scanInsertPos reports one.
theorem scanInsertPos_isSome_of_empty {S K : Type*} [DecidableEq K]
(T : S → Option K) (order : List S) (h : ∃ s ∈ order, T s = none) :
∃ s, scanInsertPos T order = some s := by
induction order with
| nil => obtain ⟨s, hs, _⟩ := h; simp at hs
| cons a rest ih =>
by_cases ha : T a = none
· exact ⟨a, by simp [scanInsertPos, ha]⟩
· obtain ⟨s, hs, hTs⟩ := h
rw [List.mem_cons] at hs
rcases hs with rfl | hmem
· exact absurd hTs ha
· obtain ⟨s', hs'⟩ := ih ⟨s, hmem, hTs⟩
exact ⟨s', by simp only [scanInsertPos, if_neg ha]; exact hs'⟩After inserting a key into the first empty slot of a duplicate-free probe order, a search along that order finds it.
theorem scanFind_update_of_scanInsertPos {S K : Type*} [DecidableEq S]
[DecidableEq K] (T : S → Option K) (k : K) (order : List S) (s : S)
(hnd : order.Nodup) (hins : scanInsertPos T order = some s) :
scanFind (Function.update T s (some k)) k order = true := by
induction order with
| nil => simp [scanInsertPos] at hins
| cons a rest ih =>
simp only [scanInsertPos] at hins
by_cases ha : T a = none
· rw [if_pos ha, Option.some_inj] at hins
subst hins
simp [scanFind, Function.update_self]
· rw [if_neg ha] at hins
have hmem : s ∈ rest := scanInsertPos_mem T rest s hins
have hnd' := List.nodup_cons.mp hnd
have has : a ≠ s := fun hEq => hnd'.1 (hEq ▸ hmem)
simp only [scanFind, Function.update_of_ne has]
by_cases hak : T a = some k
· rw [if_pos hak]
· rw [if_neg hak, if_neg ha]
exact ih hnd'.2 hinsAC-1 (absent key not found). A key stored in no slot is not found by an open-addressing search along any probe order.
theorem openSearch_eq_false_of_absent {S K : Type*} [DecidableEq K]
(T : S → Option K) (order : List S) (k : K) (h : ∀ s, T s ≠ some k) :
openSearch T order k = false :=
scanFind_absent T k h orderAC-1 (inserted key found). If a duplicate-free probe order has an empty slot, then after inserting a key it is found by a search along the same order.
theorem openSearch_openInsert {S K : Type*} [DecidableEq S] [DecidableEq K]
(T : S → Option K) (order : List S) (k : K) (hnd : order.Nodup)
(hempty : ∃ s ∈ order, T s = none) :
openSearch (openInsert T order k) order k = true := by
obtain ⟨s, hs⟩ := scanInsertPos_isSome_of_empty T order hempty
have hupd : openInsert T order k = Function.update T s (some k) := by
simp only [openInsert, hs]
rw [openSearch, hupd]
exact scanFind_update_of_scanInsertPos T k order s hnd hsProbe schemes (CLRS §11.4, equations (11.5)-(11.7))
Each scheme is a function ZMod m → ZMod m mapping a probe number i to a slot,
for a fixed key. CLRS requires each probe sequence to enumerate all m slots
(be a permutation); linear probing always does, and double hashing does when the
step size is a unit modulo m.
Linear probing (CLRS equation (11.5)): h(k,i) = (h'(k) + i) mod m.
def linearProbe {m : ℕ} (h0 : ZMod m) (i : ZMod m) : ZMod m := h0 + i
Quadratic probing (CLRS equation (11.6)):
h(k,i) = (h'(k) + c₁ i + c₂ i²) mod m.
def quadraticProbe {m : ℕ} (h0 c1 c2 : ZMod m) (i : ZMod m) : ZMod m :=
h0 + c1 * i + c2 * i * i
Double hashing (CLRS equation (11.7)):
h(k,i) = (h₁(k) + i · h₂(k)) mod m.
def doubleHashProbe {m : ℕ} (h1 h2 : ZMod m) (i : ZMod m) : ZMod m := h1 + i * h2
Linear probing enumerates every slot: as a function of the probe number it is
a bijection of ZMod m (CLRS: a linear probe sequence is a permutation).
theorem linearProbe_bijective {m : ℕ} (h0 : ZMod m) :
Function.Bijective (linearProbe h0) := by
refine Function.bijective_iff_has_inverse.mpr ⟨fun t => t - h0, ?_, ?_⟩
· intro i; show (h0 + i) - h0 = i; ring
· intro t; show h0 + (t - h0) = t; ringLinear probing covers every slot (surjectivity form of the permutation property).
theorem linearProbe_surjective {m : ℕ} (h0 : ZMod m) :
Function.Surjective (linearProbe h0) :=
(linearProbe_bijective h0).surjective
Double hashing enumerates every slot when the step h₂ is a unit modulo m
(CLRS: h₂(k) must be relatively prime to m for the probe sequence to be a
permutation).
theorem doubleHashProbe_bijective {m : ℕ} (h1 h2 : ZMod m) (hu : IsUnit h2) :
Function.Bijective (doubleHashProbe h1 h2) := by
obtain ⟨u, rfl⟩ := hu
refine Function.bijective_iff_has_inverse.mpr ⟨fun t => (t - h1) * ↑u⁻¹, ?_, ?_⟩
· intro i
show ((doubleHashProbe h1 (↑u) i) - h1) * ↑u⁻¹ = i
have hu1 : (↑u : ZMod m) * ↑u⁻¹ = 1 := u.mul_inv
unfold doubleHashProbe
calc (h1 + i * ↑u - h1) * ↑u⁻¹ = i * (↑u * ↑u⁻¹) := by ring
_ = i := by rw [hu1, mul_one]
· intro t
show doubleHashProbe h1 (↑u) ((t - h1) * ↑u⁻¹) = t
have hu2 : (↑u⁻¹ : ZMod m) * ↑u = 1 := u.inv_mul
unfold doubleHashProbe
calc h1 + (t - h1) * ↑u⁻¹ * ↑u = h1 + (t - h1) * (↑u⁻¹ * ↑u) := by ring
_ = t := by rw [hu2, mul_one]; ringDouble hashing covers every slot when the step is a unit.
theorem doubleHashProbe_surjective {m : ℕ} (h1 h2 : ZMod m) (hu : IsUnit h2) :
Function.Surjective (doubleHashProbe h1 h2) :=
(doubleHashProbe_bijective h1 h2 hu).surjective
Quadratic probing starts at the base slot h'(k) (probe number 0).
theorem quadraticProbe_zero {m : ℕ} (h0 c1 c2 : ZMod m) :
quadraticProbe h0 c1 c2 0 = h0 := by
unfold quadraticProbe; ringExpected number of probes under uniform hashing (CLRS Theorems 11.6-11.8)
Under the uniform-hashing assumption the probe sequence of each key is equally
likely to be any of the m! permutations of the slots. For an unsuccessful
search with n occupied slots, the probability that the first i probes all hit
occupied slots is the without-replacement product ∏_{j<i} (n-j)/(m-j). The
expected number of probes is the tail-sum E[X] = ∑_{i≥0} P[X > i], which we
bound by the geometric series ∑_i α^i = 1/(1-α) via CLRS's per-factor bound
(n-j)/(m-j) ≤ n/m.
The load factor α = n/m of an open-addressing table.
noncomputable def openLoadFactor (m n : ℕ) : ℝ := (n : ℝ) / (m : ℝ)
Under uniform hashing, probeTail m n i is the probability that the first i
probes of an unsuccessful search all hit occupied slots: the without-replacement
product ∏_{j<i} (n-j)/(m-j) (CLRS §11.4, the factors leading to Theorem 11.6).
noncomputable def probeTail (m n i : ℕ) : ℝ :=
∏ j ∈ Finset.range i, ((n : ℝ) - (j : ℝ)) / ((m : ℝ) - (j : ℝ))
No probe is needed with certainty: probeTail _ _ 0 = 1.
The one-step recurrence of the tail probability.
theorem probeTail_succ (m n i : ℕ) :
probeTail m n (i + 1)
= probeTail m n i * (((n : ℝ) - (i : ℝ)) / ((m : ℝ) - (i : ℝ))) := by
simp only [probeTail, Finset.prod_range_succ]
The tail probabilities are nonnegative (for i ≤ m, where all denominators
are positive).
theorem probeTail_nonneg (m n : ℕ) (hnm : n ≤ m) (i : ℕ) (hi : i ≤ m) :
0 ≤ probeTail m n i := by
by_cases hin : i ≤ n
· apply Finset.prod_nonneg
intro j hj
have hji : j < i := Finset.mem_range.mp hj
have hjn : (j : ℝ) ≤ (n : ℝ) := by
have : j ≤ n := le_trans (Nat.le_of_lt hji) hin
exact_mod_cast this
have hjm : (j : ℝ) < (m : ℝ) := by
have : j < m := lt_of_lt_of_le hji (le_trans hin hnm)
exact_mod_cast this
apply div_nonneg <;> linarith
· have hni : n < i := not_le.mp hin
have hmem : n ∈ Finset.range i := Finset.mem_range.mpr hni
have hz : probeTail m n i = 0 := by
rw [probeTail]; exact Finset.prod_eq_zero hmem (by simp)
simp [hz]
CLRS per-factor bound. Each tail probability is at most α^i
(α = n/m), because (n-j)/(m-j) ≤ n/m. This is the heart of Theorem 11.6.
theorem probeTail_le_pow (m n : ℕ) (hnm : n ≤ m) (i : ℕ) (hi : i ≤ m) :
probeTail m n i ≤ (openLoadFactor m n) ^ i := by
induction i with
| zero => rw [probeTail_zero, pow_zero]
| succ i ih =>
have hile : i ≤ m := le_of_lt (Nat.lt_of_succ_le hi)
have him : i < m := Nat.lt_of_succ_le hi
have hprev : probeTail m n i ≤ (openLoadFactor m n) ^ i := ih hile
have hptnn : 0 ≤ probeTail m n i := probeTail_nonneg m n hnm i hile
have hmpos : (0 : ℝ) < (m : ℝ) := by
have : 0 < m := lt_of_le_of_lt (Nat.zero_le i) him
exact_mod_cast this
have hden : (0 : ℝ) < (m : ℝ) - (i : ℝ) := by
have : (i : ℝ) < (m : ℝ) := by exact_mod_cast him
linarith
have hαnn : 0 ≤ openLoadFactor m n := by
rw [openLoadFactor]; exact div_nonneg (by positivity) (by positivity)
rw [probeTail_succ, pow_succ]
by_cases hin : i < n
· have hile_n : (i : ℝ) ≤ (n : ℝ) := by exact_mod_cast Nat.le_of_lt hin
have hfnn : 0 ≤ ((n : ℝ) - (i : ℝ)) / ((m : ℝ) - (i : ℝ)) :=
div_nonneg (by linarith) (le_of_lt hden)
have hfle : ((n : ℝ) - (i : ℝ)) / ((m : ℝ) - (i : ℝ)) ≤ openLoadFactor m n := by
rw [openLoadFactor, div_le_div_iff₀ hden hmpos]
have hnr : (n : ℝ) ≤ (m : ℝ) := by exact_mod_cast hnm
have hir : (0 : ℝ) ≤ (i : ℝ) := by positivity
nlinarith [mul_le_mul_of_nonneg_right hnr hir]
exact mul_le_mul hprev hfle hfnn (pow_nonneg hαnn i)
· have hni : n ≤ i := not_lt.mp hin
have hnum : (n : ℝ) - (i : ℝ) ≤ 0 := by
have : (n : ℝ) ≤ (i : ℝ) := by exact_mod_cast hni
linarith
have hinv : 0 ≤ ((m : ℝ) - (i : ℝ))⁻¹ := inv_nonneg.mpr (le_of_lt hden)
have hfnp : ((n : ℝ) - (i : ℝ)) / ((m : ℝ) - (i : ℝ)) ≤ 0 := by
rw [div_eq_mul_inv]; nlinarith [hnum, hinv]
have hprod : probeTail m n i * (((n : ℝ) - (i : ℝ)) / ((m : ℝ) - (i : ℝ))) ≤ 0 := by
nlinarith [hptnn, hfnp]
have hpow : 0 ≤ (openLoadFactor m n) ^ i * openLoadFactor m n :=
mul_nonneg (pow_nonneg hαnn i) hαnn
linarith
A partial geometric sum is bounded by the full geometric series
∑_i α^i ≤ 1/(1-α) for 0 ≤ α < 1.
theorem geom_sum_le_inv (α : ℝ) (h0 : 0 ≤ α) (h1 : α < 1) (N : ℕ) :
∑ i ∈ Finset.range N, α ^ i ≤ 1 / (1 - α) := by
have hpos : (0 : ℝ) < 1 - α := by linarith
have hid : (∑ i ∈ Finset.range N, α ^ i) * (1 - α) = 1 - α ^ N := by
have h := geom_sum_mul α N
linear_combination (-1 : ℝ) * h
rw [le_div_iff₀ hpos, hid]
have hpN : (0 : ℝ) ≤ α ^ N := pow_nonneg h0 N
linarith
The expected number of probes for an unsuccessful search under uniform
hashing, as the tail-sum E[X] = ∑_{i} P[X > i] of the probe count X.
noncomputable def expectedUnsuccessfulProbes (m n : ℕ) : ℝ :=
∑ i ∈ Finset.range (m + 1), probeTail m n i
Theorem 11.6 (unsuccessful search). Under uniform hashing the expected
number of probes in an unsuccessful search is at most 1/(1-α) (α = n/m < 1),
proved as the tail-sum bounded by the geometric series.
theorem expectedUnsuccessfulProbes_le (m n : ℕ) (hn : n < m) :
expectedUnsuccessfulProbes m n ≤ 1 / (1 - openLoadFactor m n) := by
have hnm : n ≤ m := le_of_lt hn
have hmpos : 0 < m := lt_of_le_of_lt (Nat.zero_le n) hn
have hmr : (0 : ℝ) < (m : ℝ) := by exact_mod_cast hmpos
have hα0 : 0 ≤ openLoadFactor m n :=
div_nonneg (by positivity) (by positivity)
have hα1 : openLoadFactor m n < 1 := by
rw [openLoadFactor, div_lt_one hmr]; exact_mod_cast hn
calc expectedUnsuccessfulProbes m n
= ∑ i ∈ Finset.range (m + 1), probeTail m n i := rfl
_ ≤ ∑ i ∈ Finset.range (m + 1), (openLoadFactor m n) ^ i := by
apply Finset.sum_le_sum
intro i hi
exact probeTail_le_pow m n hnm i (Nat.lt_succ_iff.mp (Finset.mem_range.mp hi))
_ ≤ 1 / (1 - openLoadFactor m n) := geom_sum_le_inv _ hα0 hα1 (m + 1)
Corollary 11.7 (insertion). Inserting a key probes exactly as an
unsuccessful search does, so its expected number of probes is at most 1/(1-α).
theorem expectedInsertionProbes_le (m n : ℕ) (hn : n < m) :
expectedUnsuccessfulProbes m n ≤ 1 / (1 - openLoadFactor m n) :=
expectedUnsuccessfulProbes_le m n hn
The expected number of probes for a successful search under uniform
hashing: averaging, over the n insertion times j = 0, …, n-1, the expected
unsuccessful-search cost in a table already holding j keys (CLRS proof of
Theorem 11.8: the (j+1)-st key's probe cost equals an unsuccessful search among
j keys).
noncomputable def expectedSuccessfulProbes (m n : ℕ) : ℝ :=
(1 / (n : ℝ)) * ∑ j ∈ Finset.range n, expectedUnsuccessfulProbes m j
Theorem 11.8 (successful search), harmonic form. Under uniform hashing
the expected number of probes in a successful search is at most
(1/α) * ∑_{j<n} 1/(m-j) = (1/α)(H_m - H_{m-n}) (α = n/m). The stated
harmonic-sum bound is proved; the classical (1/α) ln(1/(1-α)) follows via
sum_inv_shift_le_log and is proved as expectedSuccessfulProbes_le_ln.
theorem expectedSuccessfulProbes_le (m n : ℕ) (hn : n ≤ m) (hnpos : 0 < n) :
expectedSuccessfulProbes m n
≤ (1 / openLoadFactor m n) * ∑ j ∈ Finset.range n, 1 / ((m : ℝ) - (j : ℝ)) := by
have hmpos : 0 < m := lt_of_lt_of_le hnpos hn
have hmpos' : (0 : ℝ) < (m : ℝ) := by exact_mod_cast hmpos
have step1 : expectedSuccessfulProbes m n
≤ (1 / (n : ℝ)) * ∑ j ∈ Finset.range n, 1 / (1 - openLoadFactor m j) := by
unfold expectedSuccessfulProbes
apply mul_le_mul_of_nonneg_left _ (by positivity)
apply Finset.sum_le_sum
intro j hj
exact expectedUnsuccessfulProbes_le m j (lt_of_lt_of_le (Finset.mem_range.mp hj) hn)
have hrw : ∀ j ∈ Finset.range n,
1 / (1 - openLoadFactor m j) = (m : ℝ) * (1 / ((m : ℝ) - (j : ℝ))) := by
intro j hj
have hjm : j < m := lt_of_lt_of_le (Finset.mem_range.mp hj) hn
have hjmr : (j : ℝ) < (m : ℝ) := by exact_mod_cast hjm
have hm0 : (m : ℝ) ≠ 0 := ne_of_gt hmpos'
have hsub : (1 : ℝ) - (j : ℝ) / (m : ℝ) = ((m : ℝ) - (j : ℝ)) / (m : ℝ) := by
rw [sub_div, div_self hm0]
rw [openLoadFactor, hsub, one_div_div, mul_one_div]
rw [Finset.sum_congr rfl hrw, ← Finset.mul_sum] at step1
have hfin : (1 / (n : ℝ)) * ((m : ℝ) * ∑ j ∈ Finset.range n, 1 / ((m : ℝ) - (j : ℝ)))
= (1 / openLoadFactor m n) * ∑ j ∈ Finset.range n, 1 / ((m : ℝ) - (j : ℝ)) := by
rw [openLoadFactor, one_div_div]; ring
rw [hfin] at step1
exact step1
The free-capacity identity 1/(1 - n/m) = m/(m-n) for a non-full table
(n < m): the reciprocal of the free capacity equals the ratio of the table size
to the remaining slots. This is the ln(1/(1-α)) = ln(m/(m-n)) identity used to
pass from the harmonic sum bound to the logarithmic form of Theorem 11.8.
lemma openLoadFactor_inv_eq (m n : ℕ) (hn : n < m) :
(1 : ℝ) / (1 - (n : ℝ) / (m : ℝ)) = (m : ℝ) / ((m - n : ℕ) : ℝ) := by
have hmpos : (0 : ℝ) < (m : ℝ) := by exact_mod_cast (lt_of_le_of_lt (Nat.zero_le n) hn)
have hmne : (m : ℝ) ≠ 0 := ne_of_gt hmpos
have hmncast : ((m - n : ℕ) : ℝ) = (m : ℝ) - (n : ℝ) := by
rw [Nat.cast_sub (le_of_lt hn)]
rw [hmncast]
have hsub : (m : ℝ) - (n : ℝ) ≠ 0 := by
have : (n : ℝ) < (m : ℝ) := by exact_mod_cast hn
linarith
have hαne : 1 - (n : ℝ) / (m : ℝ) ≠ 0 := by
have hα : (n : ℝ) / (m : ℝ) < 1 := (div_lt_one hmpos).2 (by exact_mod_cast hn)
linarith
field_simp [hmne, hsub, hαne]
Harmonic-sum bound. For n < m, ∑_{j<n} 1/(m-j) ≤ ln(m/(m-n)): each
term 1/(m-j) is at most ln((m-j)/(m-j-1)) (from the bound
ln(1+x) ≥ x/(1+x) with x = 1/(m-j-1)), and the resulting sum telescopes to
ln(m/(m-n)). This is the integral bound used to obtain the logarithmic form of
Theorem 11.8 from its harmonic form.
lemma sum_inv_shift_le_log (m n : ℕ) (hn : n < m) :
(∑ j ∈ Finset.range n, (1 : ℝ) / ((m : ℝ) - (j : ℝ)))
≤ Real.log ((m : ℝ) / ((m - n : ℕ) : ℝ)) := by
classical
have hterm (j : ℕ) (hj : j < n) : (1 : ℝ) / ((m : ℝ) - (j : ℝ))
≤ Real.log (((m : ℝ) - (j : ℝ)) / (((m : ℝ) - (j : ℝ)) - 1)) := by
have hsucc : j + 1 < m := by omega
have hjm1 : (j : ℝ) + 1 < (m : ℝ) := by exact_mod_cast hsucc
have hden : 0 < ((m : ℝ) - (j : ℝ)) - 1 := by linarith
have hnum : 0 < (m : ℝ) - (j : ℝ) := by linarith
have hx : 0 < ((m : ℝ) - (j : ℝ)) / (((m : ℝ) - (j : ℝ)) - 1) := div_pos hnum hden
have h := Real.one_sub_inv_le_log_of_pos hx
have hlin : (1 : ℝ) - (((m : ℝ) - (j : ℝ)) / (((m : ℝ) - (j : ℝ)) - 1))⁻¹
= (1 : ℝ) / ((m : ℝ) - (j : ℝ)) := by
field_simp [hnum.ne', hden.ne']
ring
rwa [hlin] at h
calc
(∑ j ∈ Finset.range n, (1 : ℝ) / ((m : ℝ) - (j : ℝ)))
≤ ∑ j ∈ Finset.range n,
Real.log (((m : ℝ) - (j : ℝ)) / (((m : ℝ) - (j : ℝ)) - 1)) := by
apply Finset.sum_le_sum
intro j hj
exact hterm j (Finset.mem_range.mp hj)
_ = Real.log ((m : ℝ) / ((m - n : ℕ) : ℝ)) := by
have hsplit : ∀ j ∈ Finset.range n,
Real.log (((m : ℝ) - (j : ℝ)) / (((m : ℝ) - (j : ℝ)) - 1))
= Real.log ((m : ℝ) - (j : ℝ)) - Real.log (((m : ℝ) - (j : ℝ)) - 1) := by
intro j hj
have hjn : j < n := Finset.mem_range.mp hj
have hsucc : j + 1 < m := by omega
have hjm1 : (j : ℝ) + 1 < (m : ℝ) := by exact_mod_cast hsucc
have hden : 0 < ((m : ℝ) - (j : ℝ)) - 1 := by linarith
have hnum : 0 < (m : ℝ) - (j : ℝ) := by linarith
exact Real.log_div hnum.ne' hden.ne'
rw [Finset.sum_congr rfl hsplit]
have htel : (∑ j ∈ Finset.range n,
(Real.log ((m : ℝ) - (j : ℝ)) - Real.log (((m : ℝ) - (j : ℝ)) - 1)))
= Real.log ((m : ℝ) - (0 : ℝ)) - Real.log ((m : ℝ) - (n : ℝ)) := by
simpa [Nat.cast_add, Nat.cast_one, sub_add_eq_sub_sub] using
(Finset.sum_range_sub' (f := fun i : ℕ => Real.log ((m : ℝ) - (i : ℝ))) n)
rw [htel, sub_zero, ← Nat.cast_sub (le_of_lt hn)]
have hm : (m : ℝ) ≠ 0 := by exact_mod_cast (ne_of_gt (lt_of_le_of_lt (Nat.zero_le n) hn))
have hmn : ((m - n : ℕ) : ℝ) ≠ 0 := by exact_mod_cast (ne_of_gt (Nat.sub_pos_of_lt hn))
rw [← Real.log_div hm hmn]
Theorem 11.8 (successful search), logarithmic form. Under uniform
hashing the expected number of probes in a successful search is at most
(1/α) * ln(1/(1-α)) (α = n/m < 1), obtained from the harmonic form
expectedSuccessfulProbes_le via the integral bound
∑_{j<n} 1/(m-j) ≤ ln(m/(m-n)) = ln(1/(1-α)).
theorem expectedSuccessfulProbes_le_ln (m n : ℕ) (hn : n < m) (hnpos : 0 < n) :
expectedSuccessfulProbes m n
≤ (1 / openLoadFactor m n) * Real.log (1 / (1 - openLoadFactor m n)) := by
have hharm := expectedSuccessfulProbes_le m n (le_of_lt hn) hnpos
have hlog : (∑ j ∈ Finset.range n, (1 : ℝ) / ((m : ℝ) - (j : ℝ)))
≤ Real.log ((m : ℝ) / ((m - n : ℕ) : ℝ)) := sum_inv_shift_le_log m n hn
have hαpos : 0 < openLoadFactor m n := by
rw [openLoadFactor]
exact div_pos (by exact_mod_cast hnpos) (by exact_mod_cast (lt_of_le_of_lt (Nat.zero_le n) hn))
have hαnonneg : 0 ≤ 1 / openLoadFactor m n := by
exact one_div_nonneg.mpr (le_of_lt hαpos)
have hconv : Real.log (1 / (1 - openLoadFactor m n)) = Real.log ((m : ℝ) / ((m - n : ℕ) : ℝ)) := by
congr 1
rw [openLoadFactor]
exact openLoadFactor_inv_eq m n hn
have hmult := mul_le_mul_of_nonneg_left hlog hαnonneg
calc
expectedSuccessfulProbes m n
≤ (1 / openLoadFactor m n) * ∑ j ∈ Finset.range n, 1 / ((m : ℝ) - (j : ℝ)) := hharm
_ ≤ (1 / openLoadFactor m n) * Real.log ((m : ℝ) / ((m - n : ℕ) : ℝ)) := hmult
_ = (1 / openLoadFactor m n) * Real.log (1 / (1 - openLoadFactor m n)) := by rw [hconv]end Chapter11end CLRSDefinitions and proofs
CLRSLean.FourthEdition.Chapter_11.Section_11_4_Open_Addressing.UniformProbe.Counting
CLRS Section 11.4 - Counting uniform probe prefixes
We restrict a slot permutation to its first i positions. The symmetric
group acts transitively on these prefix embeddings, so every embedding has the
same number of permutation extensions. Counting all embeddings determines
that extension count and then the number whose range lies in the occupied set.
namespace CLRSnamespace Chapter11
Inclusion of the first i probe positions into a table of size
m.
def probePrefixEmbedding {m i : Nat} (hi : i ≤ m) : Fin i ↪ Fin m :=
Fin.castLEEmb hi
Restriction of a full probe permutation to its first i positions.
def probePermutationPrefix {m i : Nat} (hi : i ≤ m)
(σ : Equiv.Perm (Fin m)) : Fin i ↪ Fin m :=
(probePrefixEmbedding hi).trans σ.toEmbeddingLeft multiplication of permutations becomes the natural action on their prefix embeddings.
theorem probePermutationPrefix_mul {m i : Nat} (hi : i ≤ m)
(τ σ : Equiv.Perm (Fin m)) :
probePermutationPrefix hi (τ * σ) = τ • probePermutationPrefix hi σ := by
ext j
simp [probePermutationPrefix, probePrefixEmbedding,
Function.Embedding.smul_apply, Equiv.Perm.smul_def, Equiv.Perm.mul_apply]Every injective ordered prefix extends to a full slot permutation.
theorem probePermutationPrefix_surjective {m i : Nat} (hi : i ≤ m) :
Function.Surjective (probePermutationPrefix hi) := by
intro e
obtain ⟨τ, hτ⟩ := Equiv.Perm.exists_smul_eq_embedding
(probePrefixEmbedding hi) e
refine ⟨τ, ?_⟩
ext j
have hj := DFunLike.congr_fun hτ j
exact congrArg Fin.val hjFibers of prefix restriction have equal cardinality.
theorem probePermutationPrefix_fiber_card_eq {m i : Nat} (hi : i ≤ m)
(e₁ e₂ : Fin i ↪ Fin m) :
((Finset.univ : Finset (Equiv.Perm (Fin m))).filter
(fun σ => probePermutationPrefix hi σ = e₁)).card =
((Finset.univ : Finset (Equiv.Perm (Fin m))).filter
(fun σ => probePermutationPrefix hi σ = e₂)).card := by
classical
obtain ⟨τ, hτ⟩ := Equiv.Perm.exists_smul_eq_embedding e₁ e₂
have hτinv : τ⁻¹ • e₂ = e₁ := by
rw [← hτ]
simp
refine Finset.card_bij' (fun σ _ => τ * σ) (fun σ _ => τ⁻¹ * σ) ?_ ?_ ?_ ?_
· intro σ hσ
simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hσ ⊢
rw [probePermutationPrefix_mul, hσ, hτ]
· intro σ hσ
simp only [Finset.mem_filter, Finset.mem_univ, true_and] at hσ ⊢
rw [probePermutationPrefix_mul, hσ, hτinv]
· intro σ hσ
simp
· intro σ hσ
simp
A prescribed injective prefix has exactly (m-i)! extensions to a
permutation of all table slots.
theorem probePermutationPrefix_fiber_card {m i : Nat} (hi : i ≤ m)
(e : Fin i ↪ Fin m) :
((Finset.univ : Finset (Equiv.Perm (Fin m))).filter
(fun σ => probePermutationPrefix hi σ = e)).card = (m - i).factorial := by
classical
let base : Fin i ↪ Fin m := probePrefixEmbedding hi
let fiberCard := ((Finset.univ : Finset (Equiv.Perm (Fin m))).filter
(fun σ => probePermutationPrefix hi σ = base)).card
have htotal : m.factorial = m.descFactorial i * fiberCard := by
have hpartition := Finset.card_eq_sum_card_fiberwise
(s := (Finset.univ : Finset (Equiv.Perm (Fin m))))
(t := (Finset.univ : Finset (Fin i ↪ Fin m)))
(f := probePermutationPrefix hi) (by simp)
rw [Finset.card_univ, Fintype.card_perm, Fintype.card_fin] at hpartition
calc
m.factorial = ∑ b : Fin i ↪ Fin m,
((Finset.univ : Finset (Equiv.Perm (Fin m))).filter
(fun σ => probePermutationPrefix hi σ = b)).card := hpartition
_ = ∑ _b : Fin i ↪ Fin m, fiberCard := by
apply Finset.sum_congr rfl
intro b hb
exact probePermutationPrefix_fiber_card_eq hi b base
_ = m.descFactorial i * fiberCard := by
simp [Fintype.card_embedding_eq]
have hfactorial : (m - i).factorial * m.descFactorial i = m.factorial :=
Nat.factorial_mul_descFactorial hi
have hprefixPos : 0 < m.descFactorial i := Nat.descFactorial_pos.mpr hi
have hbase : fiberCard = (m - i).factorial := by
apply Nat.eq_of_mul_eq_mul_left hprefixPos
calc
m.descFactorial i * fiberCard = m.factorial := htotal.symm
_ = m.descFactorial i * (m - i).factorial := by
rw [← hfactorial, Nat.mul_comm]
calc
((Finset.univ : Finset (Equiv.Perm (Fin m))).filter
(fun σ => probePermutationPrefix hi σ = e)).card = fiberCard :=
probePermutationPrefix_fiber_card_eq hi e base
_ = (m - i).factorial := hbaseRestrict the codomain of an embedding to a finite occupied set.
def embeddingIntoOccupied {m i : Nat} (occupied : Finset (Fin m))
(e : Fin i ↪ Fin m) (he : ∀ j, e j ∈ occupied) : Fin i ↪ occupied where
toFun j := ⟨e j, he j⟩
inj' _a _b hab := e.injective (Subtype.ext_iff.mp hab)@[simp] theorem embeddingIntoOccupied_val {m i : Nat} (occupied : Finset (Fin m))
(e : Fin i ↪ Fin m) (he : ∀ j, e j ∈ occupied) (j : Fin i) :
((embeddingIntoOccupied occupied e he j : occupied) : Fin m) = e j := rflNumber of injective ordered prefixes whose values all lie in a fixed occupied set.
theorem occupiedPrefixEmbedding_card {m i : Nat} (occupied : Finset (Fin m)) :
((Finset.univ : Finset (Fin i ↪ Fin m)).filter
(fun e => ∀ j, e j ∈ occupied)).card = occupied.card.descFactorial i := by
classical
calc
((Finset.univ : Finset (Fin i ↪ Fin m)).filter
(fun e => ∀ j, e j ∈ occupied)).card =
(Finset.univ : Finset (Fin i ↪ occupied)).card := by
refine Finset.card_bij
(fun e he => by
have he' : ∀ j, e j ∈ occupied := by
simpa only [Finset.mem_filter, Finset.mem_univ, true_and] using he
exact embeddingIntoOccupied occupied e he') ?_ ?_ ?_
· intro e he
simp
· intro e₁ h₁ e₂ h₂ heq
ext j
have hj := congrArg Subtype.val (DFunLike.congr_fun heq j)
have hfin : e₁ j = e₂ j := by
simpa [embeddingIntoOccupied] using hj
exact congrArg Fin.val hfin
· intro e he
let lifted : Fin i ↪ Fin m :=
{ toFun := fun j => e j
inj' := fun a b hab => e.injective (Subtype.ext hab) }
refine ⟨lifted, ?_, ?_⟩
· simp only [Finset.mem_filter, Finset.mem_univ, true_and]
intro j
exact (e j).property
· ext j
rfl
_ = occupied.card.descFactorial i := by
simp [Fintype.card_embedding_eq]The occupied-prefix event can be read from the restricted embedding.
theorem firstProbesOccupied_iff_prefix {m i : Nat} (occupied : Finset (Fin m))
(hi : i ≤ m) (σ : Equiv.Perm (Fin m)) :
firstProbesOccupied occupied i σ ↔
∀ j : Fin i, probePermutationPrefix hi σ j ∈ occupied := by
constructor
· intro h j
exact h (probePrefixEmbedding hi j) j.isLt
· intro h j hj
let k : Fin i := ⟨j.val, hj⟩
simpa [probePermutationPrefix, probePrefixEmbedding, k] using h k
Full probe permutations whose first i positions are occupied.
noncomputable def occupiedPrefixPermutations {m : Nat}
(occupied : Finset (Fin m)) (i : Nat) : Finset (Equiv.Perm (Fin m)) := by
classical
exact Finset.univ.filter (firstProbesOccupied occupied i)
Exact number of full probe permutations whose first i positions
are occupied.
theorem firstProbesOccupied_card {m i : Nat} (occupied : Finset (Fin m))
(hi : i ≤ m) :
(occupiedPrefixPermutations occupied i).card =
occupied.card.descFactorial i * (m - i).factorial := by
classical
let good := (Finset.univ : Finset (Fin i ↪ Fin m)).filter
(fun e => ∀ j, e j ∈ occupied)
have hfiber := Finset.sum_card_fiberwise_eq_card_filter
(Finset.univ : Finset (Equiv.Perm (Fin m))) good
(probePermutationPrefix hi)
have hfilter :
((Finset.univ : Finset (Equiv.Perm (Fin m))).filter
(fun σ => probePermutationPrefix hi σ ∈ good)) =
occupiedPrefixPermutations occupied i := by
ext σ
simp only [Finset.mem_filter, Finset.mem_univ, true_and, good,
occupiedPrefixPermutations]
exact (firstProbesOccupied_iff_prefix occupied hi σ).symm
rw [hfilter] at hfiber
calc
(occupiedPrefixPermutations occupied i).card =
∑ e ∈ good,
((Finset.univ : Finset (Equiv.Perm (Fin m))).filter
(fun σ => probePermutationPrefix hi σ = e)).card := hfiber.symm
_ = ∑ _e ∈ good, (m - i).factorial := by
apply Finset.sum_congr rfl
intro e he
exact probePermutationPrefix_fiber_card hi e
_ = good.card * (m - i).factorial := by simp
_ = occupied.card.descFactorial i * (m - i).factorial := by
rw [show good.card = occupied.card.descFactorial i by
exact occupiedPrefixEmbedding_card occupied]end Chapter11end CLRSCLRSLean.FourthEdition.Chapter_11.Section_11_4_Open_Addressing.UniformProbe.Definitions
CLRS Section 11.4 - Uniform probe-space definitions
The sample space is the finite type of permutations of the table slots. A permutation sends probe positions to distinct table slots, exactly matching the uniform-hashing assumption used in CLRS Theorems 11.6--11.8.
namespace CLRSnamespace Chapter11open Probability
The first i positions of a probe permutation all hit occupied slots.
def firstProbesOccupied {m : Nat} (occupied : Finset (Fin m)) (i : Nat)
(σ : Equiv.Perm (Fin m)) : Prop :=
∀ j : Fin m, j.val < i → σ j ∈ occupiedIndicator of the occupied-prefix event with its classical decision procedure fixed inside the definition.
noncomputable def firstProbesOccupiedIndicator {m : Nat}
(occupied : Finset (Fin m)) (i : Nat) (σ : Equiv.Perm (Fin m)) : Real := by
classical
exact indicator (firstProbesOccupied occupied i σ)Uniform probability of an occupied prefix in the explicit permutation sample space.
noncomputable def uniformProbeTailProbability {m : Nat}
(occupied : Finset (Fin m)) (i : Nat) : Real := by
classical
exact fintypeExpect (firstProbesOccupiedIndicator occupied i)Number of probes made before an unsuccessful search reaches its first empty slot, including that final empty-slot probe. The definition is the finite tail sum of the concrete permutation execution.
noncomputable def uniformUnsuccessfulProbeCount {m : Nat} (occupied : Finset (Fin m))
(σ : Equiv.Perm (Fin m)) : Nat := by
classical
exact ∑ i ∈ Finset.range (m + 1),
if firstProbesOccupied occupied i σ then 1 else 0
The explicit unsuccessful-search probe count is bounded by the number of
its m + 1 possible tails.
theorem uniformUnsuccessfulProbeCount_le {m : Nat} (occupied : Finset (Fin m))
(σ : Equiv.Perm (Fin m)) :
uniformUnsuccessfulProbeCount occupied σ ≤ m + 1 := by
classical
unfold uniformUnsuccessfulProbeCount
calc
(∑ i ∈ Finset.range (m + 1), if firstProbesOccupied occupied i σ then 1 else 0)
≤ ∑ _i ∈ Finset.range (m + 1), 1 := by
apply Finset.sum_le_sum
intro i hi
split <;> omega
_ = m + 1 := by simpReal-cast form of the concrete tail-count definition.
theorem uniformUnsuccessfulProbeCount_cast {m : Nat} (occupied : Finset (Fin m))
(σ : Equiv.Perm (Fin m)) :
(uniformUnsuccessfulProbeCount occupied σ : Real) =
∑ i ∈ Finset.range (m + 1),
firstProbesOccupiedIndicator occupied i σ := by
classical
simp [uniformUnsuccessfulProbeCount, firstProbesOccupiedIndicator, indicator]
Canonical set consisting of the first n slots, used only to package
the successful-search average over insertion times.
def canonicalOccupied (m n : Nat) : Finset (Fin m) :=
Finset.univ.filter (fun x => x.val < n)The canonical occupied prefix has the requested cardinality when it fits inside the table.
theorem canonicalOccupied_card (m n : Nat) (hn : n ≤ m) :
(canonicalOccupied m n).card = n := by
classical
rw [canonicalOccupied]
have heq : Finset.univ.filter (fun x : Fin m => x.val < n) =
Finset.univ.map (Fin.castLEEmb hn) := by
ext x
simp only [Finset.mem_filter, Finset.mem_univ, true_and, Finset.mem_map]
constructor
· intro hx
exact ⟨⟨x.val, hx⟩, by simp⟩
· rintro ⟨y, _, rfl⟩
exact y.isLt
rw [heq, Finset.card_map, Finset.card_univ, Fintype.card_fin]
Explicit successful-search expectation: average the concrete unsuccessful
probe count at the n insertion-time loads.
noncomputable def uniformSuccessfulExpectedProbes (m n : Nat) : Real :=
(1 / (n : Real)) * ∑ j ∈ Finset.range n,
fintypeExpect (fun σ : Equiv.Perm (Fin m) =>
(uniformUnsuccessfulProbeCount (canonicalOccupied m j) σ : Real))end Chapter11end CLRSCLRSLean.FourthEdition.Chapter_11.Section_11_4_Open_Addressing.UniformProbe.Probability
CLRS Section 11.4 - Uniform permutation probabilities
The counting result is converted to the without-replacement product already used by the chapter. Finite-expectation linearity then identifies the concrete probe-count expectation with the existing tail sum and transports CLRS Theorems 11.6--11.8 to the explicit sample space.
namespace CLRSnamespace Chapter11open ProbabilityThe without-replacement product is the quotient of descending factorials.
theorem probeTail_eq_descFactorial_div (m n i : Nat) (hi : i ≤ m) :
probeTail m n i = (n.descFactorial i : Real) / (m.descFactorial i : Real) := by
by_cases hin : i ≤ n
· rw [probeTail, Nat.descFactorial_eq_prod_range, Nat.descFactorial_eq_prod_range,
Finset.prod_div_distrib]
congr 1
· rw [Nat.cast_prod]
apply Finset.prod_congr rfl
intro j hj
rw [Nat.cast_sub]
exact Nat.le_of_lt (lt_of_lt_of_le (Finset.mem_range.mp hj) hin)
· rw [Nat.cast_prod]
apply Finset.prod_congr rfl
intro j hj
rw [Nat.cast_sub]
exact Nat.le_of_lt (lt_of_lt_of_le (Finset.mem_range.mp hj) hi)
· have hni : n < i := Nat.lt_of_not_ge hin
have hnmem : n ∈ Finset.range i := Finset.mem_range.mpr hni
have hnum : n.descFactorial i = 0 := Nat.descFactorial_eq_zero_iff_lt.mpr hni
rw [probeTail, Finset.prod_eq_zero hnmem (by simp), hnum, Nat.cast_zero, zero_div]
The explicit event probability is its satisfying-permutation count divided
by m!.
theorem uniformProbeTailProbability_eq_card {m i : Nat}
(occupied : Finset (Fin m)) :
uniformProbeTailProbability occupied i =
((occupiedPrefixPermutations occupied i).card : Real) / (m.factorial : Real) := by
classical
unfold uniformProbeTailProbability Probability.fintypeExpect
rw [show (∑ σ : Equiv.Perm (Fin m), firstProbesOccupiedIndicator occupied i σ) =
((occupiedPrefixPermutations occupied i).card : Real) by
simp [firstProbesOccupiedIndicator, indicator, occupiedPrefixPermutations]]
simp [Fintype.card_perm]
The probability of an occupied prefix under a uniform slot permutation is
exactly the chapter's without-replacement product probeTail.
theorem uniformProbeTailProbability_eq_probeTail {m i : Nat}
(occupied : Finset (Fin m)) (hi : i ≤ m) :
uniformProbeTailProbability occupied i = probeTail m occupied.card i := by
rw [uniformProbeTailProbability_eq_card, firstProbesOccupied_card occupied hi]
have hfactorial : ((m.factorial : Nat) : Real) =
((m - i).factorial : Real) * (m.descFactorial i : Real) := by
exact_mod_cast (Nat.factorial_mul_descFactorial hi).symm
have hrest : ((m - i).factorial : Real) ≠ 0 := by positivity
have hprefix : ((m.descFactorial i : Nat) : Real) ≠ 0 := by
exact_mod_cast (Nat.ne_of_gt (Nat.descFactorial_pos.mpr hi))
rw [hfactorial, probeTail_eq_descFactorial_div m occupied.card i hi]
push_cast
field_simpThe expected concrete unsuccessful-search probe count is the existing tail-sum model.
theorem uniformUnsuccessfulExpectedProbes_eq {m : Nat}
(occupied : Finset (Fin m)) :
fintypeExpect (fun σ : Equiv.Perm (Fin m) =>
(uniformUnsuccessfulProbeCount occupied σ : Real)) =
expectedUnsuccessfulProbes m occupied.card := by
calc
fintypeExpect (fun σ : Equiv.Perm (Fin m) =>
(uniformUnsuccessfulProbeCount occupied σ : Real)) =
fintypeExpect (fun σ : Equiv.Perm (Fin m) =>
∑ i ∈ Finset.range (m + 1), firstProbesOccupiedIndicator occupied i σ) := by
apply congrArg fintypeExpect
funext σ
exact uniformUnsuccessfulProbeCount_cast occupied σ
_ = ∑ i ∈ Finset.range (m + 1),
fintypeExpect (firstProbesOccupiedIndicator occupied i) := by
exact fintypeExpect_sum (Finset.range (m + 1))
(fun i σ => firstProbesOccupiedIndicator occupied i σ)
_ = ∑ i ∈ Finset.range (m + 1), probeTail m occupied.card i := by
apply Finset.sum_congr rfl
intro i hi
exact uniformProbeTailProbability_eq_probeTail occupied
(Nat.lt_succ_iff.mp (Finset.mem_range.mp hi))
_ = expectedUnsuccessfulProbes m occupied.card := rflExplicit-sample-space form of CLRS Theorem 11.6.
theorem uniformUnsuccessfulExpectedProbes_le {m : Nat}
(occupied : Finset (Fin m)) (hnotFull : occupied.card < m) :
fintypeExpect (fun σ : Equiv.Perm (Fin m) =>
(uniformUnsuccessfulProbeCount occupied σ : Real)) ≤
1 / (1 - openLoadFactor m occupied.card) := by
rw [uniformUnsuccessfulExpectedProbes_eq]
exact expectedUnsuccessfulProbes_le m occupied.card hnotFullExplicit-sample-space form of CLRS Corollary 11.7 for insertion.
theorem uniformInsertionExpectedProbes_le {m : Nat}
(occupied : Finset (Fin m)) (hnotFull : occupied.card < m) :
fintypeExpect (fun σ : Equiv.Perm (Fin m) =>
(uniformUnsuccessfulProbeCount occupied σ : Real)) ≤
1 / (1 - openLoadFactor m occupied.card) :=
uniformUnsuccessfulExpectedProbes_le occupied hnotFullThe explicit successful-search average agrees with the chapter's insertion- time average.
theorem uniformSuccessfulExpectedProbes_eq (m n : Nat) (hn : n ≤ m) :
uniformSuccessfulExpectedProbes m n = expectedSuccessfulProbes m n := by
unfold uniformSuccessfulExpectedProbes expectedSuccessfulProbes
congr 1
apply Finset.sum_congr rfl
intro j hj
rw [uniformUnsuccessfulExpectedProbes_eq, canonicalOccupied_card]
exact le_trans (Nat.le_of_lt (Finset.mem_range.mp hj)) hnExplicit-sample-space logarithmic form of CLRS Theorem 11.8.
theorem uniformSuccessfulExpectedProbes_le_ln (m n : Nat)
(hn : n < m) (hnpos : 0 < n) :
uniformSuccessfulExpectedProbes m n ≤
(1 / openLoadFactor m n) * Real.log (1 / (1 - openLoadFactor m n)) := by
rw [uniformSuccessfulExpectedProbes_eq m n (le_of_lt hn)]
exact expectedSuccessfulProbes_le_ln m n hn hnposend Chapter11end CLRSImports
import Mathlib
import CLRSLean.Probability.FiniteExpectation
import CLRSLean.FourthEdition.Chapter_11.Section_11_2_Chained_Hash_Tables11.5. Perfect Hashing
This section formalises the two-level perfect-hashing scheme of CLRS §11.5: a
static key set is stored using a primary universal hash into m = n buckets,
and each bucket with n_j keys gets a secondary table of size m_j = n_j²,
which — by the birthday-style collision count — is collision-free with probability
≥ 1/2. The total expected secondary storage is O(n).
Main results:
-
Definition
PerfectHashTable: two-level perfect hash data structure. -
Theorem
perfectSearch_iff_mem: membership correctness (perfectSearch T x ↔ x ∈ T.keys), establishingO(1)worst-case search. -
Theorem
perfectHash_collision_free_prob_ge_half(Theorem 11.9): when hashingnkeys intom = n²slots under a universal family, the hash is collision-free with probability at least1/2. -
Theorem
exists_collision_free_secondary: forn ≥ 2keys inton²slots, an injective secondary hash exists. -
Theorem
perfectHash_expected_total_space_lt_2n(Theorem 11.10): whennkeys are hashed uniformly and independently intom = nprimary buckets, the expected total secondary storageE[Σ_j n_j²]is less than2n(henceO(n)). -
Theorem
perfectHash_expected_trials_le_two: in a truncated model oftindependent trials, the expected number of trials until a collision-free secondary hash is at most2(geometric bound with success probability ≥ 1/2). -
Theorem
perfectHash_expected_construction_time_le_const_n: the expected abstract budgetconstructionCostis less than5n. This formula is not itself a counter attached to a table constructor.
The Construction companion supplies an executed finite-trial constructor
with a guaranteed collision-free terminal fallback, array initialization and
placement, and a separate expected-work refinement to this abstract budget.
Secondary injectivity is required only for stored keys in the relevant bucket.
The sampling model uses all local-index hash assignments under SUHA, not a
constructed family of constant-time hash programs on arbitrary original keys.
RAM, hash-code generation, and arithmetic implementation costs are excluded.
Notation conventions used in this section:
-
n: number of keys -
m: number of primary buckets (andm = nfor Theorem 11.10) -
a : Fin n → Fin m: a hash assignment (SUHA independent-uniform model) -
A : Fin t → (Fin n → Fin (n^2)): a sequence oftindependent trial hashes ofnkeys inton²slots (construction-trial model) -
H : ι → (K → Fin m): a universal family of hash functions -
n_j: number of keys assigned to primary bucketj
namespace CLRSnamespace Chapter11open CLRS.Probabilityopen Finsetopen scoped ClassicalTwo-level perfect hash model
A PerfectHashTable for a finite set of keys uses a primary hash into m buckets
and, for each bucket j, a secondary hash that is collision-free on the keys
assigned to that bucket. The deterministic two-level lookup completes in O(1)
worst-case time (two table lookups, independent of n).
The fields sec and table are per-bucket; the invariant sec_inj ensures no two
stored keys in the same primary bucket share a secondary slot. Nonmembers may
collide with stored keys; lookup checks the stored value, so
table j (sec j x) = some x identifies x uniquely.
The set of keys stored in the table.
Primary hash function mapping each key to a primary bucket.
For each primary bucket j, a secondary hash function mapping keys to slot
indices. The codomain is ℕ; the actual table size per bucket is not needed for
correctness, only for the probabilistic space bound.
The secondary table: for each bucket j and slot s, optionally a key.
The secondary hash is collision-free on the keys in each primary bucket:
if x and y are both in keys, map to the same primary bucket, and get the
same secondary slot, then x = y.
Every key is stored in the table at the slot determined by its primary and secondary hash.
If the table stores a key at a slot, that key maps to that slot.
structure PerfectHashTable (K : Type) [DecidableEq K] (m : ℕ) : Type where keys : Finset K prim : K → Fin m sec : Fin m → K → ℕ table : Fin m → ℕ → Option K sec_inj : ∀ (j : Fin m) (x y : K),
x ∈ keys → y ∈ keys → prim x = j → prim y = j → sec j x = sec j y → x = y table_stores_keys : ∀ x ∈ keys, table (prim x) (sec (prim x) x) = some x table_only_keys : ∀ (j : Fin m) (s : ℕ) (x : K),
table j s = some x → x ∈ keys ∧ prim x = j ∧ sec j x = s
Two-level perfect-hash search: compute the primary bucket j = prim x, the
secondary slot s = sec j x, and check whether table j s holds x.
def perfectSearch [DecidableEq K] (T : PerfectHashTable K m) (x : K) : Prop :=
T.table (T.prim x) (T.sec (T.prim x) x) = some x
Membership correctness of two-level perfect-hash search. A key x is found
by perfectSearch exactly when x ∈ T.keys (CLRS §11.5). This establishes
O(1) worst-case search time (two table lookups).
theorem perfectSearch_iff_mem [DecidableEq K] (T : PerfectHashTable K m) (x : K) :
perfectSearch T x ↔ x ∈ T.keys := by
constructor
· intro h
have hmem := T.table_only_keys (T.prim x) (T.sec (T.prim x) x) x h
exact hmem.1
· intro h
exact T.table_stores_keys x hTheorem 11.9: secondary collision-free with probability at least 1/2
The fintypeExpect operator is monotone: if X ω ≤ Y ω for all ω, then
E[X] ≤ E[Y].
theorem fintypeExpect_mono {Ω : Type} [Fintype Ω] [DecidableEq Ω] {X Y : Ω → ℝ}
(hXY : ∀ ω, X ω ≤ Y ω) : fintypeExpect X ≤ fintypeExpect Y := by
unfold fintypeExpect
refine div_le_div_of_nonneg_right (Finset.sum_le_sum (fun ω _ => hXY ω)) ?_
positivity
fintypeExpect of a negated random variable is the negation of the expectation.
theorem fintypeExpect_neg {Ω : Type} [Fintype Ω] [DecidableEq Ω] (X : Ω → ℝ) :
fintypeExpect (fun ω => -X ω) = -fintypeExpect X := by
simp [fintypeExpect, Finset.sum_neg_distrib, neg_div]
Number of colliding unordered pairs {i, j} with i < j under a hash assignment
a : Fin n → Fin m. Each pair of distinct indices that hash to the same bucket
contributes 1.
noncomputable def collisionCount {m n : ℕ} (a : Fin n → Fin m) : ℝ :=
∑ i : Fin n, ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0)
collisionCount is nonnegative.
theorem collisionCount_nonneg {m n : ℕ} (a : Fin n → Fin m) : 0 ≤ collisionCount a := by
unfold collisionCount
apply Finset.sum_nonneg; intro i hi
apply Finset.sum_nonneg; intro j hj
by_cases h : i < j
· have : 0 ≤ indicator (a i = a j) := by
unfold indicator; split <;> norm_num
simp [h, this]
· simp [h, indicator]
Expected collisions under pairwise independent hashing (SUHA). For hash
assignments a : Fin n → Fin m, the expected number of colliding unordered pairs
is exactly n(n-1)/(2m).
theorem expectedCollisions_suha {m n : ℕ} (hm : 0 < m) :
fintypeExpect (fun a : Fin n → Fin m => collisionCount a)
= (n : ℝ) * ((n : ℝ) - 1) / (2 * (m : ℝ)) := by
haveI : Nonempty (Fin m) := ⟨⟨0, hm⟩⟩
unfold collisionCount
have hE : fintypeExpect (fun a : Fin n → Fin m =>
∑ i : Fin n, ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0))
= ∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 / (m : ℝ)) else 0) := by
rw [fintypeExpect_sum Finset.univ (fun (i : Fin n) (a : Fin n → Fin m) =>
∑ j : Fin n, (if i < j then indicator (a i = a j) else 0))]
refine Finset.sum_congr rfl (fun i _ => ?_)
rw [fintypeExpect_sum Finset.univ (fun (j : Fin n) (a : Fin n → Fin m) =>
if i < j then indicator (a i = a j) else 0)]
refine Finset.sum_congr rfl (fun j _ => ?_)
by_cases hlt : i < j
· simp [hlt, pairCollisionProb i j (ne_of_lt hlt) hm]
· have hcard : Fintype.card (Fin n → Fin m) ≠ 0 := Fintype.card_ne_zero
simp [hlt, fintypeExpect_const hcard 0]
have hpair : (∑ i : Fin n, ∑ j : Fin n, (if i < j then (1 / (m : ℝ)) else 0))
= (1 / (m : ℝ)) * ((n : ℝ) * ((n : ℝ) - 1) / 2) := by
rw [← sum_upper_triangle n, Finset.mul_sum]
refine Finset.sum_congr rfl (fun i _ => ?_)
rw [Finset.mul_sum]
refine Finset.sum_congr rfl (fun j _ => ?_)
by_cases h : i < j <;> simp [h]
rw [hE, hpair]
ring
When n keys are hashed into m = n² slots under SUHA, the expected number of
collisions is less than 1/2 (for n ≥ 2).
theorem expectedCollisions_sq_lt_half {n : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun a : Fin n → Fin (n^2) => collisionCount a) < 1/2 := by
have hm : 0 < n^2 := by
have hnpos : 0 < n := by omega
positivity
rw [expectedCollisions_suha hm]
have hcalc : (n : ℝ) * ((n : ℝ) - 1) / (2 * ((n : ℝ)^2)) < 1/2 := by
have hnpos' : (n : ℝ) > 0 := by exact_mod_cast (show 0 < n from by omega)
have hpos : (0 : ℝ) < 2 * ((n : ℝ)^2) := by positivity
field_simp [hpos.ne']
nlinarith
have hden : ((n ^ 2 : ℕ) : ℝ) = (n : ℝ)^2 := by simp
simpa [hden] using hcalc
Markov's inequality for nonnegative-integer-valued random variables. If
X : Ω → ℕ, then P[X ≥ 1] ≤ E[X].
theorem markov_integer {Ω : Type} [Fintype Ω] [DecidableEq Ω] (X : Ω → ℕ) :
fintypeExpect (fun ω => if X ω ≥ 1 then (1 : ℝ) else 0) ≤
fintypeExpect (fun ω => (X ω : ℝ)) := by
have hpoint : ∀ ω, (if X ω ≥ 1 then (1 : ℝ) else 0) ≤ (X ω : ℝ) := by
intro ω
by_cases h : X ω ≥ 1
· have h' : (1 : ℝ) ≤ (X ω : ℝ) := by exact_mod_cast h
simp [h, h']
· simp [h]
exact fintypeExpect_mono hpoint
Theorem 11.9 (Perfect hashing: secondary collision-free with probability ≥ 1/2).
Let n keys be hashed into m = n² slots under a universal family (or under SUHA).
Then the hash assignment is collision-free with probability at least 1/2.
Equivalently, a secondary table of size n_j² for a bucket with n_j keys is
collision-free with probability at least 1/2 (CLRS Theorem 11.9).
theorem perfectHash_collision_free_prob_ge_half {n : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun a : Fin n → Fin (n^2) =>
indicator (∀ i j : Fin n, a i = a j → i = j))
≥ 1/2 := by
have hm : 0 < n^2 := by
have hnpos' : 0 < n := by omega
positivity
have hnpos : 0 < n := by omega
haveI : Nonempty (Fin n) := ⟨⟨0, hnpos⟩⟩
haveI : Nonempty (Fin n → Fin (n^2)) :=
⟨fun _ => ⟨0, show 0 < n^2 from hm⟩⟩
have hcard : Fintype.card (Fin n → Fin (n^2)) ≠ 0 := Fintype.card_ne_zero
-- `X a` is the number of colliding unordered pairs under `a`, as a ℕ.
let X (a : Fin n → Fin (n^2)) : ℕ :=
(Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))).card
-- `X a = 0` exactly when `a` is injective (collision-free)
have h_inj_iff : ∀ a : Fin n → Fin (n^2),
(∀ i j : Fin n, a i = a j → i = j) ↔ X a = 0 := by
intro a
dsimp [X]
constructor
· intro hinj
apply Finset.card_eq_zero.mpr
apply Finset.not_nonempty_iff_eq_empty.mp
intro hne
rcases hne with ⟨p, hp⟩
rcases Finset.mem_filter.mp hp with ⟨hp_univ, ⟨hlt, heq⟩⟩
exact hlt.ne' (hinj p.1 p.2 heq).symm
· intro hzero
intro i j heq
by_contra! hne
rcases lt_trichotomy i j with (hlt | heq' | hlt)
· have hmem : (i, j) ∈ Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n)) := by
simp [hlt, heq]
have hcard_ne_zero : (Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))).card ≠ 0 :=
Finset.card_ne_zero.mpr ⟨(i, j), hmem⟩
rw [hzero] at hcard_ne_zero
exact hcard_ne_zero rfl
· exact hne heq'
· have hmem : (j, i) ∈ Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n)) := by
simp [hlt, heq.symm]
have hcard_ne_zero : (Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))).card ≠ 0 :=
Finset.card_ne_zero.mpr ⟨(j, i), hmem⟩
rw [hzero] at hcard_ne_zero
exact hcard_ne_zero rfl
-- Rewrite the collision-free indicator in terms of `X`
have h_indicator_eq : (fun a : Fin n → Fin (n^2) => indicator (∀ i j : Fin n, a i = a j → i = j))
= (fun a : Fin n → Fin (n^2) => if X a = 0 then (1 : ℝ) else 0) := by
funext a; simp [indicator, h_inj_iff a]
rw [h_indicator_eq]
-- Relate `collisionCount` (real-valued) to `X` (ℕ-valued)
have h_collision_eq : (fun (a : Fin n → Fin (n^2)) => collisionCount a) =
(fun (a : Fin n → Fin (n^2)) => (X a : ℝ)) := by
funext a
dsimp [collisionCount, X]
have h1 : (∑ i : Fin n, ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0)) =
(∑ p : Fin n × Fin n, (if p.1 < p.2 then indicator (a p.1 = a p.2) else 0)) := by
simp [Fintype.sum_prod_type]
have h2 : (∑ p : Fin n × Fin n, (if p.1 < p.2 then indicator (a p.1 = a p.2) else 0)) =
(∑ p : Fin n × Fin n, (if p.1 < p.2 ∧ a p.1 = a p.2 then (1 : ℝ) else 0)) := by
refine Finset.sum_congr rfl (fun p _ => ?_)
by_cases hlt : p.1 < p.2
· simp [hlt, indicator]
· simp [hlt, indicator]
have h3 : (∑ p : Fin n × Fin n, (if p.1 < p.2 ∧ a p.1 = a p.2 then (1 : ℝ) else 0)) =
(Finset.card (Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))) : ℝ) := by
simp [Finset.sum_filter]
calc
collisionCount a = (∑ i : Fin n, ∑ j : Fin n, (if i < j then indicator (a i = a j) else 0)) := rfl
_ = (∑ p : Fin n × Fin n, (if p.1 < p.2 then indicator (a p.1 = a p.2) else 0)) := h1
_ = (∑ p : Fin n × Fin n, (if p.1 < p.2 ∧ a p.1 = a p.2 then (1 : ℝ) else 0)) := h2
_ = (Finset.card (Finset.filter (fun (p : Fin n × Fin n) => p.1 < p.2 ∧ a p.1 = a p.2)
(Finset.univ : Finset (Fin n × Fin n))) : ℝ) := h3
_ = (X a : ℝ) := rfl
have h_expected_X_lt_half :
fintypeExpect (fun a : Fin n → Fin (n^2) => (X a : ℝ)) < 1/2 := by
rw [← h_collision_eq]
exact expectedCollisions_sq_lt_half hn
-- Markov inequality: P[X ≥ 1] ≤ E[X]
have h_markov : fintypeExpect (fun a : Fin n → Fin (n^2) => if X a ≥ 1 then (1 : ℝ) else 0) ≤
fintypeExpect (fun a : Fin n → Fin (n^2) => (X a : ℝ)) :=
markov_integer X
-- `indicator(X = 0) = 1 - indicator(X ≥ 1)`
have h_decomp : (fun a : Fin n → Fin (n^2) => (if X a = 0 then (1 : ℝ) else 0)) =
(fun a : Fin n → Fin (n^2) => (1 : ℝ) - (if X a ≥ 1 then (1 : ℝ) else 0)) := by
funext a
by_cases h : X a = 0
· simp [h]
· have hpos : X a ≥ 1 := Nat.one_le_of_lt (Nat.pos_of_ne_zero h)
simp [h, hpos]
rw [h_decomp]
have h_expect_sub : fintypeExpect (fun a : Fin n → Fin (n^2) =>
(1 : ℝ) - (if X a ≥ 1 then (1 : ℝ) else 0)) =
(1 : ℝ) - fintypeExpect (fun a : Fin n → Fin (n^2) => (if X a ≥ 1 then (1 : ℝ) else 0)) := by
calc
fintypeExpect (fun a : Fin n → Fin (n^2) =>
(1 : ℝ) - (if X a ≥ 1 then (1 : ℝ) else 0))
= fintypeExpect (fun a : Fin n → Fin (n^2) =>
(1 : ℝ) + (-(if X a ≥ 1 then (1 : ℝ) else 0))) := by
refine congrArg fintypeExpect (funext fun a => ?_)
rfl
_ = fintypeExpect (fun _ : Fin n → Fin (n^2) => (1 : ℝ)) +
fintypeExpect (fun a : Fin n → Fin (n^2) =>
-(if X a ≥ 1 then (1 : ℝ) else 0)) :=
fintypeExpect_add _ _
_ = (1 : ℝ) + (-fintypeExpect (fun a : Fin n → Fin (n^2) =>
(if X a ≥ 1 then (1 : ℝ) else 0))) := by
simp [fintypeExpect_const hcard, fintypeExpect_neg]
_ = (1 : ℝ) - fintypeExpect (fun a : Fin n → Fin (n^2) =>
(if X a ≥ 1 then (1 : ℝ) else 0)) := by ring
rw [h_expect_sub]
have h_bound : fintypeExpect (fun a : Fin n → Fin (n^2) => if X a ≥ 1 then (1 : ℝ) else 0) < 1/2 := by
linarith
linarithTheorem 11.10: expected total space O(n)
The number of keys (out of n) that hash to a given bucket j under assignment a.
noncomputable def bucketSize {m n : ℕ} (a : Fin n → Fin m) (j : Fin m) : ℝ :=
∑ i : Fin n, indicator (a i = j)
The total secondary storage for a hash assignment a: sum over buckets of the
square of the bucket size, i.e. Σ_j n_j². This is the space used if each
bucket j gets a secondary table of size n_j².
noncomputable def totalSecondarySpace {m n : ℕ} (a : Fin n → Fin m) : ℝ :=
∑ j : Fin m, (bucketSize a j) ^ 2
The algebraic identity Σ_j n_j² = Σ_i Σ_k indicator(a i = a k). This expands
the sum of squares into a double sum over key pairs (CLRS proof of Theorem 11.10).
theorem totalSecondarySpace_eq_sum_indicator {m n : ℕ} (a : Fin n → Fin m) :
totalSecondarySpace a = ∑ i : Fin n, ∑ k : Fin n, indicator (a i = a k) := by
unfold totalSecondarySpace bucketSize
calc
∑ j : Fin m, ((∑ i : Fin n, indicator (a i = j)) : ℝ) ^ 2
= ∑ j : Fin m, (∑ i : Fin n, indicator (a i = j)) * (∑ k : Fin n, indicator (a k = j)) := by
simp [sq]
_ = ∑ j : Fin m, ∑ i : Fin n, ∑ k : Fin n, indicator (a i = j) * indicator (a k = j) := by
refine Finset.sum_congr rfl (fun j hj => ?_)
calc
(∑ i : Fin n, indicator (a i = j)) * (∑ k : Fin n, indicator (a k = j))
= ∑ k : Fin n, (∑ i : Fin n, indicator (a i = j)) * indicator (a k = j) := by
rw [Finset.mul_sum]
_ = ∑ k : Fin n, ∑ i : Fin n, indicator (a i = j) * indicator (a k = j) := by
refine Finset.sum_congr rfl (fun k hk => ?_)
rw [Finset.sum_mul]
_ = ∑ i : Fin n, ∑ k : Fin n, indicator (a i = j) * indicator (a k = j) := by
rw [Finset.sum_comm]
_ = ∑ i : Fin n, ∑ k : Fin n, ∑ j : Fin m, indicator (a i = j) * indicator (a k = j) := by
calc
∑ j : Fin m, ∑ i : Fin n, ∑ k : Fin n, indicator (a i = j) * indicator (a k = j)
= ∑ i : Fin n, ∑ j : Fin m, ∑ k : Fin n, indicator (a i = j) * indicator (a k = j) := by
rw [Finset.sum_comm]
_ = ∑ i : Fin n, ∑ k : Fin n, ∑ j : Fin m, indicator (a i = j) * indicator (a k = j) := by
refine Finset.sum_congr rfl (fun i hi => ?_)
rw [Finset.sum_comm]
_ = ∑ i : Fin n, ∑ k : Fin n, indicator (a i = a k) := by
refine Finset.sum_congr rfl (fun i _ => Finset.sum_congr rfl (fun k _ => ?_))
simp [indicator, Finset.sum_ite_eq, Finset.mem_univ]
Theorem 11.10 (Expected total space is O(n)). When n keys are hashed
uniformly and independently into m = n primary buckets, the expected total
secondary storage E[Σ_j n_j²] is strictly less than 2n (CLRS Theorem 11.10).
theorem perfectHash_expected_total_space_lt_2n {n : ℕ} (hn : 0 < n) :
fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) < 2 * (n : ℝ) := by
have hm : 0 < n := hn
haveI : Nonempty (Fin n) := ⟨⟨0, hn⟩⟩
have hcard : Fintype.card (Fin n → Fin n) ≠ 0 := Fintype.card_ne_zero
-- Algebraic identity: Σ_j n_j² = n + Σ_{i≠k} indicator(a i = a k)
have h_identity : ∀ a : Fin n → Fin n,
totalSecondarySpace a = (n : ℝ) + ∑ i : Fin n, ∑ k : Fin n,
(if i ≠ k then indicator (a i = a k) else 0) := by
intro a
calc
totalSecondarySpace a = ∑ i : Fin n, ∑ k : Fin n, indicator (a i = a k) :=
totalSecondarySpace_eq_sum_indicator a
_ = (∑ i : Fin n, indicator (a i = a i)) +
(∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
calc
∑ i : Fin n, ∑ k : Fin n, indicator (a i = a k)
= ∑ i : Fin n, (indicator (a i = a i) + ∑ k : Fin n,
(if i ≠ k then indicator (a i = a k) else 0)) := by
refine Finset.sum_congr rfl (fun i hi => ?_)
have h_inner : ∑ k : Fin n, indicator (a i = a k)
= indicator (a i = a i) + ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := by
calc
∑ k : Fin n, indicator (a i = a k)
= ∑ k : Fin n, ((if i = k then indicator (a i = a i) else 0) +
(if i ≠ k then indicator (a i = a k) else 0)) := by
refine Finset.sum_congr rfl (fun k hk => ?_)
by_cases hik : i = k
· subst hik; simp
· simp [hik]
_ = (∑ k : Fin n, (if i = k then indicator (a i = a i) else 0)) +
(∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
simp [Finset.sum_add_distrib]
_ = indicator (a i = a i) + ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := by
simp [Finset.sum_ite_eq, Finset.mem_univ]
calc
∑ k : Fin n, indicator (a i = a k)
= indicator (a i = a i) + ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := h_inner
_ = indicator (a i = a i) + ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := rfl
_ = (∑ i : Fin n, indicator (a i = a i)) +
(∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
simp [Finset.sum_add_distrib]
_ = (n : ℝ) + ∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0) := by
simp [indicator, Finset.sum_const, Finset.card_univ, Fintype.card_fin]
-- Use the identity inside the expectation
have h_expect_identity :
fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) =
(n : ℝ) + fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
calc
fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) =
fintypeExpect (fun a : Fin n → Fin n => (n : ℝ) + ∑ i : Fin n, ∑ k : Fin n,
(if i ≠ k then indicator (a i = a k) else 0)) := by
refine congrArg fintypeExpect (funext h_identity)
_ = fintypeExpect (fun _ : Fin n → Fin n => (n : ℝ)) +
fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) :=
fintypeExpect_add _ _
_ = (n : ℝ) + fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
simp [fintypeExpect_const hcard, Fintype.card_fin]
rw [h_expect_identity]
-- Compute the remaining expectation: E[Σ_{i≠k} indicator(a i = a k)] = n*(n-1)*(1/n) = n-1
have h_cross_expect :
fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0))
= (n : ℝ) - 1 := by
calc
fintypeExpect (fun a : Fin n → Fin n =>
∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0))
= ∑ i : Fin n, fintypeExpect (fun a : Fin n → Fin n =>
∑ k : Fin n, (if i ≠ k then indicator (a i = a k) else 0)) := by
rw [fintypeExpect_sum Finset.univ]
_ = ∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then
fintypeExpect (fun a : Fin n → Fin n => indicator (a i = a k)) else 0) := by
refine Finset.sum_congr rfl (fun i _ => ?_)
rw [fintypeExpect_sum Finset.univ]
refine Finset.sum_congr rfl (fun k _ => ?_)
by_cases hne : i ≠ k
· simp [hne, pairCollisionProb i k hne hm]
· simp [hne, fintypeExpect_const hcard, Fintype.card_fin]
_ = ∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then (1 / (n : ℝ)) else 0) := by
refine Finset.sum_congr rfl (fun i _ => Finset.sum_congr rfl (fun k _ => ?_))
by_cases hne : i ≠ k
· rw [pairCollisionProb i k hne hm]
· simp [hne]
_ = (n : ℝ) - 1 := by
have h_inner_sum : ∀ i : Fin n, (∑ k : Fin n, (if i ≠ k then (1 / (n : ℝ)) else 0)) = ((n : ℝ) - 1) / (n : ℝ) := by
intro i
calc
(∑ k : Fin n, (if i ≠ k then (1 / (n : ℝ)) else 0))
= (∑ k : Fin n, (if i ≠ k then (1 : ℝ) else 0)) * (1 / (n : ℝ)) := by
simp [Finset.mul_sum, mul_comm]
_ = ((n : ℝ) - 1) * (1 / (n : ℝ)) := by
have hsum : (∑ k : Fin n, (if i ≠ k then (1 : ℝ) else 0)) = (n : ℝ) - 1 := by
calc
(∑ k : Fin n, (if i ≠ k then (1 : ℝ) else 0))
= (∑ k : Fin n, ((1 : ℝ) - (if i = k then (1 : ℝ) else 0))) := by
refine Finset.sum_congr rfl (fun k hk => ?_)
by_cases hik : i = k
· subst hik; simp
· simp [hik]
_ = (∑ k : Fin n, (1 : ℝ)) - (∑ k : Fin n, (if i = k then (1 : ℝ) else 0)) := by
simp [Finset.sum_add_distrib]
_ = (n : ℝ) - 1 := by simp [Fintype.card_fin, Finset.sum_ite_eq, Finset.mem_univ]
rw [hsum]
_ = ((n : ℝ) - 1) / (n : ℝ) := by ring
calc
(∑ i : Fin n, ∑ k : Fin n, (if i ≠ k then (1 / (n : ℝ)) else 0))
= ∑ i : Fin n, (((n : ℝ) - 1) / (n : ℝ)) := by
refine Finset.sum_congr rfl (fun i hi => ?_); rw [h_inner_sum i]
_ = (n : ℝ) * (((n : ℝ) - 1) / (n : ℝ)) := by
simp [Finset.sum_const, Finset.card_univ, Fintype.card_fin]
_ = (n : ℝ) - 1 := by
field_simp [show (n : ℝ) ≠ 0 from by exact_mod_cast hn.ne']
rw [h_cross_expect]
nlinarithConstruction trials: expected trials to a collision-free secondary hash
A hash assignment a : Fin n → Fin (n^2) of n keys into n² slots is
collision-free exactly when it is injective.
abbrev collisionFree {n : ℕ} (a : Fin n → Fin (n^2)) : Prop :=
∀ i j : Fin n, a i = a j → i = j
Split a sequence of k + 1 independent trial hashes into the first k
trials and the last trial. This witnesses that the prefix coordinates are
independent of the last coordinate.
noncomputable def trialsSplitLast {n k : ℕ} :
(Fin (k + 1) → (Fin n → Fin (n^2))) ≃
(Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) where
toFun A := ((fun j : Fin k => A (Fin.castSucc j)), A ⟨k, Nat.lt_succ_self k⟩)
invFun q := fun x : Fin (k + 1) =>
if hx : x.val < k then q.1 ⟨x.val, hx⟩ else q.2
left_inv A := by
funext x
by_cases hx : x.val < k
· simp [hx]
· simp [hx]
have hx' : x = ⟨k, Nat.lt_succ_self k⟩ := by
apply Fin.ext
change x.val = k
omega
rw [hx']
right_inv q := by
obtain ⟨P, L⟩ := q
refine Prod.ext ?_ ?_
· funext j
simp
· simp
Split a sequence of t independent trial hashes into the first k trials
and the remaining t - k trials, for k ≤ t. This witnesses that the prefix
coordinates are independent of the suffix coordinates.
noncomputable def trialsSplitPrefix {n t k : ℕ} (hkt : k ≤ t) :
(Fin t → (Fin n → Fin (n^2))) ≃
(Fin k → (Fin n → Fin (n^2))) × (Fin (t - k) → (Fin n → Fin (n^2))) where
toFun A := ((fun j : Fin k => A (Fin.castLE hkt j)),
(fun j : Fin (t - k) => A ⟨k + j.val, by omega⟩))
invFun q := fun x : Fin t =>
if hx : x.val < k then q.1 ⟨x.val, hx⟩ else q.2 ⟨x.val - k, by omega⟩
left_inv A := by
funext x
by_cases hx : x.val < k
· simp [hx]
· simp [hx]
apply congrArg A
apply Fin.ext
change k + (x.val - k) = x.val
omega
right_inv q := by
obtain ⟨P, S⟩ := q
refine Prod.ext ?_ ?_
· funext j
simp
· funext j
have hnot : ¬ k + j.val < k := by omega
simp [hnot]
Independent-trials failure bound. In k independent trials, each of which
produces a collision-free hash of n ≥ 2 keys into n² slots with probability
at least 1/2 (Theorem 11.9), the probability that all k trials fail is at
most (1/2)^k (CLRS §11.5).
theorem perfectHash_prefix_fail_prob_le {n k : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (A j))) ≤ (1/2 : ℝ)^k := by
induction k with
| zero =>
have htrue : ∀ A : Fin 0 → (Fin n → Fin (n^2)),
(∀ j : Fin 0, ¬ collisionFree (A j)) := by
intro A j
exact Fin.elim0 j
haveI : Nonempty (Fin n → Fin (n^2)) := ⟨fun _ => ⟨0, by
have hnpos : 0 < n := by omega
positivity⟩⟩
have hcard : Fintype.card (Fin 0 → (Fin n → Fin (n^2))) ≠ 0 := Fintype.card_ne_zero
calc
fintypeExpect (fun A : Fin 0 → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin 0, ¬ collisionFree (A j)))
= fintypeExpect (fun _ : Fin 0 → (Fin n → Fin (n^2)) => (1 : ℝ)) := by
refine congrArg fintypeExpect (funext fun A => ?_)
have hP : (∀ j : Fin 0, ¬ collisionFree (A j)) := htrue A
unfold indicator
rw [if_pos hP]
_ = 1 := by
simp [fintypeExpect_const hcard]
_ ≤ (1/2 : ℝ)^0 := by
simp
| succ k ih =>
have hnpos : 0 < n := by omega
haveI : Nonempty (Fin n) := ⟨⟨0, hnpos⟩⟩
haveI : Nonempty (Fin n → Fin (n^2)) := ⟨fun _ => ⟨0, by positivity⟩⟩
have hcardΩ : Fintype.card (Fin n → Fin (n^2)) ≠ 0 := Fintype.card_ne_zero
-- `∀ j : Fin (k+1), ¬ CF (A j)` splits as the prefix conjunction and the
-- last trial
have hsplit : ∀ A : Fin (k + 1) → (Fin n → Fin (n^2)),
indicator (∀ j : Fin (k + 1), ¬ collisionFree (A j))
= indicator ((∀ j : Fin k, ¬ collisionFree (A (Fin.castSucc j))) ∧
¬ collisionFree (A ⟨k, Nat.lt_succ_self k⟩)) := by
intro A
have hiff : (∀ j : Fin (k + 1), ¬ collisionFree (A j)) ↔
(∀ j : Fin k, ¬ collisionFree (A (Fin.castSucc j))) ∧
¬ collisionFree (A ⟨k, Nat.lt_succ_self k⟩) := by
constructor
· intro h
constructor
· intro j
exact h (Fin.castSucc j)
· exact h ⟨k, Nat.lt_succ_self k⟩
· rintro ⟨hpre, hlast⟩ j
by_cases hj : j.val < k
· have hj' : j = Fin.castSucc ⟨j.val, hj⟩ := by
ext
rfl
rw [hj']
exact hpre ⟨j.val, hj⟩
· have hj' : j = ⟨k, Nat.lt_succ_self k⟩ := by
apply Fin.ext
change j.val = k
omega
rw [hj']
exact hlast
unfold indicator
by_cases hP1 : ∀ j : Fin (k + 1), ¬collisionFree (A j)
· rw [if_pos hP1, if_pos (hiff.mp hP1)]
· rw [if_neg hP1, if_neg (fun hP2 => hP1 (hiff.mpr hP2))]
-- reindex the product sample space so the prefix and last trial separate
have he := fintypeExpect_equiv (trialsSplitLast (n := n) (k := k))
(fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2)))
have hLHS : fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k + 1), ¬ collisionFree (A j)))
= fintypeExpect (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2))) := by
calc
fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k + 1), ¬ collisionFree (A j)))
= fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (A (Fin.castSucc j))) ∧
¬ collisionFree (A ⟨k, Nat.lt_succ_self k⟩))) := by
refine congrArg fintypeExpect (funext fun A => hsplit A)
_ = fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree ((trialsSplitLast (n := n) (k := k) A).1 j)) ∧
¬ collisionFree ((trialsSplitLast (n := n) (k := k) A).2))) := by
refine congrArg fintypeExpect (funext fun A => ?_)
simp [trialsSplitLast]
_ = fintypeExpect (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2))) := he
-- the conjunction of the independent prefix event and the last-trial event
-- factorizes as a product of expectations
have hprod : fintypeExpect (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2)))
= fintypeExpect (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j)))
* fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a)) := by
have hsplit2 : (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
indicator ((∀ j : Fin k, ¬ collisionFree (p.1 j)) ∧ ¬ collisionFree (p.2)))
= (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin n → Fin (n^2)) =>
(fun B : Fin k → (Fin n → Fin (n^2)) => indicator (∀ j : Fin k, ¬ collisionFree (B j))) p.1
* (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a)) p.2) := by
funext p
by_cases hpre : ∀ j : Fin k, ¬ collisionFree (p.1 j)
· by_cases hlast : ¬ collisionFree (p.2)
· simp [indicator, hpre, hlast]
· simp [indicator, hpre, hlast]
· by_cases hlast : ¬ collisionFree (p.2)
· simp [indicator, hpre, hlast]
· simp [indicator, hpre, hlast]
rw [hsplit2]
exact expect_mul_of_indep (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j)))
(fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a))
-- a single trial fails with probability at most 1/2 (Theorem 11.9)
have hfail : fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a)) ≤ 1/2 := by
have hE : fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a))
= 1 - fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (collisionFree a)) := by
calc
fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a))
= fintypeExpect (fun a : Fin n → Fin (n^2) => (1 : ℝ) - indicator (collisionFree a)) := by
refine congrArg fintypeExpect (funext fun a => ?_)
by_cases h : collisionFree a
· unfold indicator
rw [if_neg (fun hna => hna h), if_pos h]
simp
· unfold indicator
rw [if_pos h, if_neg h]
simp
_ = fintypeExpect (fun a : Fin n → Fin (n^2) => (1 : ℝ) + (-indicator (collisionFree a))) := by
refine congrArg fintypeExpect (funext fun a => ?_)
ring
_ = fintypeExpect (fun _ : Fin n → Fin (n^2) => (1 : ℝ)) +
fintypeExpect (fun a : Fin n → Fin (n^2) => -indicator (collisionFree a)) := fintypeExpect_add _ _
_ = 1 - fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (collisionFree a)) := by
rw [fintypeExpect_const hcardΩ, fintypeExpect_neg]
ring
rw [hE]
have hcf : 1/2 ≤ fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (collisionFree a)) := by
simpa [collisionFree] using (perfectHash_collision_free_prob_ge_half (n := n) hn)
linarith
-- assemble: E[prefix ∧ last] = E[prefix] · E[last] ≤ (1/2)^k · (1/2)
calc
fintypeExpect (fun A : Fin (k + 1) → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k + 1), ¬ collisionFree (A j)))
= fintypeExpect (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j)))
* fintypeExpect (fun a : Fin n → Fin (n^2) => indicator (¬ collisionFree a)) := by
rw [hLHS, hprod]
_ ≤ (1/2 : ℝ)^k * (1/2) := by
exact mul_le_mul ih hfail
(fintypeExpect_nonneg (fun a : Fin n → Fin (n^2) => by
unfold indicator
split <;> norm_num))
(by positivity)
_ = (1/2 : ℝ)^(k + 1) := by
simp [pow_succ]
The probability that the first k of t independent trials all fail is at
most (1/2)^k: the remaining t - k trials are independent of the prefix and
marginalise out.
theorem perfectHash_trials_prefix_fail_prob_le {n k t : ℕ} (hn : 2 ≤ n) (hkt : k ≤ t) :
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (A (Fin.castLE hkt j)))) ≤ (1/2 : ℝ)^k := by
haveI : Nonempty (Fin n → Fin (n^2)) := ⟨fun _ => ⟨0, by
have hnpos : 0 < n := by omega
positivity⟩⟩
have hcard_suffix : Fintype.card (Fin (t - k) → (Fin n → Fin (n^2))) ≠ 0 := Fintype.card_ne_zero
have hpre : fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (A (Fin.castLE hkt j))))
= fintypeExpect (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j))) := by
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (A (Fin.castLE hkt j))))
= fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree ((trialsSplitPrefix hkt A).1 j))) := by
refine congrArg fintypeExpect (funext fun A => ?_)
simp [trialsSplitPrefix]
_ = fintypeExpect (fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin (t - k) → (Fin n → Fin (n^2))) =>
indicator (∀ j : Fin k, ¬ collisionFree (p.1 j))) := by
exact fintypeExpect_equiv (trialsSplitPrefix hkt)
(fun p : (Fin k → (Fin n → Fin (n^2))) × (Fin (t - k) → (Fin n → Fin (n^2))) =>
indicator (∀ j : Fin k, ¬ collisionFree (p.1 j)))
_ = fintypeExpect (fun B : Fin k → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin k, ¬ collisionFree (B j))) := by
exact fintypeExpect_fst hcard_suffix
(fun B : Fin k → (Fin n → Fin (n^2)) => indicator (∀ j : Fin k, ¬ collisionFree (B j)))
rw [hpre]
exact perfectHash_prefix_fail_prob_le hn
The number of failed trials before the first collision-free trial in a
sequence of t independent trials. Equivalently, the sum over k of the
indicators "the first k + 1 trials all fail": each failing trial before the
first success contributes exactly one such prefix. If every trial fails, the
value is t.
noncomputable def failedTrials {n t : ℕ} (A : Fin t → (Fin n → Fin (n^2))) : ℕ :=
(Finset.univ.filter (fun k : Fin t =>
∀ j : Fin (k.val + 1), ¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))).card
failedTrials decomposes as the sum over trial prefixes of the indicator
that the first k + 1 trials all fail.
theorem failedTrials_eq_sum {n t : ℕ} (A : Fin t → (Fin n → Fin (n^2))) :
failedTrials A = ∑ k : Fin t, (if (∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j))) then 1 else 0) := by
unfold failedTrials
-- (Finset.univ.filter p).card = ∑ k ∈ univ, if p k then 1 else 0
simpa using (Finset.sum_boole (fun k : Fin t => ∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))
(Finset.univ : Finset (Fin t))).symm
Expected failed trials before a collision-free hash. In the truncated
model of t independent trials (each collision-free with probability at least
1/2, Theorem 11.9), the expected number of failed trials before the first
collision-free trial is at most 1 — via the tail-sum identity
E[F] = Σ_k P[first k trials fail] ≤ Σ_k (1/2)^k = 1 (CLRS §11.5).
theorem perfectHash_expected_failedTrials_le_one {n t : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ)) ≤ 1 := by
have hdecomp : (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ))
= (fun A : Fin t → (Fin n → Fin (n^2)) => ∑ k : Fin t,
indicator (∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))) := by
funext A
rw [failedTrials_eq_sum A]
simp [indicator, Nat.cast_sum]
have hlin : fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ))
= ∑ k : Fin t, fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))) := by
rw [hdecomp]
exact fintypeExpect_sum Finset.univ _
have hterm : ∀ k : Fin t, fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
indicator (∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j)))) ≤ (1/2 : ℝ)^(k.val + 1) := by
intro k
exact perfectHash_trials_prefix_fail_prob_le hn (Nat.succ_le_of_lt k.isLt)
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ))
≤ ∑ k : Fin t, (1/2 : ℝ)^(k.val + 1) := by
rw [hlin]
exact Finset.sum_le_sum (fun k _ => hterm k)
_ ≤ 1 := by
-- ∑ k : Fin t, (1/2)^(k.val + 1) = (1/2) · ∑ k : Fin t, (1/2)^k.val ≤ (1/2) · 2 = 1
have hfactor : (∑ k : Fin t, (1/2 : ℝ)^(k.val + 1)) = (1/2) * (∑ k : Fin t, (1/2 : ℝ)^k.val) := by
calc
(∑ k : Fin t, (1/2 : ℝ)^(k.val + 1))
= (∑ k : Fin t, (1/2 : ℝ)^k.val * (1/2)) := by
refine Finset.sum_congr rfl (fun k _ => ?_)
rw [pow_succ]
_ = (1/2) * (∑ k : Fin t, (1/2 : ℝ)^k.val) := by
rw [Finset.mul_sum]
simp [mul_comm]
rw [hfactor]
have hgeom : (∑ k : Fin t, (1/2 : ℝ)^k.val) ≤ 2 := by
have hrange : (∑ k : Fin t, (1/2 : ℝ)^k.val) = ∑ i ∈ Finset.range t, (1/2 : ℝ)^i := by
rw [Fin.sum_univ_eq_sum_range (fun i : ℕ => (1/2 : ℝ)^i) t]
rw [hrange]
have hgeom_mul := geom_sum_mul_of_le_one (x := (1/2 : ℝ)) (by norm_num) t
-- (∑ i ∈ range t, (1/2)^i) * (1 - 1/2) = 1 - (1/2)^t, so the product is ≤ 1
have hmul : (∑ i ∈ Finset.range t, (1/2 : ℝ)^i) * (1/2) ≤ 1 := by
norm_num at hgeom_mul
rw [hgeom_mul]
have hpow : 0 ≤ (1/2 : ℝ)^t := by positivity
nlinarith
nlinarith
nlinarith
The number of trials performed until a collision-free secondary hash is
obtained, in a truncated model of t independent trials: the failed trials
before the first collision-free trial, plus the successful trial itself. If
none of the t trials is collision-free, the value is t + 1.
This is a lower truncation of an unbounded waiting time, not an upper estimate.
A constructor using a guaranteed terminal fallback may instead interpret the
last unit as its successful fallback attempt; that requires a separate bridge.
noncomputable def trialsUntilCollisionFree {n t : ℕ} (A : Fin t → (Fin n → Fin (n^2))) : ℕ :=
failedTrials A + 1
Expected number of trials to obtain a collision-free secondary hash. In
the truncated model of t independent trials — each trial hashes n ≥ 2 keys
into n² slots and is collision-free with probability at least 1/2 (Theorem
11.9) — the expected number of trials performed up to and including the first
collision-free trial is at most 2 (CLRS §11.5). This theorem bounds each finite truncation. It does not by itself supply
an infinite trial process or a limit theorem for its expectation.
theorem perfectHash_expected_trials_le_two {n t : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (trialsUntilCollisionFree A : ℝ)) ≤ 2 := by
unfold trialsUntilCollisionFree
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => ((failedTrials A + 1 : ℕ) : ℝ))
= fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ) + 1) := by
refine congrArg fintypeExpect (funext fun A => ?_)
simp
_ = fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ)) + 1 := by
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ) + 1)
= fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ)) +
fintypeExpect (fun _ : Fin t → (Fin n → Fin (n^2)) => (1 : ℝ)) := fintypeExpect_add _ _
_ = fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) => (failedTrials A : ℝ)) + 1 := by
haveI : Nonempty (Fin n → Fin (n^2)) := ⟨fun _ => ⟨0, by
have hnpos : 0 < n := by omega
positivity⟩⟩
have hcard : Fintype.card (Fin t → (Fin n → Fin (n^2))) ≠ 0 := Fintype.card_ne_zero
simp [fintypeExpect_const hcard]
_ ≤ 2 := by
linarith [perfectHash_expected_failedTrials_le_one (n := n) (t := t) hn]
Existence of a collision-free secondary hash. For n ≥ 2 keys hashed into
n² slots, some hash assignment is injective. This follows from Theorem 11.9:
the collision-free probability is at least 1/2 > 0, so the event cannot be
empty (CLRS §11.5).
theorem exists_collision_free_secondary {n : ℕ} (hn : 2 ≤ n) :
∃ a : Fin n → Fin (n^2), ∀ i j : Fin n, a i = a j → i = j := by
by_contra h
have hnone : ∀ a : Fin n → Fin (n^2), ¬ (∀ i j : Fin n, a i = a j → i = j) := by
intro a ha
exact h ⟨a, ha⟩
have hzero : fintypeExpect (fun a : Fin n → Fin (n^2) =>
indicator (∀ i j : Fin n, a i = a j → i = j)) = 0 := by
unfold fintypeExpect
rw [show (∑ a : Fin n → Fin (n ^ 2), indicator (∀ i j : Fin n, a i = a j → i = j)) = 0 by
apply Finset.sum_eq_zero
intro a ha
exact if_neg (hnone a)]
simp
have hcf := perfectHash_collision_free_prob_ge_half (n := n) hn
linarithAbstract construction budget and its finite expectation
Historical abstract two-level budget: primary size plus twice the sum of
squared bucket sizes. This is an analysis expression, not measured execution.
The companion Construction module supplies a counted constructor and a
constant-factor conditional-expectation bridge to this expression.
noncomputable def constructionCost {n : ℕ} (a : Fin n → Fin n) : ℝ :=
(n : ℝ) + 2 * totalSecondarySpace aExpected finite trial-count budget times squared bucket size, for at least two keys. The expression charges a supplied square cost per trial; actual collision checking, initialization, and placement are counted in the companion.
theorem perfectHash_expected_bucket_cost_le {n t : ℕ} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ) * (n : ℝ)^2) ≤ 2 * (n : ℝ)^2 := by
have hsq : 0 ≤ (n : ℝ)^2 := by positivity
calc
fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ) * (n : ℝ)^2)
= (n : ℝ)^2 * fintypeExpect (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ)) := by
have hswap : (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ) * (n : ℝ)^2)
= (fun A : Fin t → (Fin n → Fin (n^2)) => (n : ℝ)^2 * (trialsUntilCollisionFree A : ℝ)) := by
funext A
ring
rw [hswap]
exact fintypeExpect_const_mul ((n : ℝ)^2) (fun A : Fin t → (Fin n → Fin (n^2)) =>
(trialsUntilCollisionFree A : ℝ))
_ ≤ (n : ℝ)^2 * 2 := by
exact mul_le_mul_of_nonneg_left (perfectHash_expected_trials_le_two hn) hsq
_ = 2 * (n : ℝ)^2 := by
ring
The historical abstract construction budget has expectation below 5n
under uniform primary assignment. The theorem name is retained for compatibility;
executed construction costs require the companion's refinement theorem.
theorem perfectHash_expected_construction_time_le_const_n {n : ℕ} (hn : 0 < n) :
fintypeExpect (fun a : Fin n → Fin n => constructionCost a) < 5 * (n : ℝ) := by
have hlin : fintypeExpect (fun a : Fin n → Fin n => constructionCost a)
= (n : ℝ) + 2 * fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) := by
unfold constructionCost
calc
fintypeExpect (fun a : Fin n → Fin n => (n : ℝ) + 2 * totalSecondarySpace a)
= fintypeExpect (fun _ : Fin n → Fin n => (n : ℝ)) +
fintypeExpect (fun a : Fin n → Fin n => 2 * totalSecondarySpace a) :=
fintypeExpect_add _ _
_ = (n : ℝ) + 2 * fintypeExpect (fun a : Fin n → Fin n => totalSecondarySpace a) := by
haveI : Nonempty (Fin n) := ⟨⟨0, hn⟩⟩
have hcard : Fintype.card (Fin n → Fin n) ≠ 0 := Fintype.card_ne_zero
rw [fintypeExpect_const hcard, fintypeExpect_const_mul]
rw [hlin]
have hspace := perfectHash_expected_total_space_lt_2n hn
nlinarithend Chapter11end CLRSDefinitions and proofs
CLRSLean.FourthEdition.Chapter_11.Section_11_5_Perfect_Hashing.Construction.Secondary
Executed finite secondary perfect-hash construction
Every candidate runs a collision check, initializes its secondary slot array,
and places its local key indices. The returned ledger counts hash evaluations,
equality comparisons, slot initialization, and indexed writes. Supplied finite
traces stop on success; exhaustion executes a deterministic injective fallback.
Thus the actual attempts equal the existing failedTrials + 1 statistic.
Hashes act on local indices Fin n. Hash-family sampling and representation
costs are outside this indexed-operation model. The finite expectation concerns
uniform full assignments, and is not a theorem about an unbounded retry process.
namespace CLRS.Chapter11.PerfectConstructionHash equality tests, charged for two hash evaluations and one comparison.
def differsFrom (a : α → β) [DecidableEq β] (x : α) : List α → Bool × Nat
| [] => (true, 0)
| y :: ys =>
if a x = a y then (false, 3)
else let rest := differsFrom a x ys; (rest.1, rest.2 + 3)theorem differsFrom_correct (a : α → β) [DecidableEq β] (x : α) (xs : List α) :
(differsFrom a x xs).1 = true ↔ ∀ y ∈ xs, a x ≠ a y := by
induction xs with
| nil => simp [differsFrom]
| cons y ys ih => by_cases h : a x = a y <;> simp [differsFrom, h, ih]theorem differsFrom_work_le (a : α → β) [DecidableEq β] (x : α) (xs : List α) :
(differsFrom a x xs).2 ≤ 3 * xs.length := by
induction xs with
| nil => simp [differsFrom]
| cons y ys ih => simp only [differsFrom]; split <;> simp_all; omegaCheck all distinct input positions, stopping at the first duplicate hash.
def checkDistinct (a : α → β) [DecidableEq β] : List α → Bool × Nat
| [] => (true, 0)
| x :: xs =>
let head := differsFrom a x xs
if head.1 then
let tail := checkDistinct a xs
(tail.1, head.2 + tail.2)
else (false, head.2)theorem checkDistinct_correct (a : α → β) [DecidableEq β] (xs : List α) :
(checkDistinct a xs).1 = true ↔ (xs.map a).Nodup := by
induction xs with
| nil => simp [checkDistinct]
| cons x xs ih =>
simp only [checkDistinct]
split
next h =>
have hh := (differsFrom_correct a x xs).mp h
simp only [List.map_cons, List.nodup_cons, List.mem_map]
constructor
· intro ht
exact ⟨by rintro ⟨y, hy, he⟩; exact hh y hy he.symm, ih.mp ht⟩
· intro ht; exact ih.mpr ht.2
next h =>
simp only [Bool.false_eq_true, List.map_cons, List.nodup_cons, false_iff]
intro hn
apply h
apply (differsFrom_correct a x xs).mpr
intro y hy he
exact hn.1 (List.mem_map.mpr ⟨y, hy, he.symm⟩)theorem checkDistinct_work_le (a : α → β) [DecidableEq β] (xs : List α) :
(checkDistinct a xs).2 ≤ 3 * xs.length ^ 2 := by
induction xs with
| nil => simp [checkDistinct]
| cons x xs ih =>
have hh := differsFrom_work_le a x xs
simp only [checkDistinct]
split <;> simp only [List.length_cons] <;> nlinarithabbrev Hash (n : Nat) := Fin n → Fin (n ^ 2)
theorem check_hash_correct {n : Nat} (a : Hash n) :
(checkDistinct a (List.finRange n)).1 = true ↔ collisionFree a := by
rw [checkDistinct_correct, List.nodup_map_iff_inj_on (List.nodup_finRange n)]
simp [collisionFree]Initialize actual empty secondary slots, counting each array push.
def emptySlots (α : Type*) : Nat → Array (Option α) × Nat
| 0 => (#[], 0)
| m + 1 => let prev := emptySlots α m; (prev.1.push none, prev.2 + 1)@[simp] theorem emptySlots_array (α : Type*) (m : Nat) :
(emptySlots α m).1 = Array.replicate m none := by
induction m with
| zero => simp [emptySlots]
| succ m ih => simp [emptySlots, ih, Array.replicate_succ]@[simp] theorem emptySlots_work (α : Type*) (m : Nat) : (emptySlots α m).2 = m := by
induction m with
| zero => rfl
| succ m ih => simp [emptySlots, ih]Place each key in its selected slot. A step charges a hash evaluation and an indexed write; initialization and collision checks are counted separately.
def place {n : Nat} (a : Hash n) : List (Fin n) → Array (Option (Fin n)) →
Array (Option (Fin n)) × Nat
| [], out => (out, 0)
| i :: is, out =>
let rest := place a is out
(rest.1.setIfInBounds (a i).val (some i), rest.2 + 2)@[simp] theorem place_size {n : Nat} (a : Hash n) (xs : List (Fin n)) (out : Array (Option (Fin n))) :
(place a xs out).1.size = out.size := by
induction xs with
| nil => rfl
| cons i xs ih => simp [place, ih]@[simp] theorem place_work {n : Nat} (a : Hash n) (xs : List (Fin n)) (out : Array (Option (Fin n))) :
(place a xs out).2 = 2 * xs.length := by
induction xs with
| nil => simp [place]
| cons i xs ih => simp [place, ih]; omegaEach slot contains the first input key assigned there, or its initial value.
theorem place_get {n : Nat} (a : Hash n) (xs : List (Fin n))
(out : Array (Option (Fin n))) (s : Nat) (hs : s < out.size) :
(place a xs out).1[s]'(by simpa using hs) =
(xs.find? (fun i => (a i).val == s)).orElse (fun _ => out[s]) := by
induction xs with
| nil => simp [place]
| cons i xs ih =>
simp only [place]
rw [Array.getElem_setIfInBounds (by simpa using hs)]
by_cases he : (a i).val = s <;> simp [he, List.find?, ih, Bool.beq_eq_decide_eq]structure Attempt (n : Nat) where
hash : Hash n
slots : Array (Option (Fin n))
success : Bool
work : NatAn actual collision check followed by secondary-array placement.
def attempt {n : Nat} (a : Hash n) : Attempt n :=
let checked := checkDistinct a (List.finRange n)
let initial := emptySlots (Fin n) (n ^ 2)
let filled := place a (List.finRange n) initial.1
⟨a, filled.1, checked.1, checked.2 + initial.2 + filled.2⟩@[simp] theorem attempt_hash {n : Nat} (a : Hash n) : (attempt a).hash = a := rfl@[simp] theorem attempt_size {n : Nat} (a : Hash n) : (attempt a).slots.size = n ^ 2 := by
simp [attempt]@[simp] theorem attempt_success {n : Nat} (a : Hash n) :
(attempt a).success = true ↔ collisionFree a := check_hash_correct a
theorem attempt_work_le {n : Nat} (a : Hash n) : (attempt a).work ≤ 6 * n ^ 2 := by
have h := checkDistinct_work_le a (List.finRange n)
have hn : n ≤ n ^ 2 := by nlinarith
simpa only [attempt, emptySlots_work, place_work, List.length_finRange] using
(show (checkDistinct a (List.finRange n)).2 + n ^ 2 + 2 * n ≤ 6 * n ^ 2 by
simp only [List.length_finRange] at h
nlinarith)
theorem attempt_get {n : Nat} (a : Hash n) (s : Nat) (hs : s < n ^ 2) :
(attempt a).slots[s]'(by simpa using hs) =
(List.finRange n).find? (fun i => (a i).val == s) := by
have h := place_get a (List.finRange n) (emptySlots (Fin n) (n ^ 2)).1 s (by simpa using hs)
simpa [attempt] using h
theorem attempt_stores {n : Nat} (a : Hash n) (ha : collisionFree a) (i : Fin n) :
(attempt a).slots[(a i).val]'(by simp) = some i := by
rw [attempt_get a _ (a i).isLt]
cases h : (List.finRange n).find? (fun j => (a j).val == (a i).val) with
| none =>
have hh := List.find?_eq_none.mp h i (by simp)
simp at hh
| some j =>
have hh := List.find?_some h
have he : a j = a i := Fin.ext (by simpa using hh)
rw [ha j i he]The terminal assignment is explicitly injective, including the empty domain.
def fallback (n : Nat) : Hash n := fun i =>
⟨i.val, lt_of_lt_of_le i.isLt (by nlinarith : n ≤ n ^ 2)⟩theorem fallback_injective (n : Nat) : collisionFree (fallback n) := by
intro i j hij
exact Fin.ext (congrArg (fun x : Fin (n ^ 2) => x.val) hij)structure Build (n : Nat) where
selected : Attempt n
attempts : Nat
work : NatTry supplied candidates in order; after their exhaustion execute one explicit injective fallback. Every returned table therefore succeeds.
def build {n : Nat} : List (Hash n) → Build n
| [] => let final := attempt (fallback n); ⟨final, 1, final.work⟩
| a :: as =>
let trial := attempt a
if trial.success then ⟨trial, 1, trial.work⟩
else let rest := build as; ⟨rest.selected, rest.attempts + 1, trial.work + rest.work⟩theorem build_success {n : Nat} (as : List (Hash n)) : (build as).selected.success = true := by
induction as with
| nil => simpa [build] using (attempt_success (fallback n)).mpr (fallback_injective n)
| cons a as ih =>
simp only [build]
split
· assumption
· exact ihtheorem build_work_le {n : Nat} (as : List (Hash n)) :
(build as).work ≤ 6 * n ^ 2 * (build as).attempts := by
induction as with
| nil => simpa [build] using attempt_work_le (fallback n)
| cons a as ih =>
have h := attempt_work_le a
simp only [build]
split <;> simp only <;> nlinarithdef failedPrefix {n : Nat} : List (Hash n) → Nat
| [] => 0
| a :: as => if (attempt a).success then 0 else failedPrefix as + 1theorem build_attempts {n : Nat} (as : List (Hash n)) :
(build as).attempts = failedPrefix as + 1 := by
induction as with
| nil => simp [build, failedPrefix]
| cons a as ih => simp only [build, failedPrefix]; split <;> simp_all
lemma failedTrials_cons {n t : Nat} (A : Fin (t + 1) → Hash n) :
failedTrials A = if collisionFree (A 0) then 0 else
failedTrials (fun j : Fin t => A j.succ) + 1 := by
classical
rw [failedTrials_eq_sum, Fin.sum_univ_succ]
have htail (k : Fin t) :
(∀ j : Fin (k.succ.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.succ.isLt) j))) ↔
(¬ collisionFree (A 0) ∧ ∀ j : Fin (k.val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt k.isLt) j).succ)) := by
rw [Fin.forall_fin_succ]
rfl
have hzero :
(∀ j : Fin ((0 : Fin (t + 1)).val + 1),
¬ collisionFree (A (Fin.castLE (Nat.succ_le_of_lt (0 : Fin (t + 1)).isLt) j))) ↔
¬ collisionFree (A 0) := by
constructor
· intro hh
exact hh 0
· intro hh j
have hj : Fin.castLE (Nat.succ_le_of_lt (0 : Fin (t + 1)).isLt) j = 0 := by
apply Fin.ext
have h := j.isLt
change j.val < 1 at h
change j.val = 0
omega
rw [hj]
exact hh
simp_rw [htail, hzero]
by_cases h : collisionFree (A 0)
· have hh : collisionFree (A 0) ↔ True := iff_true_intro h
simp only [hh, not_true_eq_false, false_and, ite_false, Finset.sum_const_zero,
zero_add, ite_true]
· simp only [h, not_false_eq_true, true_and, ite_true, ite_false]
rw [failedTrials_eq_sum]
omega
theorem failedPrefix_ofFn {n t : Nat} (A : Fin t → Hash n) :
failedPrefix (List.ofFn A) = failedTrials A := by
classical
induction t with
| zero => simp [List.ofFn_zero, failedPrefix, failedTrials]
| succ t ih =>
rw [List.ofFn_succ, failedPrefix, failedTrials_cons]
simp only [attempt_success, ih]def buildTrace {n : Nat} : {t : Nat} → (Fin t → Hash n) → Build n
| 0, _ => build []
| _ + 1, A =>
let current := attempt (A 0)
if current.success then ⟨current, 1, current.work⟩
else
let rest := buildTrace (fun j => A j.succ)
⟨rest.selected, rest.attempts + 1, current.work + rest.work⟩theorem buildTrace_eq {n t : Nat} (A : Fin t → Hash n) :
buildTrace A = build (List.ofFn A) := by
induction t with
| zero => simp [buildTrace]
| succ t ih => simp [buildTrace, List.ofFn_succ, build, ih]The legacy finite trial random variable is the exact attempt count of finite sampling followed by the terminal injective fallback.
theorem buildTrace_attempts {n t : Nat} (A : Fin t → Hash n) :
(buildTrace A).attempts = trialsUntilCollisionFree A := by
rw [buildTrace_eq, build_attempts, failedPrefix_ofFn, trialsUntilCollisionFree]theorem buildTrace_work_le {n t : Nat} (A : Fin t → Hash n) :
(buildTrace A).work ≤ 6 * n ^ 2 * trialsUntilCollisionFree A := by
simpa [← buildTrace_attempts A, buildTrace_eq] using build_work_le (List.ofFn A)open CLRS.ProbabilityExpected measured secondary work over the stated finite SUHA trace space.
theorem expected_buildTrace_work_le {n t : Nat} (hn : 2 ≤ n) :
fintypeExpect (fun A : Fin t → Hash n => ((buildTrace A).work : ℝ)) ≤
12 * (n : ℝ) ^ 2 := by
classical
calc
fintypeExpect (fun A : Fin t → Hash n => ((buildTrace A).work : ℝ)) ≤
fintypeExpect (fun A : Fin t → Hash n =>
6 * (n : ℝ) ^ 2 * (trialsUntilCollisionFree A : ℝ)) := by
apply fintypeExpect_mono
intro A
exact_mod_cast buildTrace_work_le A
_ = (6 * (n : ℝ) ^ 2) *
fintypeExpect (fun A : Fin t → Hash n => (trialsUntilCollisionFree A : ℝ)) :=
fintypeExpect_const_mul _ _
_ ≤ (6 * (n : ℝ) ^ 2) * 2 :=
mul_le_mul_of_nonneg_left (perfectHash_expected_trials_le_two hn) (by positivity)
_ = _ := by ringEvery selected result is a real executed secondary attempt.
theorem build_selected_eq {n : Nat} (as : List (Hash n)) :
(build as).selected = attempt (build as).selected.hash := by
induction as with
| nil => rfl
| cons a as ih => simp only [build]; split <;> first | rfl | exact ih
theorem build_hash_injective {n : Nat} (as : List (Hash n)) :
collisionFree (build as).selected.hash := by
have h := build_success as
rw [build_selected_eq] at h
exact attempt_success _ |>.mp h
theorem attempt_only {n : Nat} (a : Hash n) (s : Nat) (i : Fin n)
(h : (attempt a).slots[s]?.join = some i) : (a i).val = s := by
by_cases hs : s < n ^ 2
· have hsize : s < (attempt a).slots.size := by simpa using hs
rw [Array.getElem?_eq_getElem hsize, Option.join_some, attempt_get a s hs] at h
have hh := List.find?_some h
simpa using hh
· have hsize : ¬ s < (attempt a).slots.size := by simpa using hs
simp [Array.getElem?_eq_none (Nat.le_of_not_gt hsize)] at hPackage the constructed array as a one-bucket perfect table. All key slots come from the returned placement execution, rather than from a specification search.
def tableOfBuild {n : Nat} (as : List (Hash n)) : PerfectHashTable (Fin n) 1 where
keys := Finset.univ
prim := fun _ => 0
sec := fun _ i => ((build as).selected.hash i).val
table := fun _ s => (build as).selected.slots[s]?.join
sec_inj := by
intro j x y hx hy hpx hpy hs
exact build_hash_injective as x y (Fin.ext hs)
table_stores_keys := by
intro i hi
rw [build_selected_eq]
have hs := attempt_stores (build as).selected.hash (build_hash_injective as) i
simp only [attempt_hash]
rw [Array.getElem?_eq_getElem (by simp), Option.join_some]
exact hs
table_only_keys := by
intro j s i hi
refine ⟨by simp, Subsingleton.elim _ _, ?_⟩
rw [build_selected_eq] at hi
exact attempt_only _ s i hiThe successful finite builder has a verified membership-query interface.
theorem tableOfBuild_search {n : Nat} (as : List (Hash n)) (i : Fin n) :
perfectSearch (tableOfBuild as) i := by
rw [perfectSearch_iff_mem]
simp [tableOfBuild]
theorem build_work_le_small {n : Nat} (hn : n ≤ 1) (as : List (Hash n)) :
(build as).work ≤ 6 * n ^ 2 := by
cases as with
| nil => simpa [build] using attempt_work_le (fallback n)
| cons a as =>
have hc : collisionFree a := by
intro i j hij
apply Fin.ext
have hi := i.isLt
have hj := j.isLt
omega
have hs := (attempt_success a).mpr hc
simpa only [build, hs, Bool.true_eq, ↓reduceIte] using attempt_work_le aEmpty and singleton buckets are handled directly; the SUHA retry bound is needed only for buckets with at least two keys.
theorem expected_buildTrace_work_le_all (n t : Nat) :
fintypeExpect (fun A : Fin t → Hash n => ((buildTrace A).work : ℝ)) ≤
12 * (n : ℝ) ^ 2 := by
classical
by_cases hn : 2 ≤ n
· exact expected_buildTrace_work_le hn
haveI : Nonempty (Fin t → Hash n) := ⟨fun _ => fallback n⟩
calc
fintypeExpect (fun A : Fin t → Hash n => ((buildTrace A).work : ℝ)) ≤
fintypeExpect (fun _ : Fin t → Hash n => 6 * (n : ℝ) ^ 2) := by
apply fintypeExpect_mono
intro A
rw [buildTrace_eq]
exact_mod_cast build_work_le_small (by omega : n ≤ 1) (List.ofFn A)
_ = 6 * (n : ℝ) ^ 2 := fintypeExpect_const Fintype.card_ne_zero _
_ ≤ _ := by nlinarith [sq_nonneg (n : ℝ)]end CLRS.Chapter11.PerfectConstructionCLRSLean.FourthEdition.Chapter_11.Section_11_5_Perfect_Hashing.Construction.TwoLevel
Measured assembly of all secondary tables
The constructor actually distributes original Fin n payloads into primary
buckets, caches bucket sizes, and executes and retains every secondary builder.
The measured indexed-operation work has conditional expectation at most nine
times constructionCost and unconditional expectation below 45 * n.
Secondary tables hash and store local bucket indices. Original payload buckets remain in the result; converting an original query key to its local index is a separate representation boundary. This does not claim constant-time hashing on an arbitrary original key universe or machine runtime for persistent arrays.
namespace CLRS.Chapter11.PerfectConstructionopen CLRS.ProbabilityUniform dependent-product sampling has the expected one-coordinate marginal.
theorem expect_pi_apply {ι : Type} [Fintype ι] [DecidableEq ι]
(Ω : ι → Type) [∀ i, Fintype (Ω i)] [∀ i, DecidableEq (Ω i)]
[∀ i, Nonempty (Ω i)] (j : ι) (X : Ω j → ℝ) :
fintypeExpect (fun w : (i : ι) → Ω i => X (w j)) = fintypeExpect X := by
classical
calc
fintypeExpect (fun w : (i : ι) → Ω i => X (w j)) =
fintypeExpect (fun p : Ω j × ((i : {i : ι // i ≠ j}) → Ω i.val) => X p.1) :=
fintypeExpect_equiv (Equiv.piSplitAt j Ω) (fun p => X p.1)
_ = _ := fintypeExpect_fst Fintype.card_ne_zero Xstructure Assembly {m : Nat} (sizes : Fin m → Nat) where
buckets : Array (Σ j : Fin m, Build (sizes j))
work : NatExecute every bucket constructor, storing its returned table in the output array. Each push of a completed bucket record contributes one more operation.
def assemble {m t : Nat} (sizes : Fin m → Nat)
(A : (j : Fin m) → Fin t → Hash (sizes j)) :
List (Fin m) → Array (Σ j : Fin m, Build (sizes j)) → Assembly sizes
| [], out => ⟨out, 0⟩
| j :: js, out =>
let built := buildTrace (A j)
let rest := assemble sizes A js (out.push ⟨j, built⟩)
⟨rest.buckets, built.work + 1 + rest.work⟩theorem assemble_result {m t : Nat} (sizes : Fin m → Nat)
(A : (j : Fin m) → Fin t → Hash (sizes j))
(js : List (Fin m)) (out : Array (Σ j : Fin m, Build (sizes j))) :
(assemble sizes A js out).buckets.toList =
out.toList ++ js.map (fun j => ⟨j, buildTrace (A j)⟩) := by
induction js generalizing out with
| nil => simp [assemble]
| cons j js ih => simp [assemble, ih, List.append_assoc]theorem assemble_work {m t : Nat} (sizes : Fin m → Nat)
(A : (j : Fin m) → Fin t → Hash (sizes j))
(js : List (Fin m)) (out : Array (Σ j : Fin m, Build (sizes j))) :
(assemble sizes A js out).work = js.length + (js.map (fun j => (buildTrace (A j)).work)).sum := by
induction js generalizing out with
| nil => simp [assemble]
| cons j js ih => simp [assemble, ih]; omega
theorem assemble_success {m t : Nat} (sizes : Fin m → Nat)
(A : (j : Fin m) → Fin t → Hash (sizes j)) :
∀ entry ∈ (assemble sizes A (List.finRange m) #[]).buckets.toList,
entry.2.selected.success = true := by
rw [assemble_result]
simp only [List.nil_append, List.mem_map]
rintro entry ⟨j, hj, rfl⟩
simpa only [buildTrace_eq] using build_success _Expected work of the actual assembly over independent finite per-bucket trace spaces; no assumption on empty/singleton bucket sizes is needed.
theorem expected_assemble_work_le {m : Nat} (sizes : Fin m → Nat) (t : Nat) :
fintypeExpect (fun A : (j : Fin m) → Fin t → Hash (sizes j) =>
((assemble sizes A (List.finRange m) #[]).work : ℝ)) ≤
(m : ℝ) + 12 * ∑ j : Fin m, (sizes j : ℝ) ^ 2 := by
classical
haveI (j : Fin m) : Nonempty (Fin t → Hash (sizes j)) := ⟨fun _ => fallback (sizes j)⟩
have hex : (fun A : (j : Fin m) → Fin t → Hash (sizes j) =>
((assemble sizes A (List.finRange m) #[]).work : ℝ)) =
(fun A => (m : ℝ) + ∑ j : Fin m, ((buildTrace (A j)).work : ℝ)) := by
funext A
rw [assemble_work]
simp [← List.ofFn_id, List.map_ofFn, List.sum_ofFn]
rw [hex, fintypeExpect_add, fintypeExpect_const Fintype.card_ne_zero,
fintypeExpect_sum]
have hmarginal (j : Fin m) :
fintypeExpect (fun A : (j : Fin m) → Fin t → Hash (sizes j) =>
((buildTrace (A j)).work : ℝ)) ≤ 12 * (sizes j : ℝ) ^ 2 := by
rw [expect_pi_apply (fun j : Fin m => Fin t → Hash (sizes j)) j
(fun B : Fin t → Hash (sizes j) => ((buildTrace B).work : ℝ))]
exact expected_buildTrace_work_le_all (sizes j) t
calc
(m : ℝ) + ∑ j : Fin m,
fintypeExpect (fun A : (j : Fin m) → Fin t → Hash (sizes j) =>
((buildTrace (A j)).work : ℝ)) ≤
(m : ℝ) + ∑ j : Fin m, 12 * (sizes j : ℝ) ^ 2 := by
gcongr with j
exact hmarginal j
_ = _ := by rw [Finset.mul_sum]Count a bucket's length by traversing its elements once.
def measureLength : List α → Nat × Nat
| [] => (0, 0)
| _ :: xs => let rest := measureLength xs; (rest.1 + 1, rest.2 + 1)@[simp] theorem measureLength_result (xs : List α) : (measureLength xs).1 = xs.length := by
induction xs <;> simp_all [measureLength]@[simp] theorem measureLength_work (xs : List α) : (measureLength xs).2 = xs.length := by
induction xs <;> simp_all [measureLength]Cache bucket cardinalities once, counting both visits and cache writes.
def measureBuckets : List (List α) → Array Nat → Array Nat × Nat
| [], out => (out, 0)
| b :: bs, out =>
let measured := measureLength b
let rest := measureBuckets bs (out.push measured.1)
(rest.1, measured.2 + 1 + rest.2)theorem measureBuckets_result (bs : List (List α)) (out : Array Nat) :
(measureBuckets bs out).1.toList = out.toList ++ bs.map List.length := by
induction bs generalizing out with
| nil => simp [measureBuckets]
| cons b bs ih => simp [measureBuckets, ih, List.append_assoc]theorem measureBuckets_work (bs : List (List α)) (out : Array Nat) :
(measureBuckets bs out).2 = bs.length + bs.flatten.length := by
induction bs generalizing out with
| nil => simp [measureBuckets]
| cons b bs ih => simp [measureBuckets, ih]; omegastructure Primary (n : Nat) where
buckets : Array (List (Fin n))
sizes : Array Nat
work : NatOne primary distribution and one bucket-cardinality traversal. Subsequent secondary attempts read cached sizes instead of recomputing the partition.
def preparePrimary {n : Nat} (a : Fin n → Fin n) : Primary n :=
let initial := Chapter08.CountingExecution.initializeBuckets (α := Fin n) n
let distributed := Chapter08.CountingExecution.distribute
(fun i => (a i).val) (List.finRange n) initial.1
let measured := measureBuckets distributed.buckets.toList #[]
⟨distributed.buckets, measured.1,
initial.2 + 2 * distributed.inputs + 3 * distributed.updates + measured.2⟩@[simp] theorem preparePrimary_bucket_size {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).buckets.size = n := by
simp [preparePrimary]theorem preparePrimary_buckets {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).buckets.toList =
(List.range n).map (Chapter08.bucket (fun i => (a i).val) (List.finRange n)) := by
simp [preparePrimary, Chapter08.CountingExecution.distribute_toList]theorem preparePrimary_sizes {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).sizes.toList =
(preparePrimary a).buckets.toList.map List.length := by
simp [preparePrimary, measureBuckets_result]
@[simp] theorem preparePrimary_sizes_size {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).sizes.size = n := by
rw [← Array.length_toList, preparePrimary_sizes]
simp
theorem preparePrimary_total_length {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).buckets.toList.flatten.length = n := by
rw [preparePrimary_buckets]
cases n with
| zero => simp
| succ n =>
change (Chapter08.countingSortBy n (fun i => (a i).val) (List.finRange (n + 1))).length = n + 1
have hp := Chapter08.countingSortBy_perm n (fun i => (a i).val) (List.finRange (n + 1))
(by intro i hi; change (a i).val ≤ n; have h := (a i).isLt; omega)
simpa using hp.length_eq
theorem preparePrimary_work {n : Nat} (a : Fin n → Fin n) :
(preparePrimary a).work = 8 * n := by
have hu : (Chapter08.CountingExecution.distribute
(fun i : Fin n => (a i).val) (List.finRange n) (Array.replicate n [])).updates = n := by
simpa using Chapter08.CountingExecution.distribute_updates_eq
(fun i : Fin n => (a i).val) (List.finRange n) (Array.replicate n [])
(by intro i hi; simp)
have hl := preparePrimary_total_length a
unfold preparePrimary at hl ⊢
simp only [Chapter08.CountingExecution.initializeBuckets_visits,
Chapter08.CountingExecution.distribute_inputs, hu, List.length_finRange,
measureBuckets_work, Array.length_toList, Chapter08.CountingExecution.distribute_size,
Chapter08.CountingExecution.initializeBuckets_array, Array.size_replicate] at hl ⊢
rw [hl]
omegaA stored cached cardinality, not a repeated list-length computation.
def bucketCard {n : Nat} (a : Fin n → Fin n) (j : Fin n) : Nat :=
(preparePrimary a).sizes[j.val]'(by simp)theorem bucketCard_eq {n : Nat} (a : Fin n → Fin n) (j : Fin n) :
bucketCard a j = (Chapter08.bucket (fun i => (a i).val) (List.finRange n) j.val).length := by
unfold bucketCard
simp only [← Array.getElem_toList, preparePrimary_sizes, preparePrimary_buckets,
List.getElem_map, List.getElem_range]
theorem bucketCard_cast {n : Nat} (a : Fin n → Fin n) (j : Fin n) :
(bucketCard a j : ℝ) = bucketSize a j := by
rw [bucketCard_eq, Chapter08.bucket_length_eq_card]
exact (Chapter08.bucketOccupancy_eq_card a j).symm
theorem bucketCard_sq_sum {n : Nat} (a : Fin n → Fin n) :
(∑ j : Fin n, (bucketCard a j : ℝ) ^ 2) = totalSecondarySpace a := by
unfold totalSecondarySpace
exact Finset.sum_congr rfl (fun j _ => by rw [bucketCard_cast])structure TwoLevel {n : Nat} (a : Fin n → Fin n) where
primary : Primary n
secondary : Assembly (bucketCard a)
work : NatConstruct every actual secondary array from finite supplied trial traces, retaining the primary payload buckets and all completed secondary arrays.
def buildTwoLevel {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j)) : TwoLevel a :=
let primary := preparePrimary a
let secondary := assemble (fun j => primary.sizes[j.val]'(by simp [primary])) A (List.finRange n) #[]
⟨primary, secondary, primary.work + secondary.work⟩All secondary tables returned by the complete constructor succeeded.
theorem buildTwoLevel_success {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j)) :
∀ entry ∈ (buildTwoLevel a A).secondary.buckets.toList,
entry.2.selected.success = true :=
assemble_success (bucketCard a) AConditional expected measured work is bounded by the pre-existing analytic budget up to an explicit operation-count constant.
theorem expected_buildTwoLevel_work_le_budget {n : Nat} (a : Fin n → Fin n) (t : Nat) :
fintypeExpect (fun A : (j : Fin n) → Fin t → Hash (bucketCard a j) =>
((buildTwoLevel a A).work : ℝ)) ≤ 9 * constructionCost a := by
classical
haveI (j : Fin n) : Nonempty (Fin t → Hash (bucketCard a j)) :=
⟨fun _ => fallback (bucketCard a j)⟩
have he := expected_assemble_work_le (bucketCard a) t
rw [bucketCard_sq_sum] at he
have hex : (fun A : (j : Fin n) → Fin t → Hash (bucketCard a j) =>
((buildTwoLevel a A).work : ℝ)) =
(fun A => (8 * n : ℝ) + ((assemble (bucketCard a) A (List.finRange n) #[]).work : ℝ)) := by
funext A
change (((preparePrimary a).work + (assemble (bucketCard a) A (List.finRange n) #[]).work : Nat) : ℝ) = _
rw [preparePrimary_work]
push_cast
rfl
rw [hex, fintypeExpect_add, fintypeExpect_const Fintype.card_ne_zero]
have hs : 0 ≤ totalSecondarySpace a := by
unfold totalSecondarySpace
exact Finset.sum_nonneg (fun j _ => sq_nonneg _)
unfold constructionCost
nlinarithAverage actual finite-with-fallback construction work is linear under the primary SUHA assignment and the conditional independent secondary trace spaces.
theorem expected_buildTwoLevel_work_lt {n : Nat} (hn : 0 < n) (t : Nat) :
fintypeExpect (fun a : Fin n → Fin n =>
fintypeExpect (fun A : (j : Fin n) → Fin t → Hash (bucketCard a j) =>
((buildTwoLevel a A).work : ℝ))) < 45 * (n : ℝ) := by
classical
calc
fintypeExpect (fun a : Fin n → Fin n =>
fintypeExpect (fun A : (j : Fin n) → Fin t → Hash (bucketCard a j) =>
((buildTwoLevel a A).work : ℝ))) ≤
fintypeExpect (fun a : Fin n → Fin n => 9 * constructionCost a) := by
apply fintypeExpect_mono
intro a
exact expected_buildTwoLevel_work_le_budget a t
_ = 9 * fintypeExpect (fun a : Fin n → Fin n => constructionCost a) :=
fintypeExpect_const_mul 9 _
_ < 9 * (5 * (n : ℝ)) :=
mul_lt_mul_of_pos_left (perfectHash_expected_construction_time_le_const_n hn) (by norm_num)
_ = _ := by ringEach returned secondary array stores every local key index at the slot selected by the hash returned by that same execution.
theorem buildTwoLevel_stores {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j)) :
∀ entry ∈ (buildTwoLevel a A).secondary.buckets.toList,
∀ i : Fin (bucketCard a entry.1),
entry.2.selected.slots[(entry.2.selected.hash i).val]?.join = some i := by
change ∀ entry ∈ (assemble (bucketCard a) A (List.finRange n) #[]).buckets.toList, _
rw [assemble_result]
simp only [List.nil_append, List.mem_map]
rintro entry ⟨j, hj, rfl⟩ i
simp only [buildTrace_eq]
change (build (List.ofFn (A j))).selected.slots[
((build (List.ofFn (A j))).selected.hash i).val]?.join = some i
rw [build_selected_eq]
simp only [attempt_hash]
rw [Array.getElem?_eq_getElem (by simp), Option.join_some]
exact attempt_stores _ (build_hash_injective (List.ofFn (A j))) i
theorem preparePrimary_bucket_length {n : Nat} (a : Fin n → Fin n) (j : Fin n) :
((preparePrimary a).buckets[j.val]'(by simp)).length = bucketCard a j := by
rw [bucketCard_eq]
simp only [← Array.getElem_toList, preparePrimary_buckets,
List.getElem_map, List.getElem_range]Resolve a supplied local index through its selected secondary slot to the original payload. Supplying the local index is an explicit interface requirement.
def recoverPayload {n : Nat} {a : Fin n → Fin n} (built : TwoLevel a)
(entry : Σ j : Fin n, Build (bucketCard a j)) (i : Fin (bucketCard a entry.1)) :
Option (Fin n) := do
let localIndex ← entry.2.selected.slots[(entry.2.selected.hash i).val]?.join
let payloads ← built.primary.buckets[entry.1.val]?
payloads[localIndex.val]?
theorem buildTwoLevel_recovers {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j))
(entry : Σ j : Fin n, Build (bucketCard a j))
(he : entry ∈ (buildTwoLevel a A).secondary.buckets.toList)
(i : Fin (bucketCard a entry.1)) :
recoverPayload (buildTwoLevel a A) entry i =
some (((preparePrimary a).buckets[entry.1.val]'(by simp))[i.val]'(by
rw [preparePrimary_bucket_length]; exact i.isLt)) := by
unfold recoverPayload
rw [buildTwoLevel_stores a A entry he i]
change ((preparePrimary a).buckets[entry.1.val]?.bind fun xs => xs[i.val]?) = _
rw [Array.getElem?_eq_getElem (by simp), Option.bind_some,
List.getElem?_eq_getElem (by rw [preparePrimary_bucket_length]; exact i.isLt)]
theorem preparePrimary_covers {n : Nat} (a : Fin n → Fin n) (x : Fin n) :
∃ i : Fin (bucketCard a (a x)),
((preparePrimary a).buckets[(a x).val]'(by simp))[i.val]'(by
rw [preparePrimary_bucket_length]; exact i.isLt) = x := by
have hm : x ∈ (preparePrimary a).buckets[(a x).val]'(by simp) := by
simp only [← Array.getElem_toList, preparePrimary_buckets,
List.getElem_map, List.getElem_range]
simp [CLRS.Chapter08.bucket]
obtain ⟨i, hi, hx⟩ := List.getElem_of_mem hm
exact ⟨⟨i, by rwa [preparePrimary_bucket_length] at hi⟩, hx⟩Every original stored payload can be recovered through its actual returned secondary table, given its local index. This does not construct an inverse map from arbitrary original query keys to local indices.
theorem buildTwoLevel_recovers_original {n t : Nat} (a : Fin n → Fin n)
(A : (j : Fin n) → Fin t → Hash (bucketCard a j)) (x : Fin n) :
∃ i : Fin (bucketCard a (a x)),
recoverPayload (buildTwoLevel a A) ⟨a x, buildTrace (A (a x))⟩ i = some x := by
obtain ⟨i, hi⟩ := preparePrimary_covers a x
refine ⟨i, ?_⟩
rw [buildTwoLevel_recovers, hi]
change (⟨a x, buildTrace (A (a x))⟩ : Σ j : Fin n, Build (bucketCard a j)) ∈
(assemble (bucketCard a) A (List.finRange n) #[]).buckets.toList
rw [assemble_result]
simp only [List.nil_append, List.mem_map]
exact ⟨a x, List.mem_finRange _, rfl⟩end CLRS.Chapter11.PerfectConstructionScope and implementation notes
Imports
import CLRSLean.FourthEdition.Chapter_11.Section_11_5_Perfect_Hashing.Construction
import CLRSLean.FourthEdition.Chapter_11.Section_11_1_Direct_Address_Tables
import CLRSLean.FourthEdition.Chapter_11.Section_11_2_Chained_Hash_Tables
import CLRSLean.FourthEdition.Chapter_11.Section_11_3_Hash_Functions
import CLRSLean.FourthEdition.Chapter_11.Section_11_4_Open_Addressing
import CLRSLean.FourthEdition.Chapter_11.Section_11_4_Open_Addressing.UniformProbe
import CLRSLean.FourthEdition.Chapter_11.Section_11_5_Perfect_HashingNative fourth-edition chapter guide.
Current source
This guide sources fourth-edition §11.1–§11.5 from the native section modules
under CLRSLean.FourthEdition.Chapter_11. Declarations retain the
CLRS.Chapter11 namespace; the legacy import CLRSLean.Chapter_11
and its Section_11_* modules forward to these sources during the
compatibility period.
Chapter 11 introduces direct-address tables and hash tables. The current
CLRS-Lean pass separates deterministic table correctness from probabilistic
performance analysis. Section 11.2 now includes a finite-uniform bucket
interface: when the searched bucket is uniformly distributed, expected chain
length is exactly the load factor, and one insertion increases load factor and
unsuccessful-search cost by 1/m.
Sections
-
11.1 Direct-address tables:
provedfor the functional table model. Main results:CLRS.Chapter11.search_insert_same,CLRS.Chapter11.search_insert_other,CLRS.Chapter11.search_delete_same. -
11.2 Hash tables:
provedfor the advertised functional and finite probability models. Main results:CLRS.Chapter11.bucket_hashInsert_same,CLRS.Chapter11.hashSearch_hashInsert_self,CLRS.Chapter11.hashSearch_hashInsert_iff,CLRS.Chapter11.hashSearch_hashDelete_self,CLRS.Chapter11.hashSearch_hashDelete_iff,CLRS.Chapter11.uniformAverageFin_indicator_singleton,CLRS.Chapter11.uniformAverageFin_add,CLRS.Chapter11.uniformAverageFin_nonneg,CLRS.Chapter11.finiteHashLoadFactor_nonneg,CLRS.Chapter11.expectedSearchChainLength_eq_loadFactor,CLRS.Chapter11.expectedSearchChainLength_nonneg,CLRS.Chapter11.expectedUnsuccessfulSearchCost_eq_one_plus_loadFactor,CLRS.Chapter11.expectedUnsuccessfulSearchCost_ge_one,CLRS.Chapter11.expectedSearchChainLength_finiteHashInsert,CLRS.Chapter11.finiteHashLoadFactor_finiteHashInsert,CLRS.Chapter11.expectedUnsuccessfulSearchCost_finiteHashInsert,CLRS.Chapter11.expectedRandomChainLength_eq_loadFactor,CLRS.Chapter11.expectedRandomUnsuccessfulSearchCost,CLRS.Chapter11.pairCollisionProb,CLRS.Chapter11.expectedRandomSuccessfulSearchCost,CLRS.Chapter11.universal_expected_collisions, andCLRS.Chapter11.universal_expected_search_cost. -
11.3 Hash functions:
proved. Main results:CLRS.Chapter11.divisionHash_lt,CLRS.Chapter11.multiplicationHash_lt,CLRS.Chapter11.affineHash_isUniversal,CLRS.Chapter11.affineHash_expected_collisions,CLRS.Chapter11.affineHash_expected_search_cost, andCLRS.Chapter11.affineHashMod_isUniversal(CLRS Theorem 11.5, the general mod-maffine family). -
11.4 Open addressing:
proved. Main results:CLRS.Chapter11.openSearch_openInsert,CLRS.Chapter11.openSearch_eq_false_of_absent,CLRS.Chapter11.linearProbe_bijective,CLRS.Chapter11.doubleHashProbe_bijective,CLRS.Chapter11.firstProbesOccupied_card,CLRS.Chapter11.uniformProbeTailProbability_eq_probeTail,CLRS.Chapter11.uniformUnsuccessfulExpectedProbes_eq,CLRS.Chapter11.uniformUnsuccessfulExpectedProbes_le,CLRS.Chapter11.expectedUnsuccessfulProbes_le,CLRS.Chapter11.expectedSuccessfulProbes_le, andCLRS.Chapter11.expectedSuccessfulProbes_le_ln(CLRS Theorem 11.8, logarithmic form). -
11.5 Perfect hashing:
proved. Main results:CLRS.Chapter11.perfectSearch_iff_mem,CLRS.Chapter11.perfectHash_collision_free_prob_ge_half,CLRS.Chapter11.perfectHash_expected_total_space_lt_2n, and thePerfectConstructioncompanion's successful finite-trial array construction and expected executed-work bounds.
Current Gaps
The deterministic insert/delete/search layer is compiler-clean, and the
finite-uniform bucket layer proves the load-factor, nonnegativity, and
single-insert expected-cost interfaces. The SUHA layer proves the expected
chain length, unsuccessful-search cost 1 + α, and successful-search cost
1 + (n-1)/(2m) as true expectations, and a universal random-hash-function
family bounds expected collisions by α and search cost by 1 + α.
Section 11.3 supplies a concrete universal family (the prime-field affine family
h_{a,b}(k) = a * k + b) that discharges the IsUniversal hypothesis,
so the universal-hashing bounds are no longer conditional. Section 11.4
formalises the open addressing model with probe sequences (linear, quadratic,
double hashing). Its uniform-hashing assumption is now realized by the finite
sample space Equiv.Perm (Fin m): every injective prefix has exactly
(m-i)! full-permutation extensions, the occupied-prefix probability is
proved equal to the without-replacement product probeTail, and the
expected concrete probe count is proved equal to the chapter's tail sum. This
gives explicit-sample-space versions of the expected-probe bounds:
unsuccessful search and insertion ≤ 1/(1-α) (CLRS Theorems 11.6-11.7)
and successful search (1/α) · ∑_{j<n} 1/(m-j) (CLRS Theorem 11.8 harmonic
form), refined to the closed form (1/α) · ln(1/(1-α))
(CLRS.Chapter11.uniformSuccessfulExpectedProbes_le_ln). Section 11.5
formalises the two-level perfect-hashing scheme: a primary
hash into m = n buckets plus per-bucket secondary tables of size n_j², which
are collision-free with probability ≥ 1/2 (Theorem 11.9) and collectively use
expected O(n) space (Theorem 11.10). Secondary injectivity is required only
for stored keys in their primary bucket, allowing unstored keys to collide.
The PerfectConstruction companion builds primary payload buckets and
secondary arrays, trying supplied random local-index hashes before a guaranteed
injective fallback. Actual attempts agree with the finite trial-count model.
Counted collision checks, initialization, placement, and assembly have conditional
expectation at most 9 * constructionCost, and nested uniform expectation below
45n for positive n. The older theorem named
perfectHash_expected_construction_time_le_const_n bounds the abstract
budget itself by 5n; it is not the executed counter.
This SUHA construction samples functions on local bucket indices. Conversion from arbitrary original-key queries to constant-time hash programs, hash-code generation, persistent allocation, and machine arithmetic are not established by these execution bounds.
See docs/clrs-fourth-edition-map.csv for the section-level mapping and
docs/migrations/clrs4.md for compatibility and deprecation policy.
CLRS, fourth edition · Chapter 11 of 35