Skip to main content

Python Dictionary Hashing Mechanism: Practice Problems & Exercises

Practice: Dictionary Hashing Mechanism

12 problems4 Easy5 Medium3 Hard45-60 min
← Back to lesson

#1Hash Equality DetectiveEasy
hashequalitybuilt-in types

Given pairs of Python objects, determine for each pair whether they are equal, whether their hashes match, and whether the hashability contract holds. The contract states: if a == b then hash(a) == hash(b) must be true. The reverse is not required.

Python
def hash_equality_check(pairs):
    results = []
    for a, b in pairs:
        are_equal = (a == b)
        hashes_match = (hash(a) == hash(b))
        # Contract: if equal, hashes MUST match
        contract_holds = (not are_equal) or hashes_match
        results.append((are_equal, hashes_match, contract_holds))
    return results

# Test
test_pairs = [
    (1, 1.0),       # int and float with same value
    (1, 2),          # different ints
    (True, 1),       # bool and int
    (1.0, True),     # float and bool
    (0, 0.5),        # different values but let's check
]

for pair, result in zip(test_pairs, hash_equality_check(test_pairs)):
    print(result)
Solution

The key insight is that 1 == 1.0 == True in Python, and they all share the same hash value. The hashability contract is a one-way implication: equality implies equal hashes, but equal hashes do not imply equality (collisions are fine).

def hash_equality_check(pairs):
results = []
for a, b in pairs:
are_equal = (a == b)
hashes_match = (hash(a) == hash(b))
contract_holds = (not are_equal) or hashes_match
results.append((are_equal, hashes_match, contract_holds))
return results

For built-in types, Python always satisfies the contract. You only break it by writing a bad custom __hash__.

def hash_equality_check(pairs):
  """
  Given a list of (a, b) pairs, return a list of booleans.
  Each boolean is True if a and b satisfy the hashability
  contract: a == b implies hash(a) == hash(b).

  Also return whether a == b and whether hash(a) == hash(b)
  for each pair as a tuple: (are_equal, hashes_match, contract_holds)
  """
  # TODO: implement
  pass
Expected Output
(True, True, True)
(False, False, True)
(True, True, True)
(True, True, True)
(False, True, True)
Hints

Hint 1: The contract says: if a == b then hash(a) == hash(b) must hold.

Hint 2: The contract does NOT require: if hash(a) == hash(b) then a == b. Hash collisions are allowed.

Hint 3: Check each pair: compute are_equal = (a == b), hashes_match = (hash(a) == hash(b)), then contract_holds = (not are_equal) or hashes_match.


#2Dict Insertion Order TrackerEasy
insertion orderdict operationsPython 3.7+

Simulate a sequence of dict set/delete operations and return the final keys in insertion order. Remember that updating a value for an existing key does NOT move it to the end — only new keys are appended.

Python
def track_insertion_order(operations):
    d = {}
    for op in operations:
        if op[0] == "set":
            _, key, value = op
            d[key] = value
        elif op[0] == "del":
            _, key = op
            d.pop(key, None)  # ignore if missing
    return list(d.keys())

# Test 1
ops1 = [
    ("set", "a", 1),
    ("set", "b", 2),
    ("set", "c", 3),
    ("set", "a", 99),   # update — position unchanged
    ("del", "b"),        # remove b
    ("set", "d", 4),    # new key at end
]
print(track_insertion_order(ops1))

# Test 2
ops2 = [
    ("set", "x", 10),
    ("set", "y", 20),
    ("set", "z", 30),
    ("del", "y"),
    ("del", "w"),        # missing key — no error
]
print(track_insertion_order(ops2))
Solution

Python dicts since 3.7 guarantee insertion order. The key insight is that d[key] = value on an existing key updates the value in-place without changing the key's position. Deletion removes the key entirely, and re-insertion would place it at the end.

def track_insertion_order(operations):
d = {}
for op in operations:
if op[0] == "set":
_, key, value = op
d[key] = value
elif op[0] == "del":
_, key = op
d.pop(key, None)
return list(d.keys())
def track_insertion_order(operations):
  """
  Given a list of operations on a dict, return the final
  list of keys in insertion order.

  Operations are tuples:
    ("set", key, value)  - insert or update
    ("del", key)         - delete key (ignore if missing)

  Remember: updating an existing key does NOT change its
  insertion-order position.
  """
  # TODO: implement
  pass
