Hashing & Sets
Topic 2 of 7, with 3 concept checks. Turning O(n^2) lookups into O(1) with the right data structure
Trade repeated scans for constant-time lookup
Remembered state
Decide which key represents previously seen information, what value must be stored, and whether lookup happens before insertion in common hashing interview patterns.
Core lesson 01
Whenever a brute-force solution has a nested loop searching for something (a complement, a duplicate, a previously-seen value), storing what you've seen so far in a dict or set turns that inner search into an O(1) average lookup, collapsing O(n^2) to O(n).
The pattern is almost always the same: iterate once, and at each step ask 'have I seen the thing I need already?' by checking a hash-backed structure built incrementally, then add the current element before moving on. This single-pass-with-memory approach is one of the most common transformations in interview problems.
def contains_duplicate(nums):
seen = set()
for num in nums:
if num in seen:
return True
seen.add(num)
return FalseWhat to remember
What's the core insight that makes hash-map problems solvable in O(n) instead of O(n^2)?
Common footguns
- Checking membership and inserting in the wrong order for problems where the current element could pair with itself (e.g. target = 2*num) - decide deliberately whether to check before or after inserting.
Core lesson 02
Use collections.Counter whenever the problem cares about HOW MANY times something appears - anagrams, frequency thresholds, majority elements - not just whether it appears at all.
A set answers 'is x present?' A Counter answers 'how many x are there?' - necessary whenever comparing multiset structure (two strings are anagrams if their Counters are equal) or ranking by frequency (top-k). Counter also supports arithmetic (subtraction, intersection via &) convenient for these problems.
from collections import Counter
def is_anagram(s1, s2):
return Counter(s1) == Counter(s2)
def most_common_k(nums, k):
return [num for num, _ in Counter(nums).most_common(k)]What to remember
When do you need a Counter/frequency dict instead of a plain set?
Common footguns
- Using a set to check 'anagram-ness' by comparing unique characters only - misses cases with different character counts, like 'aab' vs 'abb' (same set of characters, not anagrams).
Core lesson 03
For 'group by' problems, compute a hashable signature for each item (e.g. its sorted characters for anagrams) and use it as a dict key, appending each original item to that key's list.
Grouping problems (group anagrams, group by remainder, group by pattern) all follow the same shape: define a function mapping an item to a canonical key representing its group, then build a dict from that key to a list of original items sharing it. The cleverness is entirely in choosing the right key function - the dict mechanics are always the same.
from collections import defaultdict
def group_anagrams(words):
groups = defaultdict(list)
for word in words:
key = ''.join(sorted(word)) # canonical signature
groups[key].append(word)
return list(groups.values())What to remember
How do you use a hash map to solve 'group items by some computed key' problems?
Common footguns
- Using an expensive or non-canonical key (e.g. an unsorted string) that doesn't actually group equivalent items together correctly.
Python lab
Browser Python lab
Runtime · idle
Python loads on your first run. Your code stays in this browser.
Best practices
- Default to a hash map/set whenever you catch yourself writing a nested loop to check 'have I seen this before?'.
- Use Counter for anything involving frequency, not just presence.
- For grouping problems, spend your effort choosing the right canonical key function - the dict logic itself is always the same.
- Remember dict/set operations are O(1) AVERAGE, not worst-case guaranteed.
Apply the concept in Interview practice
Group AnagramsmediumLeetCode #49 · O(n·k log k) time
Key a dict by the sorted-tuple of each word's letters, appending each original word to its group.
Open problemTop K Frequent ElementsmediumLeetCode #347 · O(n log k) time
Count with Counter, then use a heap or bucket sort by frequency to get the top k.
Open problemValid SudokumediumLeetCode #36 · O(1) - fixed 9x9 board
Use a set per row, per column, and per 3x3 box; for each filled cell, check whether its value already exists in the corresponding row/column/box set.
Open problemLongest Consecutive SequencemediumLeetCode #128 · O(n) time
Put all numbers in a set; only start counting a sequence from numbers whose predecessor (num-1) is NOT in the set, then count upward.
Open problemSubarray Sum Equals KmediumLeetCode #560 · O(n) time
Track running prefix sums in a dict counting how many times each sum has occurred; at each step check if (running_sum - k) has been seen before.
Open problemConcept checks
What's the core insight that makes hash-map problems solvable in O(n) instead of O(n^2)?
Hint
Trade a nested loop for a single pass plus O(1) average lookups.
Whatever you'd search for in the inner loop, store it in a dict/set as you go instead.
Answer
Whenever a brute-force solution has a nested loop searching for something (a complement, a duplicate, a previously-seen value), storing what you've seen so far in a dict or set turns that inner search into an O(1) average lookup, collapsing O(n^2) to O(n).
The pattern is almost always the same: iterate once, and at each step ask 'have I seen the thing I need already?' by checking a hash-backed structure built incrementally, then add the current element before moving on. This single-pass-with-memory approach is one of the most common transformations in interview problems.
def contains_duplicate(nums):
seen = set()
for num in nums:
if num in seen:
return True
seen.add(num)
return FalseWatch out
- Checking membership and inserting in the wrong order for problems where the current element could pair with itself (e.g. target = 2*num) - decide deliberately whether to check before or after inserting.
When do you need a Counter/frequency dict instead of a plain set?
Hint
A set only tells you whether something exists; a Counter tells you how many times.
Anagram, frequency, and 'k most common' problems almost always need counts, not just presence.
Answer
Use collections.Counter whenever the problem cares about HOW MANY times something appears - anagrams, frequency thresholds, majority elements - not just whether it appears at all.
A set answers 'is x present?' A Counter answers 'how many x are there?' - necessary whenever comparing multiset structure (two strings are anagrams if their Counters are equal) or ranking by frequency (top-k). Counter also supports arithmetic (subtraction, intersection via &) convenient for these problems.
from collections import Counter
def is_anagram(s1, s2):
return Counter(s1) == Counter(s2)
def most_common_k(nums, k):
return [num for num, _ in Counter(nums).most_common(k)]Watch out
- Using a set to check 'anagram-ness' by comparing unique characters only - misses cases with different character counts, like 'aab' vs 'abb' (same set of characters, not anagrams).
How do you use a hash map to solve 'group items by some computed key' problems?
Hint
The key doesn't have to be the item itself - compute a signature for it.
Sorted tuple of characters, a rounded value, a normalized form - anything hashable works as a key.
Answer
For 'group by' problems, compute a hashable signature for each item (e.g. its sorted characters for anagrams) and use it as a dict key, appending each original item to that key's list.
Grouping problems (group anagrams, group by remainder, group by pattern) all follow the same shape: define a function mapping an item to a canonical key representing its group, then build a dict from that key to a list of original items sharing it. The cleverness is entirely in choosing the right key function - the dict mechanics are always the same.
from collections import defaultdict
def group_anagrams(words):
groups = defaultdict(list)
for word in words:
key = ''.join(sorted(word)) # canonical signature
groups[key].append(word)
return list(groups.values())Watch out
- Using an expensive or non-canonical key (e.g. an unsorted string) that doesn't actually group equivalent items together correctly.