🗺️ Hash Tables and Maps
A hash table maps keys to values using a hash function to compute an index into an underlying array, giving average O(1) lookup, insert, and delete. Python’s dict and set ARE hash tables under the hood.
How Hashing Works (Conceptually)
def simple_hash(key, table_size):
return sum(ord(c) for c in key) % table_size # toy example, real hash functions are far more robust
simple_hash("apple", 10) # some index between 0 and 9hash() in Python is built in and used automatically by dict/set:
hash("apple") # a large integer, consistent within a single process run
hash(42) # ints hash to themselves for small values
hash((1, 2)) # tuples are hashable if their contents are
hash([1, 2]) # TypeError! lists are unhashable, mutability breaks the hash guaranteeCollisions
Two different keys can hash to the same slot. Common resolution strategies:
- Chaining: each slot holds a list of entries that hashed there.
- Open addressing: probe for the next free slot instead of chaining (Python’s
dictuses a variant of open addressing internally).
You rarely implement this yourself in Python
dictandsetare highly optimized C implementations. The main practical skill is RECOGNIZING when a hash table is the right tool, not reimplementing one from scratch, though understanding the internals explains WHY operations are O(1) average but O(n) worst case (with many collisions).
The Core Pattern: Trading Space for Time
The single most common DSA trick: replace an O(n) linear scan with an O(1) hash lookup by pre-storing seen values.
def two_sum(nums, target):
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen: # O(1) lookup instead of scanning the rest of the array
return [seen[complement], i]
seen[num] = i
return []
two_sum([2, 7, 11, 15], 9) # [0, 1], since nums[0] + nums[1] == 9O(n) time, O(n) space, a massive improvement over the naive O(n²) nested-loop approach.
Grouping with a Hash Map
from collections import defaultdict
def group_anagrams(words):
groups = defaultdict(list)
for word in words:
key = "".join(sorted(word)) # anagrams share the same sorted-letter key
groups[key].append(word)
return list(groups.values())
group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"])
# [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]Frequency Counting
from collections import Counter
def most_frequent(nums, k):
counts = Counter(nums)
return [num for num, _ in counts.most_common(k)]
most_frequent([1, 1, 1, 2, 2, 3], 2) # [1, 2]Checking for Existence: Set vs Dict
seen = set() # use a set when you only need MEMBERSHIP, not an associated value
seen.add(5)
5 in seen # O(1)
counts = {} # use a dict when you need a VALUE tied to each key (count, index, etc)Designing a Simple Hash Map (Educational, Interview Prep)
class MyHashMap:
def __init__(self, size=1000):
self.size = size
self.buckets = [[] for _ in range(size)] # chaining for collision resolution
def _hash(self, key):
return hash(key) % self.size
def put(self, key, value):
bucket = self.buckets[self._hash(key)]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value) # update existing key
return
bucket.append((key, value)) # new key
def get(self, key):
bucket = self.buckets[self._hash(key)]
for k, v in bucket:
if k == key:
return v
return -1
def remove(self, key):
bucket = self.buckets[self._hash(key)]
for i, (k, v) in enumerate(bucket):
if k == key:
del bucket[i]
returnWhen a hash table is the wrong tool
If you need ORDERED traversal by key, or range queries (“all keys between 10 and 50”), a hash table doesn’t help, reach for a balanced tree structure instead (see Binary Search Trees). Hash tables optimize for exact-match lookup only.