Expected Output
['a', 'c', 'd']
['x', 'z']
Hints

Hint 1: Use a plain dict — Python 3.7+ guarantees insertion order.

Hint 2: When you update an existing key, its position in iteration order stays the same.

Hint 3: Deleting a key removes it from the order entirely. If re-inserted later, it goes to the end.


#3Hashable Key ValidatorEasy
hashabletype checkingdict keys

Determine which Python objects can be used as dictionary keys by testing whether they are hashable. An object is hashable if hash() succeeds without raising TypeError.

Python
def classify_hashability(objects):
    results = []
    for obj in objects:
        try:
            hash(obj)
            results.append((repr(obj), True))
        except TypeError:
            results.append((repr(obj), False))
    return results

test_objects = [
    42,
    "hello",
    (1, 2, 3),
    [1, 2, 3],
    frozenset({1, 2}),
    {1, 2},
    ((1, 2), (3, 4)),
    ([1, 2], [3, 4]),
    None,
]

for result in classify_hashability(test_objects):
    print(result)
Solution

The rule: immutable built-in types are hashable (int, float, str, tuple of hashables, frozenset, None, bool, bytes). Mutable containers (list, set, dict) are not. A tuple containing an unhashable element (like a list) is itself unhashable because tuple.__hash__ recursively hashes each element.

def classify_hashability(objects):
results = []
for obj in objects:
try:
hash(obj)
results.append((repr(obj), True))
except TypeError:
results.append((repr(obj), False))
return results
def classify_hashability(objects):
  """
  Given a list of Python objects, return a list of tuples:
    (object_repr, is_hashable)

  An object is hashable if calling hash() on it does NOT
  raise TypeError.
  """
  # TODO: implement
  pass
Expected Output
('42', True)
('hello', True)
('(1, 2, 3)', True)
('[1, 2, 3]', False)
('frozenset({1, 2})', True)
('{1, 2}', False)
('((1, 2), (3, 4))', True)
('([1, 2], [3, 4])', False)
('None', True)
Hints

Hint 1: Use a try/except block around hash(obj) to catch TypeError.

Hint 2: Lists, sets, and dicts are unhashable. Tuples are hashable ONLY if all their elements are hashable.

Hint 3: A tuple containing a list is NOT hashable — the hash function recurses into elements.


#4Dict Comprehension ToolkitEasy
dict comprehensionfilteringtransformation

Implement three common dict comprehension patterns: inverting a dict, filtering by value, and merging with conflict resolution.

Python
def invert_dict(d):
    return {v: k for k, v in d.items()}

def filter_by_value(d, threshold):
    return {k: v for k, v in d.items() if v > threshold}

def merge_with_sum(d1, d2):
    merged = dict(d1)  # shallow copy
    for k, v in d2.items():
        merged[k] = merged.get(k, 0) + v
    return merged

# Test invert
print(invert_dict({"a": 1, "b": 2, "c": 3}))

# Test filter
prices = {"apple": 1.2, "banana": 0.5, "cherry": 2.0, "durian": 5.5}
print(filter_by_value(prices, 1.5))

# Test merge with sum
d1 = {"a": 1, "b": 2, "c": 3}
d2 = {"a": 2, "b": 3, "d": 4, "c": 3}
print(merge_with_sum(d1, d2))
Solution
def invert_dict(d):
return {v: k for k, v in d.items()}

def filter_by_value(d, threshold):
return {k: v for k, v in d.items() if v > threshold}

def merge_with_sum(d1, d2):
merged = dict(d1)
for k, v in d2.items():
merged[k] = merged.get(k, 0) + v
return merged

Key points:

  • invert_dict assumes values are unique. If not, later keys overwrite earlier ones.
  • filter_by_value uses the if clause in the comprehension — clean and readable.
  • merge_with_sum uses .get(k, 0) to handle keys that exist in only one dict.
def invert_dict(d):
  """Return a new dict with keys and values swapped."""
  # TODO: implement with a dict comprehension
  pass

def filter_by_value(d, threshold):
  """Return entries where the value exceeds the threshold."""
  # TODO: implement with a dict comprehension
  pass

def merge_with_sum(d1, d2):
  """
  Merge two dicts. If a key exists in both, sum the values.
  Keys unique to either dict are included as-is.
  """
  # TODO: implement
  pass
Expected Output
{1: 'a', 2: 'b', 3: 'c'}
{'cherry': 2.0, 'durian': 5.5}
{'a': 3, 'b': 5, 'c': 6, 'd': 4}
Hints

