Level 5 — MasteryHashing

Hashing

75 minutes60 marksprintable — key stays hidden on paper

Chapter: 3.3 Hashing Level: 5 — Mastery (cross-domain: probability + algorithm analysis + coding) Time limit: 75 minutes Total marks: 60

Instructions: Answer all three questions. Show reasoning, derivations, and complexity arguments. Code may be written in Python-like pseudocode or valid Python. Use ...... for inline math.


Question 1 — Probabilistic Analysis of Chaining & Universal Hashing (20 marks)

A hash table with chaining has mm buckets and stores nn keys. Assume a family H\mathcal{H} of hash functions that is universal, i.e. for any distinct keys xyx \neq y, PrhH[h(x)=h(y)]1m.\Pr_{h \in \mathcal{H}}[h(x) = h(y)] \le \frac{1}{m}.

(a) Define load factor α\alpha and state, with a one-line justification, the expected number of keys per bucket. (3)

(b) Fix a key xx already in the table. Let CxC_x be the number of other keys that collide with xx (land in the same bucket). Prove that E[Cx]n1m.\mathbb{E}[C_x] \le \frac{n-1}{m}. Use indicator random variables and linearity of expectation. (6)

(c) Hence show the expected time for an unsuccessful search is O(1+α)O(1 + \alpha) and explain why the "+1+1" term cannot be dropped. (4)

(d) Consider the concrete universal family over a prime p>up > u (universe size uu): ha,b(k)=((ak+b)modp)modm,a{1,,p1}, b{0,,p1}.h_{a,b}(k) = \big((a k + b) \bmod p\big) \bmod m, \quad a \in \{1,\dots,p-1\},\ b \in \{0,\dots,p-1\}. For p=17p = 17, m=6m = 6, a=3a = 3, b=5b = 5, compute ha,b(k)h_{a,b}(k) for keys k=4k = 4 and k=21k = 21. Do they collide? (4)

(e) State in one sentence why a deterministic fixed hash function cannot give the same worst-case guarantee that universal hashing gives. (3)


Question 2 — Open Addressing, Tombstones & Amortized Resizing (22 marks)

A table uses open addressing with table size m=11m = 11 and the hash function h(k)=kmod11h(k) = k \bmod 11.

(a) Insert the keys 22,1,13,24,1222, 1, 13, 24, 12 in this order using linear probing (probe(k,i)=(h(k)+i)mod11\text{probe}(k,i) = (h(k)+i)\bmod 11). Show the final array, listing index → key. (5)

(b) Now insert the same keys using double hashing with h2(k)=7(kmod7)h_2(k) = 7 - (k \bmod 7), probe(k,i)=(h(k)+ih2(k))mod11\text{probe}(k,i) = (h(k) + i\,h_2(k)) \bmod 11. Show the final array. (5)

