Hashing
Subject: Coding · Chapter: Hashing Time limit: 45 minutes · Total marks: 60
Instructions: Write code from memory where asked. Explain reasoning explicitly. Use for math.
Question 1 — Hash Table with Chaining (from scratch) [12 marks]
Implement a hash table using separate chaining (Python, no dict/set). Your class HashMap must support put(key, value), get(key), and delete(key), and must resize (double the number of buckets and rehash) when the load factor exceeds 0.75.
(a) Write the complete class. [9] (b) State the load factor formula and explain why resizing keeps operations amortized . [3]
Question 2 — Open Addressing & Tombstones [11 marks]
An open-addressing table of size uses linear probing with . Keys are inserted in order: .
(a) Show the final array contents (index → key). [4]
(b) Now delete(17). Explain why you cannot simply blank the slot, and show the array using a tombstone marker. [3]
(c) After the deletion, trace a search(24). Show every probe and explain why the tombstone must not stop the search. [4]
Question 3 — Probing strategies & clustering [9 marks]
(a) Define linear probing, quadratic probing, and double hashing with their probe-sequence formulas. [4] (b) Explain primary clustering and which strategy suffers from it. [2] (c) For double hashing on table size with and , list the first three probe indices for key . [3]
Question 4 — Load factor, resizing cost, amortization [10 marks]
(a) A chaining table resizes by doubling. Starting from capacity 1, we insert keys. Show that the total cost of all rehashes is , hence amortized per insert. [5] (b) Under uniform hashing with load factor , give the expected number of probes for an unsuccessful search in (i) chaining and (ii) open addressing. [4] (c) State one reason to keep well below 1 in open addressing. [1]
Question 5 — Universal hashing [8 marks]
Let be a family of hash functions mapping a universe to .
(a) Give the formal definition of a universal family. [3] (b) For a randomly chosen and two distinct keys , what is the bound on collision probability, and why does this give a probabilistic guarantee independent of adversarial input? [3] (c) With keys in a table of size using a universal family, give the expected number of keys colliding with a fixed key . [2]
Question 6 — Application: Two-Sum with a hash set [10 marks]
(a) Write a function two_sum(nums, target) returning indices (i, j) with nums[i] + nums[j] == target, using a hash map in a single pass. [5]
(b) State its time and space complexity and justify. [2]
(c) Explain why a Python dict gives average lookups here, referencing its internal open-addressing implementation and what happens on a collision. [3]
Answer keyMark scheme & solutions
Question 1 [12]
(a) [9] — 3 for structure/hash+resize trigger, 3 for put/get, 3 for delete + rehash correctness.
class HashMap:
def __init__(self, cap=8):
self.cap = cap
self.size = 0
self.buckets = [[] for _ in range(cap)]
def _idx(self, key):
return hash(key) % self.cap # deterministic, fast
def put(self, key, value):
b = self.buckets[self._idx(key)]
for i, (k, v) in enumerate(b):
if k == key:
b[i] = (key, value) # update existing
return
b.append((key, value))
self.size += 1
if self.size / self.cap > 0.75: # load factor check
self._resize()
def get(self, key):
for k, v in self.buckets[self._idx(key)]:
if k == key:
return v
raise KeyError(key)
def delete(self, key):
b = self.buckets[self._idx(key)]
for i, (k, v) in enumerate(b):
if k == key:
b.pop(i)
self.size -= 1
return
raise KeyError(key)
def _resize(self):
old = self.buckets
self.cap *= 2
self.buckets = [[] for _ in range(self.cap)]
self.size = 0
for b in old:
for k, v in b:
self.put(k, v) # rehash into larger table(b) [3]
- Load factor (entries / buckets). [1]
- With chaining, expected chain length is ; keeping keeps chains short so each op is expected. [1]
- Resizing is but happens rarely (only after doublings), so its cost amortizes to per insert. [1]
Question 2 [11]
(a) [4] :
- → slot 3
- → occupied, probe 4 → slot 4
- → 3,4 occupied, probe 5 → slot 5
- → 3,4,5 occupied, probe 6 → slot 6
| idx | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| key | – | – | – | 10 | 17 | 24 | 3 |
(1 mark per correctly placed key.)
(b) [3] Blanking slot 4 would break the probe chain: a later search for 24 (which probed 3→4→5) would hit the empty slot 4 and stop early, wrongly concluding 24 is absent. [2] Instead mark slot 4 as a tombstone (DELETED): [1]
| idx | 3 | 4 | 5 | 6 |
|---|---|---|---|---|
| key | 10 | 🪦 | 24 | 3 |
(c) [4] search(24): .
- Probe 3: key 10 ≠ 24, continue. [1]
- Probe 4: tombstone → do not stop, keep probing (a tombstone means "was occupied, may have kept a later key on the chain"). [1+1]
- Probe 5: key 24 → found. [1]
Question 3 [9]
(a) [4] Probe sequence for slot :
- Linear: . [1]
- Quadratic: . [1.5]
- Double hashing: . [1.5]
(b) [2] Primary clustering: contiguous runs of occupied slots grow and merge, so long runs get longer, degrading probes. Linear probing suffers from it. [2]
(c) [3] ; .
- :
- :
- :
Probe indices: 1, 7, 0. [1 each]
Question 4 [10]
(a) [5] Doubling from capacity 1: rehashes occur at sizes . Cost of rehash at size is . Total: [3] Dividing by inserts gives amortized per insert. [2]
(b) [4]
- (i) Chaining: expected probes (must scan the whole chain). [2]
- (ii) Open addressing (uniform): . [2]
(c) [1] As in open addressing, blows up (probes explode / table fills); keeping low keeps probes bounded. [1]
Question 5 [8]
(a) [3] is universal if for all distinct :
(b) [3] Collision probability regardless of which keys the adversary picks, because is chosen randomly after the keys are fixed. [2] So expected chain length / probe count stays in expectation over the choice of — no fixed input can force worst-case behaviour. [1]
(c) [2] Expected colliders with fixed among the other keys:
Question 6 [10]
(a) [5]
def two_sum(nums, target):
seen = {} # value -> index
for j, x in enumerate(nums):
need = target - x
if need in seen: # O(1) average lookup
return (seen[need], j)
seen[x] = j
return None(2 for single-pass structure, 2 for complement lookup, 1 for correct return.)
(b) [2] Time : one pass, each dict op average. Space : up to entries stored. [2]
(c) [3] CPython's dict is an open-addressing hash table. The key's hash selects a slot; [1] on a collision it uses a perturbation-based probe sequence to find the next slot; [1] since is kept below ~2/3 (it resizes/rehashes otherwise), probe chains stay short, giving average lookup. [1]
[
{"claim":"Q2a: linear probing places 10,17,24,3 at slots 3,4,5,6",
"code":"m=7; slots=[None]*m\nfor k in [10,17,24,3]:\n i=k%m\n while slots[i] is not None: i=(i+1)%m\n slots[i]=k\nresult = slots==[None,None,None,10,17,24,3]"},
{"claim":"Q3c: double hashing probes for k=27 are 1,7,0",
"code":"m=13; h1=27%13; h2=1+(27%11)\nprobes=[(h1+i*h2)%m for i in range(3)]\nresult = probes==[1,7,0]"},
{"claim":"Q4a: geometric doubling total cost is 2n-1 for n a power of 2 (n=8)",
"code":"n=8; total=sum(2**j for j in range(0, n.bit_length()))\nresult = total==2*n-1"},
{"claim":"Q4b: open-addressing unsuccessful search probes 1/(1-alpha) at alpha=0.75 equals 4",
"code":"from sympy import Rational\nalpha=Rational(3,4)\nresult = (1/(1-alpha))==4"}
]