Hint 1: For invert_dict, use: return a comprehension iterating over d.items() and swapping k, v.

Hint 2: For filter_by_value, add an if clause to the comprehension.

Hint 3: For merge_with_sum, start with a copy of d1, then iterate over d2 and use .get() with a default of 0 to handle missing keys.


#5Implement __hash__ and __eq__ for a Color ClassMedium
__hash____eq__hashability contractcustom class

Create a Color class with proper __hash__ and __eq__ so that two Color objects with the same RGB values can be used interchangeably as dict keys. Verify the hashability contract holds.

Python
class Color:
    def __init__(self, r: int, g: int, b: int):
        self.r = r
        self.g = g
        self.b = b

    def __repr__(self):
        return f"Color({self.r}, {self.g}, {self.b})"

    def __eq__(self, other):
        if not isinstance(other, Color):
            return NotImplemented
        return self.r == other.r and self.g == other.g and self.b == other.b

    def __hash__(self):
        return hash((self.r, self.g, self.b))

# Test
c1 = Color(220, 20, 60)
c2 = Color(220, 20, 60)
c3 = Color(0, 0, 255)

# Contract checks
print(c1 == c2)                        # True
print(hash(c1) == hash(c2))            # True (required by contract)
print(c1 != c3)                        # True

# Use as dict key
palette = {c1: "crimson", c3: "blue"}
print(len(palette))                     # 1 key per unique color
print(palette[c2])                      # "crimson" — c2 finds c1's entry
Solution
class Color:
def __init__(self, r: int, g: int, b: int):
self.r = r
self.g = g
self.b = b

def __repr__(self):
return f"Color({self.r}, {self.g}, {self.b})"

def __eq__(self, other):
if not isinstance(other, Color):
return NotImplemented
return self.r == other.r and self.g == other.g and self.b == other.b

def __hash__(self):
return hash((self.r, self.g, self.b))

Critical rules:

  1. __eq__ must return NotImplemented (not False) for unknown types — this lets Python try the other operand's __eq__.
  2. __hash__ must use the exact same fields as __eq__. If you hash only (self.r, self.g) but compare all three, two Colors with same r,g but different b would be equal yet have different hashes — violating the contract.
  3. Using hash((self.r, self.g, self.b)) delegates to Python's built-in tuple hashing, which is well-distributed and collision-resistant.
class Color:
  """
  Represents an RGB color. Two colors are equal if they
  have the same (r, g, b) values. Must be usable as a
  dict key.
  """
  def __init__(self, r: int, g: int, b: int):
      self.r = r
      self.g = g
      self.b = b

  def __repr__(self):
      return f"Color({self.r}, {self.g}, {self.b})"

  def __eq__(self, other):
      # TODO: implement
      pass

  def __hash__(self):
      # TODO: implement
      pass
Expected Output
True
True
True
1
crimson
Hints

Hint 1: In __eq__, first check isinstance(other, Color), returning NotImplemented if not.

Hint 2: Compare all three channels: self.r == other.r and self.g == other.g and self.b == other.b.

Hint 3: In __hash__, hash the tuple of the same fields used in __eq__: return hash((self.r, self.g, self.b)).

Hint 4: If you define __eq__ without __hash__, Python sets __hash__ = None and your class becomes unhashable.


#6Separate Chaining Hash MapMedium
hash tableseparate chainingcollision handling

Implement a hash map that uses separate chaining for collision resolution. Each bucket holds a list of (key, value) pairs. Collisions are handled by appending to the same bucket list.

Python
class SimpleHashMap:
    def __init__(self, capacity=8):
        self.capacity = capacity
        self.size = 0
        self.buckets = [[] for _ in range(capacity)]

    def _index(self, key):
        return hash(key) % self.capacity

    def put(self, key, value):
        idx = self._index(key)
        bucket = self.buckets[idx]
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, value)
                return
        bucket.append((key, value))
        self.size += 1

    def get(self, key, default=None):
        idx = self._index(key)
        for k, v in self.buckets[idx]:
            if k == key:
                return v
        return default

    def remove(self, key):
        idx = self._index(key)
        bucket = self.buckets[idx]
        for i, (k, v) in enumerate(bucket):
            if k == key:
                del bucket[i]
                self.size -= 1
                return True
        return False

    def __len__(self):
        return self.size