(c) Explain precisely why, when deleting from an open-addressing table, we must place a tombstone rather than emptying the slot. Give a small concrete failure scenario (using part (a)'s table) that shows what breaks if we simply empty a slot. (4)

(d) Amortized cost proof. A dynamic table starts empty and doubles its capacity whenever load factor reaches 11 (each resize rehashes all current elements at cost equal to the number of elements moved). Using the aggregate method, prove that the total cost of inserting nn items is O(n)O(n), hence amortized O(1)O(1) per insertion. Include the geometric-series bound explicitly. (6)

(e) For linear probing with load factor α\alpha, the expected number of probes for an unsuccessful search is approximately 12(1+1(1α)2)\tfrac{1}{2}\left(1 + \tfrac{1}{(1-\alpha)^2}\right). Evaluate this for α=0.9\alpha = 0.9 and comment on what it implies about resize thresholds. (2)


Question 3 — Build & Apply: LRU Cache and Two-Sum (18 marks)

(a) The two-sum problem: given an array AA and target TT, return indices i<ji<j with A[i]+A[j]=TA[i]+A[j]=T. Write a single-pass algorithm using a hash map and state its time and space complexity, justifying the O(n)O(n) time via amortized hash operations. (6)

(b) Trace your algorithm on A=[3,8,2,5,11]A = [3, 8, 2, 5, 11], T=10T = 10. Show the map contents step by step and the returned indices. (4)

(c) Build an LRU cache supporting get(key) and put(key, value) in O(1)O(1) average time with capacity CC. Describe the data structures used (hash map + doubly linked list), give pseudocode for both operations, and explain how each achieves O(1)O(1). (6)

(d) Explain why a Python dict (which preserves insertion order) alone is not sufficient to get true O(1)O(1) LRU behaviour without extra care, and what operation would be O(n)O(n) if done naively. (2)

Answer keyMark scheme & solutions

Question 1

(a) [3]

  • Load factor α=n/m\alpha = n/m. (1) — definition.
  • Under simple uniform hashing each key is equally likely in any bucket, so expected keys per bucket =n1m=α= n \cdot \tfrac{1}{m} = \alpha. (2) — justification by symmetry/linearity.

(b) [6] For each other key yxy \neq x define indicator Xy=1X_y = 1 if h(y)=h(x)h(y)=h(x), else 00. (2) By universality, E[Xy]=Pr[h(y)=h(x)]1/m\mathbb{E}[X_y] = \Pr[h(y)=h(x)] \le 1/m. (2) Cx=yxXyC_x = \sum_{y\neq x} X_y, so by linearity of expectation: E[Cx]=yxE[Xy](n1)1m=n1m.(2)\mathbb{E}[C_x] = \sum_{y \neq x}\mathbb{E}[X_y] \le (n-1)\cdot\frac1m = \frac{n-1}{m}. \quad\text{(2)}

(c) [4]

  • Unsuccessful search hashes xx (cost O(1)O(1)) then scans the whole chain of its bucket. (1)
  • Expected chain length α\le \alpha (or α\alpha for a probe key), so scan cost O(α)O(\alpha); total O(1+α)O(1+\alpha). (2)
  • The "+1+1" accounts for computing the hash and accessing the bucket even when the chain is empty (α0\alpha \to 0); dropping it would wrongly imply zero cost when α=0\alpha=0. (1)

(d) [4]

  • k=4k=4: 34+5=173\cdot4+5 = 17; 17mod17=017 \bmod 17 = 0; 0mod6=00 \bmod 6 = 0. (1.5)
  • k=21k=21: 321+5=683\cdot21+5 = 68; 68mod17=68417=068 \bmod 17 = 68 - 4\cdot17 = 0; 0mod6=00 \bmod 6 = 0. (1.5)
  • Both map to bucket 00they collide. (1)

(e) [3]

  • For any fixed deterministic hash function, an adversary who knows the function can choose nn keys that all hash to the same bucket, forcing Θ(n)\Theta(n) worst-case per operation; randomizing the choice of hh from a universal family means no fixed input is bad in expectation, giving the probabilistic guarantee. (3)

Question 2

(a) Linear probing, m=11m=11, h(k)=kmod11h(k)=k\bmod11. [5]

key h probes slot
22 0 0
1 1 1
13 2 2
24 2 2 taken, 3 free 3
12 1 1,2,3 taken → 4 free 4

Final: index0→22, 1→1, 2→13, 3→24, 4→12; others empty. (5) (deduct 1 per misplaced key)

(b) Double hashing, h2(k)=7(kmod7)h_2(k)=7-(k\bmod7). [5]

  • 22: h=0h=0 → slot 0.
  • 1: h=1h=1 → slot 1.
  • 13: h=2h=2 → slot 2.
  • 24: h=2h=2 occupied. h2(24)=7(24mod7)=73=4h_2(24)=7-(24\bmod7)=7-3=4. probe i=1i=1: (2+4)mod11=6(2+4)\bmod11=6 → slot 6.
  • 12: h=1h=1 occupied. h2(12)=7(12mod7)=75=2h_2(12)=7-(12\bmod7)=7-5=2. probe i=1i=1: (1+2)mod11=3(1+2)\bmod11=3 → slot 3.

Final: 0→22, 1→1, 2→13, 3→12, 6→24. (5)

(c) [4]

  • In open addressing, a search stops when it hits an empty slot (that signals "key not present along this probe chain"). (1)
  • If we simply empty a deleted slot, keys that were placed after it during probing become unreachable — the search terminates early at the false empty slot. (2)
  • A tombstone marks "deleted but keep probing"; it is treated as occupied for search, free for insertion. (1)
  • Concrete failure (from (a)): delete key 13 (slot 2) by emptying. Searching for 24: h(24)=2h(24)=2 finds slot 2 empty → wrongly reports 24 absent, though 24 sits at slot 3. (counts toward the 3 marks above)

(d) Aggregate/amortized proof. [6]

  • Between resizes, each ordinary insertion costs O(1)O(1); over nn inserts these contribute O(n)O(n). (1)
  • Resizes occur when size hits 1,2,4,8,1,2,4,8,\dots; the jj-th resize copies 2j2^j elements. (2)
  • Total copy cost j=0log2n2j=2log2n+11<2n\le \sum_{j=0}^{\lfloor\log_2 n\rfloor} 2^j = 2^{\lfloor\log_2 n\rfloor+1}-1 < 2n (geometric series). (2)
  • Total cost =O(n)+O(n)=O(n)= O(n) + O(n) = O(n); dividing by nn gives amortized O(1)O(1) per insertion. (1)

(e) [2] 12(1+1(10.9)2)=12(1+10.01)=12(1+100)=50.5.\tfrac12\left(1 + \tfrac{1}{(1-0.9)^2}\right) = \tfrac12\left(1 + \tfrac{1}{0.01}\right) = \tfrac12(1+100) = 50.5. About 50 probes on average — catastrophic; hence linear-probing tables should resize at a much lower threshold (e.g. α0.7\alpha \le 0.7). (2)


Question 3

(a) [6]

def two_sum(A, T):
    seen = {}                     # value -> index
    for j, x in enumerate(A):
        if (T - x) in seen:       # O(1) avg lookup
            return (seen[T - x], j)
        seen[x] = j               # O(1) avg insert
    return None
  • Single pass, each element does one lookup + one insert, both amortized O(1)O(1) (hashing) → total O(n)O(n) time. (4)
  • Space O(n)O(n) for the map. (2)

(b) Trace on A=[3,8,2,5,11]A=[3,8,2,5,11], T=10T=10. [4]

j x T-x in seen? seen after
0 3 7 no {3:0}
1 8 2 no {3:0, 8:1}
2 2 8 yes (8→1) return (1,2)

Returned indices (1, 2) since A[1]+A[2]=8+2=10A[1]+A[2]=8+2=10. (4)

(c) [6] Structures: a hash map key → node and a doubly linked list ordered by recency (head = most recent, tail = least recent). (2)

get(key):
    if key not in map: return -1
    node = map[key]
    move_to_head(node)      # unlink + insert at head, O(1)
    return node.value

put(key, value):
    if key in map:
        node = map[key]; node.value = value; move_to_head(node)
    else:
        node = new Node(key, value)
        map[key] = node; add_to_head(node)
        if len(map) > C:
            lru = tail.prev
            remove(lru); del map[lru.key]   # evict LRU
  • Map gives O(1)O(1) average lookup; DLL gives O(1)O(1) unlink/insert/eviction because node pointers are known directly (no scan). (2 for pseudocode, 2 for the O(1)O(1) justification)

(d) [2] A plain dict preserves order, but to mark a key most-recently-used you must move it to the end. Doing that naively (delete then reinsert) is O(1)O(1) in CPython, but finding/removing the least-recently-used front element, or reordering an arbitrary middle element by rebuilding, would be O(n)O(n); the explicit DLL (or OrderedDict.move_to_end) is needed to guarantee O(1)O(1) eviction/repositioning. (2)


[
  {"claim":"h(4)=0 and h(21)=0 for a=3,b=5,p=17,m=6, so they collide",
   "code":"a,b,p,m=3,5,17,6\nh4=((a*4+b)%p)%m\nh21=((a*21+b)%p)%m\nresult=(h4==0 and h21==0 and h4==h21)"},
  {"claim":"Linear probing final placement: 22->0,1->1,13->2,24->3,12->4",
   "code":"m=11\ntab=[None]*m\nfor k in [22,1,13,24,12]:\n    i=0\n    while tab[(k%m+i)%m] is not None: i+=1\n    tab[(k%m+i)%m]=k\nresult=(tab[0]==22 and tab[1]==1 and tab[2]==13 and tab[3]==24 and tab[4]==12)"},
  {"claim":"Double hashing places 24 at slot 6 and 12 at slot 3",
   "code":"m=11\ntab=[None]*m\ndef h2(k): return 7-(k%7)\nfor k in [22,1,13,24,12]:\n    i=0\n    while tab[(k%m+i*h2(k))%m] is not None: i+=1\n    tab[(k%m+i*h2(k))%m]=k\nresult=(tab[6]==24 and tab[3]==12)"},
  {"claim":"Linear probing unsuccessful-search estimate at alpha=0.9 equals 50.5",
   "code":"al=Rational(9,10)\nval=Rational(1,2)*(1+1/(1-al)**2)\nresult=(val==Rational(101,2))"},
  {"claim":"Geometric sum of resize copies up to n=8 is < 2n",
   "code":"n=8\ntotal=sum(2**j for j in range(0,4))\nresult=(total==15 and total<2*n+1)"}
]