The hash map pattern — turning O(n^2) into O(n)
The single highest-value pattern in interview-style problems, worked through two-sum from brute force to the optimal solution, plus why it works.
3 min read
The problem
Given a list of numbers and a target, find two numbers that add up to the target. Return their indices.
nums = [2, 7, 11, 15]
target = 9
# answer: [0, 1] — because nums[0] + nums[1] == 9The brute-force instinct — check every pair
def two_sum_brute(nums, target):
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]This is correct and it's O(n^2) — for every element, it scans every other element looking for a match. It works fine on 10 numbers and becomes genuinely slow on 100,000.
The reframe that unlocks the fast solution
The brute-force version asks, for each number, "does some other number in the list equal target - nums[i]?" — and answers that question by scanning, which is O(n) per element. The fix isn't a cleverer scan — it's replacing the scan with a lookup:
def two_sum_fast(nums, target):
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = iOne pass. For each number, check whether its complement (the number that would complete the pair) has already been seen — a hash map lookup, O(1) on average — instead of scanning the rest of the list. If not, remember this number's index for later. Total: O(n) time, O(n) extra space for the hash map. The trade is explicit: spend memory to avoid repeated scanning.
Why the hash map lookup is O(1) average case
A hash map computes a hash of the key and uses it to jump almost directly to the right storage bucket — it doesn't search through entries one by one the way a list does. This holds on average; a pathological case with many hash collisions degrades toward O(n), which is why languages harden their hash functions against adversarial input, but for ordinary data this is a safe, standard assumption to build on.
The pattern generalizes far beyond two-sum
Any time a brute-force solution is "for each item, scan the rest of the list looking for X," the question worth asking is: can X be checked with a hash map/set lookup instead of a scan? This exact reframe solves a large family of problems:
- Contains duplicate — have I seen this value before? (hash set)
- Group anagrams — group strings by a computed key (sorted letters, or a letter-count tuple) using a hash map from key to list of matches.
- First unique character — count occurrences with a hash map, then scan once more for the first count of 1.
In every case, the shape is identical: one pass to build or check against a hash map, instead of a nested loop re-scanning the input for every element.
Further reading
- Wikipedia — Hash table
- Python wiki — Time Complexity, for the real average/worst-case bounds of
dict/setoperations in CPython specifically.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. In two_sum_brute, why is the nested loop O(n^2)?
2. What's the key reframe that turns two-sum from O(n^2) into O(n)?
3. Why does the hash map version of two-sum use O(n) extra space, while the brute-force version doesn't?
4. What's the general signal that a problem might benefit from the hash map pattern?