# Test
hm = SimpleHashMap(capacity=4)
hm.put("alice", "[email protected]")
hm.put("bob", "[email protected]")
hm.put("charlie", "[email protected]")

print(hm.get("alice"))       # [email protected]
print(hm.get("dave"))        # None
print(len(hm))               # 3 (but we test removal next)

hm.remove("bob")
print(hm.get("charlie"))     # still works — [email protected]
print(len(hm))               # 2 (but printed as 1 after the second removal test)
Solution
class SimpleHashMap:
def __init__(self, capacity=8):
self.capacity = capacity
self.size = 0
self.buckets = [[] for _ in range(capacity)]

def _index(self, key):
return hash(key) % self.capacity

def put(self, key, value):
idx = self._index(key)
bucket = self.buckets[idx]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
bucket.append((key, value))
self.size += 1

def get(self, key, default=None):
idx = self._index(key)
for k, v in self.buckets[idx]:
if k == key:
return v
return default

def remove(self, key):
idx = self._index(key)
bucket = self.buckets[idx]
for i, (k, v) in enumerate(bucket):
if k == key:
del bucket[i]
self.size -= 1
return True
return False

With capacity=4, collisions are very likely (3 keys in 4 buckets). The separate chaining approach handles this gracefully — multiple entries share a bucket list, and linear search within each bucket finds the right key. The trade-off: worst case is O(n) per bucket if all keys collide, but with a good hash function and reasonable load factor, buckets stay short.

class SimpleHashMap:
  """
  A hash map using separate chaining (each bucket is a list).
  Implement put, get, remove, and __len__.
  """
  def __init__(self, capacity=8):
      self.capacity = capacity
      self.size = 0
      self.buckets = [[] for _ in range(capacity)]

  def _index(self, key):
      # TODO: compute bucket index from key
      pass

  def put(self, key, value):
      # TODO: insert or update key-value pair
      pass

  def get(self, key, default=None):
      # TODO: retrieve value or return default
      pass

  def remove(self, key):
      # TODO: remove key, return True if found
      pass

  def __len__(self):
      return self.size
Expected Output
Hints

Hint 1: Use hash(key) % self.capacity to find the bucket index.

Hint 2: In put(), iterate the bucket to check if the key already exists (update in place). If not found, append a new (key, value) tuple.

Hint 3: In get(), iterate the bucket and return the value if the key matches. Return default if not found.

Hint 4: In remove(), iterate with enumerate to find the key, then use del bucket[i] to remove it.


#7Group Anagrams Using DictMedium
dictgroupingtuple keyshashable

Group a list of words into anagram sets. The key insight is choosing a hashable representation that is the same for all anagrams of a word. Since lists are not hashable, you need a tuple.

Python
def group_anagrams(words):
    groups = {}
    for word in words:
        key = tuple(sorted(word))
        groups.setdefault(key, []).append(word)

    # Sort each group and collect
    result = [sorted(group) for group in groups.values()]
    result.sort(key=lambda g: g[0])
    return result

# Test
words = ["eat", "tea", "tan", "ate", "nat", "bat", "eta", "ant", "aet"]
print(group_anagrams(words))
Solution
def group_anagrams(words):
groups = {}
for word in words:
key = tuple(sorted(word))
groups.setdefault(key, []).append(word)
result = [sorted(group) for group in groups.values()]
result.sort(key=lambda g: g[0])
return result

Why tuple(sorted(word)) works:

  • sorted("eat") produces ['a', 'e', 't'] — same for "tea", "ate", "eta"
  • Wrapping in tuple() makes it hashable: ('a', 'e', 't')
  • All anagrams map to the same tuple key, so they land in the same dict bucket

This is O(n * k log k) where n is the number of words and k is the max word length. The dict gives O(1) amortized grouping per word.

def group_anagrams(words):
  """
  Group words that are anagrams of each other.
  Return a list of groups (each group is a sorted list of words).
  The groups themselves should be sorted by their first word.

  Two words are anagrams if they contain the same characters
  in any order (case-sensitive).

  Hint: what hashable key uniquely identifies an anagram group?
  """
  # TODO: implement
  pass
Expected Output
[['aet', 'ate', 'eat', 'eta', 'tea'], ['ant', 'nat', 'tan'], ['bat']]
Hints

Hint 1: Two words are anagrams if and only if their sorted characters are identical.

Hint 2: Use tuple(sorted(word)) as the dict key — tuples are hashable, lists are not.

Hint 3: Use dict.setdefault(key, []).append(word) to build groups incrementally.

Hint 4: Sort each group and sort the final list of groups for deterministic output.


#8Safe Dict Iteration with ModificationMedium
iterationRuntimeErrordict mutationsnapshot

Practice the safe-iteration pattern: when you need to modify a dict during iteration, iterate over a snapshot (list(d.keys())) instead of the dict itself.

Python
def remove_low_scores(scores, threshold):
    for name in list(scores.keys()):  # snapshot of keys
        if scores[name] < threshold:
            del scores[name]
    return scores

def transfer_entries(source, dest, keys_to_move):
    for key in keys_to_move:
        if key in source:
            dest[key] = source.pop(key)
    return source, dest

# Test remove_low_scores
scores = {"Alice": 45, "Bob": 85, "Charlie": 30, "Diana": 92}
print(remove_low_scores(scores, 50))

# Test transfer_entries
src = {"x": 1, "y": 2, "z": 3}
dst = {"a": 10, "b": 20}
src, dst = transfer_entries(src, dst, ["y", "z", "missing"])
print(src, dst)
Solution
def remove_low_scores(scores, threshold):
for name in list(scores.keys()):
if scores[name] < threshold:
del scores[name]
return scores

def transfer_entries(source, dest, keys_to_move):
for key in keys_to_move:
if key in source:
dest[key] = source.pop(key)
return source, dest

Two patterns:

  1. Snapshot iteration: list(d.keys()) creates an independent list. You can safely del d[key] during the loop because you are iterating the list, not the dict.
  2. External key iteration: iterating over keys_to_move (not the source dict) means modifications to source via .pop() do not interfere with the iteration.

The dangerous version — for name in scores: del scores[name] — raises RuntimeError: dictionary changed size during iteration.

def remove_low_scores(scores, threshold):
  """
  Given a dict of name -> score, remove all entries where
  score is below the threshold. Return the modified dict.

  You must NOT trigger RuntimeError from modifying during
  iteration. Use the snapshot pattern.
  """
  # TODO: implement
  pass

def transfer_entries(source, dest, keys_to_move):
  """
  Move specified keys from source dict to dest dict.
  If a key doesn't exist in source, skip it.
  Return (source, dest) after transfer.
  """
  # TODO: implement
  pass
Expected Output
{'Bob': 85, 'Diana': 92}
{'x': 1} {'a': 10, 'b': 20, 'y': 2, 'z': 3}
Hints

Hint 1: Never delete from a dict while iterating over it directly — you get RuntimeError.

Hint 2: Use list(scores.keys()) or list(scores.items()) to take a snapshot before iterating.

Hint 3: For transfer_entries, iterate over keys_to_move (not the dict), use pop() to atomically remove and retrieve.


#9Two Sum with Hash MapMedium
hash mapO(1) lookupclassic algorithm

The classic Two Sum problem — solve it in O(n) time by leveraging the O(1) average lookup of a Python dict. For each element, check whether its complement (target - num) has already been seen.

Python
def two_sum(nums, target):
    seen = {}  # value -> index
    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return (seen[complement], i)
        seen[num] = i
    return None

# Test
print(two_sum([2, 7, 11, 15], 9))      # (0, 1): 2 + 7 = 9
print(two_sum([3, 2, 4], 6))            # (1, 2): 2 + 4 = 6
print(two_sum([1, 2, 3], 10))           # None
Solution
def two_sum(nums, target):
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return None

This is the single-pass hash map approach:

  • We iterate through the list once (O(n))
  • For each number, we compute target - num and check if it exists in our dict (O(1) average)
  • If found, we immediately return both indices
  • If not, we store the current number as a potential complement for future elements

Without a dict, the brute-force approach would be O(n^2) — checking all pairs. The dict reduces this to O(n) by turning the inner loop into a constant-time lookup.

def two_sum(nums, target):
  """
  Given a list of integers and a target sum, return the
  indices of two numbers that add up to target.

  Return a tuple (i, j) where i < j.
  If no solution exists, return None.

  Must be O(n) time using a dict for lookups.
  """
  # TODO: implement with a single-pass hash map approach
  pass
Expected Output
(0, 1)
(1, 2)
None
Hints

Hint 1: For each number, compute the complement: target - num.

Hint 2: Check if the complement is already in your dict. If so, return the pair of indices.

Hint 3: If not, store the current number and its index in the dict for future lookups.

Hint 4: The dict gives O(1) lookup for the complement, making the overall algorithm O(n).


#10Hash Map with Automatic ResizingHard
hash tableresizingload factorrehashing

Build a hash map that automatically doubles its capacity and rehashes all entries when the load factor exceeds 0.75. This demonstrates why resizing is O(n) per resize but O(1) amortized per insertion.

Python
class ResizingHashMap:
    def __init__(self, initial_capacity=4):
        self.capacity = initial_capacity
        self.size = 0
        self.resize_count = 0
        self.buckets = [[] for _ in range(self.capacity)]

    @property
    def load_factor(self):
        return self.size / self.capacity

    def put(self, key, value):
        idx = hash(key) % self.capacity
        bucket = self.buckets[idx]
        for i, (k, v) in enumerate(bucket):
            if k == key:
                bucket[i] = (key, value)
                return
        bucket.append((key, value))
        self.size += 1
        if self.load_factor > 0.75:
            self._resize()

    def get(self, key, default=None):
        idx = hash(key) % self.capacity
        for k, v in self.buckets[idx]:
            if k == key:
                return v
        return default

    def _resize(self):
        old_buckets = self.buckets
        self.capacity *= 2
        self.buckets = [[] for _ in range(self.capacity)]
        self.size = 0
        self.resize_count += 1
        for bucket in old_buckets:
            for key, value in bucket:
                self.put(key, value)

    def __len__(self):
        return self.size

# Test with 12 insertions — should trigger resizes at capacity 4 and 8
hm = ResizingHashMap(initial_capacity=4)
for i in range(12):
    hm.put(f"key_{i}", i * 100)
    lf = hm.load_factor
    if lf in (0.5, 0.75, 0.625):
        print(f"Load factor: {lf:.2f}")
    if hm.resize_count > 0 and hm.capacity != getattr(hm, '_last_cap', 0):
        print(f"Resized! New capacity: {hm.capacity}")
        hm._last_cap = hm.capacity

# Verify all values survived resizing
all_ok = all(hm.get(f"key_{i}") == i * 100 for i in range(12))
print(f"All 12 values verified correct" if all_ok else "ERROR: values lost!")
print(f"Total resizes: {hm.resize_count}")
Solution
class ResizingHashMap:
def __init__(self, initial_capacity=4):
self.capacity = initial_capacity
self.size = 0
self.resize_count = 0
self.buckets = [[] for _ in range(self.capacity)]

@property
def load_factor(self):
return self.size / self.capacity

def put(self, key, value):
idx = hash(key) % self.capacity
bucket = self.buckets[idx]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
bucket.append((key, value))
self.size += 1
if self.load_factor > 0.75:
self._resize()

def get(self, key, default=None):
idx = hash(key) % self.capacity
for k, v in self.buckets[idx]:
if k == key:
return v
return default

def _resize(self):
old_buckets = self.buckets
self.capacity *= 2
self.buckets = [[] for _ in range(self.capacity)]
self.size = 0
self.resize_count += 1
for bucket in old_buckets:
for key, value in bucket:
self.put(key, value)

The resize sequence with initial capacity 4:

  • After 3 inserts: load = 3/4 = 0.75 (at threshold)
  • 4th insert pushes load above 0.75 -- resize to capacity 8
  • After 6 inserts in capacity-8 table: load = 6/8 = 0.75
  • 7th insert triggers resize to capacity 16
  • Final 12 entries in capacity-16 table: load = 12/16 = 0.75

Each resize is O(n) — all entries are rehashed. But resizes happen at exponentially increasing intervals, so the amortized cost per insertion is O(1).

class ResizingHashMap:
  """
  Hash map with automatic resizing when load factor exceeds 0.75.
  Uses separate chaining. Track the number of resizes.

  Implement:
    - put(key, value)
    - get(key, default=None)
    - _resize() — double capacity and rehash all entries
    - load_factor property
  """
  def __init__(self, initial_capacity=4):
      self.capacity = initial_capacity
      self.size = 0
      self.resize_count = 0
      self.buckets = [[] for _ in range(self.capacity)]

  @property
  def load_factor(self):
      # TODO: return current load factor
      pass

  def put(self, key, value):
      # TODO: insert/update, then resize if load_factor > 0.75
      pass

  def get(self, key, default=None):
      # TODO: retrieve value
      pass

  def _resize(self):
      # TODO: double capacity, rehash all entries
      pass

  def __len__(self):
      return self.size
Expected Output
Load factor: 0.50
Load factor: 0.75
Resized! New capacity: 8
Load factor: 0.50
Load factor: 0.62
Load factor: 0.75
Resized! New capacity: 16
All 12 values verified correct
Total resizes: 2
Hints

Hint 1: Load factor = self.size / self.capacity.

Hint 2: In _resize(), save the old buckets, create new buckets with double capacity, then re-insert every (key, value) pair.

Hint 3: Rehashing is necessary because the bucket index depends on capacity: hash(key) % new_capacity gives a different index than hash(key) % old_capacity.

Hint 4: Check the load factor AFTER inserting (not before) — resize when it exceeds 0.75.


#11LRU Cache Using Dict OrderingHard
insertion orderOrderedDictcachedesign

Implement an LRU (Least Recently Used) cache that exploits Python 3.7+ dict insertion-order guarantee. The trick: deleting a key and re-inserting it moves it to the end of the iteration order, effectively marking it as "most recently used."

Python
class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = {}

    def get(self, key):
        if key not in self.cache:
            return -1
        # Move to end (most recently used)
        value = self.cache.pop(key)
        self.cache[key] = value
        return value

    def put(self, key, value):
        if key in self.cache:
            del self.cache[key]
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            # Evict least recently used (first key in order)
            oldest = next(iter(self.cache))
            del self.cache[oldest]

# Test — LeetCode 146 example
lru = LRUCache(2)
lru.put(1, 1)
lru.put(2, 2)
print(lru.get(1))       # 1 — marks key 1 as recently used
lru.put(3, 3)            # evicts key 2 (LRU)
print(lru.get(2))        # -1 (evicted)
lru.put(4, 4)            # evicts key 1? No — key 1 was recently used, evicts key 3
print(lru.get(1))        # -1 — actually key 1 was moved to end by get, then 3 was added
                          # order after get(1): [2, 1]. put(3,3) evicts 2. order: [1, 3]
                          # put(4,4) evicts 1. order: [3, 4]
print(lru.get(3))        # 3
print(lru.get(4))        # 4
Solution
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = {}

def get(self, key):
if key not in self.cache:
return -1
value = self.cache.pop(key)
self.cache[key] = value
return value

def put(self, key, value):
if key in self.cache:
del self.cache[key]
self.cache[key] = value
if len(self.cache) > self.capacity:
oldest = next(iter(self.cache))
del self.cache[oldest]

This works because:

  1. Dict insertion order (Python 3.7+): keys iterate in the order they were inserted.
  2. Delete + re-insert = move to end: removing a key and adding it back places it at the end of the order.
  3. next(iter(d)) returns the first key in order — the least recently used one.

Both get() and put() are O(1) average because dict operations are O(1). The collections.OrderedDict version has a dedicated move_to_end() method, but plain dict with delete+insert achieves the same result.

class LRUCache:
  """
  Least Recently Used cache with a fixed max size.
  Uses a plain dict (Python 3.7+ insertion order).

  - get(key): return value or -1 if missing. Marks key as recently used.
  - put(key, value): insert or update. Evicts the LEAST recently
    used entry if at capacity.

  Hint: to "move" a key to the end of insertion order,
  delete it and re-insert it.
  """
  def __init__(self, capacity: int):
      self.capacity = capacity
      self.cache = {}

  def get(self, key):
      # TODO: implement
      pass

  def put(self, key, value):
      # TODO: implement
      pass
Expected Output
1
-1
-1
3
4
Hints

Hint 1: Python 3.7+ dicts maintain insertion order. The 'oldest' key is the first in iteration order.

Hint 2: To mark a key as 'recently used', delete it and re-insert it — this moves it to the end of the order.

Hint 3: To evict the LRU entry, use next(iter(self.cache)) to get the first (oldest) key, then delete it.

Hint 4: In put(), if the key already exists, delete it first before re-inserting to refresh its position.


#12Frozen Hashable Dict WrapperHard
__hash____eq__frozensetimmutableadvanced

Build an immutable, hashable dict wrapper that can be used as a dict key or set element. Regular Python dicts are unhashable because they are mutable. FrozenDict solves this by blocking mutation and providing a stable hash based on frozenset(items()).

Python
class FrozenDict:
    def __init__(self, *args, **kwargs):
        object.__setattr__(self, '_data', dict(*args, **kwargs))

    def __getitem__(self, key):
        return self._data[key]

    def __contains__(self, key):
        return key in self._data

    def __len__(self):
        return len(self._data)

    def __iter__(self):
        return iter(self._data)

    def keys(self):
        return self._data.keys()

    def values(self):
        return self._data.values()

    def items(self):
        return self._data.items()

    def __eq__(self, other):
        if not isinstance(other, FrozenDict):
            return NotImplemented
        return self._data == other._data

    def __hash__(self):
        return hash(frozenset(self._data.items()))

    def __setitem__(self, key, value):
        raise TypeError("FrozenDict is immutable")

    def __delitem__(self, key):
        raise TypeError("FrozenDict is immutable")

    def __repr__(self):
        return f"FrozenDict({self._data})"

# Test basic access
fd = FrozenDict(a=1, b=2)
print(f"a => {fd['a']}")
print("b" in fd)

# Test equality and hashing
fd2 = FrozenDict({"b": 2, "a": 1})  # same content, different order
print(fd == fd2)
print(hash(fd) == hash(fd2))  # frozenset is order-independent

# Use as dict key
config_cache = {
    fd: "config A",
    FrozenDict(x=10): "config B",
}
print(f"found: {config_cache[fd2]}")  # fd2 finds fd's entry

# Use in a set
config_set = {fd, fd2, FrozenDict(a=1, b=2)}
print(len(config_set))  # 1 — all three are equal (but we added 3, two are dupes of fd)

# Verify immutability
try:
    fd["c"] = 3
except TypeError:
    print(True)
Solution
class FrozenDict:
def __init__(self, *args, **kwargs):
object.__setattr__(self, '_data', dict(*args, **kwargs))

def __getitem__(self, key):
return self._data[key]

def __contains__(self, key):
return key in self._data

def __len__(self):
return len(self._data)

def __iter__(self):
return iter(self._data)

def keys(self):
return self._data.keys()

def values(self):
return self._data.values()

def items(self):
return self._data.items()

def __eq__(self, other):
if not isinstance(other, FrozenDict):
return NotImplemented
return self._data == other._data

def __hash__(self):
return hash(frozenset(self._data.items()))

def __setitem__(self, key, value):
raise TypeError("FrozenDict is immutable")

def __delitem__(self, key):
raise TypeError("FrozenDict is immutable")

Key design decisions:

  1. object.__setattr__ in __init__: we need to set _data once, but our class blocks mutation via __setitem__. Using object.__setattr__ bypasses our class's attribute setting.
  2. frozenset(self._data.items()) for hashing: frozenset is unordered, so FrozenDict(a=1, b=2) and FrozenDict(b=2, a=1) produce the same hash. This satisfies the contract since they are also equal.
  3. Limitation: values must themselves be hashable for frozenset(items()) to work. A FrozenDict containing a list value would fail at hash time.
class FrozenDict:
  """
  An immutable, hashable dict wrapper. Can be used as a dict
  key or added to a set.

  Requirements:
  - Immutable: __setitem__ and __delitem__ raise TypeError
  - Hashable: implements __hash__ using frozenset of items
  - Supports read operations: __getitem__, __contains__,
    __len__, __iter__, keys(), values(), items()
  - Two FrozenDicts with the same content are equal
  """
  def __init__(self, *args, **kwargs):
      # Store data in a private regular dict
      # TODO: implement
      pass

  def __getitem__(self, key):
      # TODO
      pass

  def __contains__(self, key):
      # TODO
      pass

  def __len__(self):
      # TODO
      pass

  def __iter__(self):
      # TODO
      pass

  def __eq__(self, other):
      # TODO
      pass

  def __hash__(self):
      # TODO: use frozenset of items
      pass

  def __setitem__(self, key, value):
      raise TypeError("FrozenDict is immutable")

  def __delitem__(self, key):
      raise TypeError("FrozenDict is immutable")
Expected Output
a => 1
True
True
True
found: config A
2
True
Hints

Hint 1: Store the data in object.__setattr__(self, '_data', dict(*args, **kwargs)) to bypass your own __setitem__ block.

Hint 2: For __hash__, use hash(frozenset(self._data.items())) — frozenset is order-independent and hashable.

Hint 3: Two FrozenDicts are equal if their underlying dicts are equal: self._data == other._data.

Hint 4: Delegate read methods (__getitem__, __contains__, __len__, __iter__) to self._data.

© 2026 EngineersOfAI. All rights reserved.