Patterns/Hash Maps

Hash Maps

Most used pattern
85 min read
Updated June 2026
What you'll learn
  • Identify the inner-loop search pattern in O(n²) solutions and replace it with an O(1) hash map lookup to drop to linear time
  • Decide between one-pass and two-pass complement lookup based on whether valid matches can appear after the current element
  • Pair prefix sums with hash maps to count subarrays with a target sum, the approach that works when negative values make sliding window unreliable
  • Choose the right hash map key for grouping problems so that equivalent elements collide: sorted characters, canonical forms, computed signatures

A hash map stores key-value pairs and retrieves any value in O(1) average time, no matter how large the data set. This turns brute force solutions that search for matches in O(n) per element into single-pass O(n) algorithms. But choosing the wrong key, storing the wrong value, or missing an initialization like {0: 1} produces code that looks correct but silently fails on edge cases.

What Is a Hash Map?

In most brute force solutions, the bottleneck is a repeated search: for each element, scan the rest of the array looking for a match. A hash map eliminates that scan. It stores data so that any lookup takes O(1) average time, turning an O(n) inner search into a single operation. That is why hash maps appear across so many interview patterns.

Linear Search · O(n)

Target
"alice"
Checking
Checked
0
Found
0
bob
1
2
carol
3
4
dave
5
6
alice
7

Find "alice" in this array. No structure, no shortcuts. Must check each slot.

Hash Lookup · O(1)

Key
"alice"
Hash
Index
Found
0
bob
1
2
carol
3
4
dave
5
6
alice
7

What if we could compute WHERE to look instead of searching?

Key Insight
How a Hash Function Gives O(1) Access
Arrays give O(1) access by position, but you rarely know the position. A hash function computes the position from the data itself. Instead of scanning to find where something is, you calculate where it must be.
  1. The hash function converts a key to a number
  2. Modulo maps that number to an array index
  3. Arrays give O(1) access by index

If you can express what you need as a key, the hash function computes its position directly instead of scanning.

Hash Map Basics
1# Python: dict or defaultdict
2seen = {}
3seen[key] = value
4if key in seen: # O(1) average
5 value = seen[key]
6
7# Counting shorthand
8from collections import Counter
9freq = Counter(nums) # freq[x] = count of x
Common Mistake
When a hash map is the wrong choice
Hash maps cost O(n) extra space and destroy ordering. If you need sorted access or range queries, a sorted array with binary search may be better. If all you need is existence checks with no associated values, use a set instead of a map.

In Python, dict is the hash map. For counting, Counter is a specialized dict that defaults missing keys to 0. For maps where missing keys should auto-initialize, defaultdict avoids the repeated if key not in dict check.

What Hash Maps Actually Do

Start with the brute force: for each of n elements, scan all other elements to find a match. That is n iterations times O(n) work each, giving O(n²). What if the inner search cost O(1) instead of O(n)? Total work drops to n times O(1), which is O(n). A hash map provides exactly that O(1) inner lookup. Every hash map optimization follows the same three steps. Identify the repeated search, precompute its answer into a map, replace the search with a lookup.

Before and After
1# O(n²): For each element, search the rest
2for i in range(n):
3 for j in range(i+1, n): # inner loop = O(n) search
4 if condition(nums[i], nums[j]):
5 ...
6
7# O(n): Store what you've seen, check in O(1)
8seen = {}
9for i, num in enumerate(nums):
10 if complement in seen: # O(1) lookup replaces inner loop
11 ...
12 seen[num] = i
Interview Tip
Spotting the Hidden Search
Not every O(n²) solution has an obvious nested loop. Sometimes the inner search is disguised: calling x in list (O(n) in Python), using .index(), or re-scanning a substring. Whenever you see O(n²) runtime, ask: where is the repeated search? That search is your hash map target.

How a Hash Map Replaces the Inner Loop

Two Sum is the cleanest place to see the four hash map decisions: what value to search for, what to use as the key, what to store as the value, and whether to check before or after storing. The same four decisions apply to every hash map transformation.

Two Sum: Four Design Decisions

Problem: Find two indices where nums[i] + nums[j] == target.

Decision 1: What am I searching for?

The inner loop checks every other element to see if it equals target - nums[i]. We are searching for a complement.

Brute Force
1# Brute force O(n²): inner loop searches for complement
2def two_sum_brute(nums, target):
3 n = len(nums)
4 for i in range(n):
5 complement = target - nums[i]
6 for j in range(i + 1, n): # O(n) search for complement
7 if nums[j] == complement:
8 return [i, j]
9 return []

Decision 2: What is the key?

We need to look up whether a specific value exists. The key is the value itself. When we encounter 7 and need to know if 3 is in the map, we look up 3 as the key.

Decision 3: What is the value?

The problem asks for indices, not just whether a match exists. So the map stores value to index. A set would tell us "yes, 3 exists" but not where it is.

Decision 4: Check before or after storing?

If we store first, an element could match itself. Check first, then store, so each element only matches elements that came before it. One pass suffices because if (a, b) is a valid pair, whichever you reach second will find the other already in the map.

Hash Map Solution
1# Optimal O(n): all four decisions applied
2def two_sum(nums, target):
3 seen = {} # Decision 2+3: value -> index
4 for i, num in enumerate(nums):
5 complement = target - num # Decision 1: searching for complement
6 if complement in seen: # Decision 4: check BEFORE storing
7 return [seen[complement], i]
8 seen[num] = i # Store after checking
9 return []

The Same Decisions, Different Problems

Subarray Sum Equals K: The same four decisions apply, but Decision 1 is harder because the "search" is hidden inside prefix sums. You are not searching for an element in the array. You are searching for a previous prefix sum that, when subtracted from the current prefix sum, equals k. We cover this in the deep dive section below.

Group Anagrams: This uses a different shape of the same idea. Instead of searching for a complement, you compute a canonical form (sorted characters) and group items by it. Decision 2, choosing the right key, is the hard part. The key must be identical for items that belong together and different for items that do not.

Two Sum · target = 6

Index
Value
Need
Map
{}
Found
nums[]302142seenempty

For each element, check if its complement exists. O(n) nested loops become O(n).

Common Mistake
Why a set does not solve Two Sum

A set can answer "does this value exist?" in constant time, which seems like enough. But Two Sum does not just ask whether two numbers sum to the target. It asks for the indices of those two numbers. A set discards positional information entirely.

Consider nums = [3, 3] with target = 6. A set gives {3}. You know 3 exists, but you cannot distinguish the first 3 from the second 3. You have no way to produce the answer [0, 1] because the set does not remember where each value appeared.

A hash map stores value to index. When you encounter the second 3 at index 1, you look up the complement (6 minus 3 = 3) and find it at index 0 in the map. The map gives you both the existence check and the positional information in a single lookup.

The general rule: when a problem asks "which elements" or "at what positions," you need a map. When it only asks "does something exist," a set is sufficient and communicates intent more clearly.

Complement Lookup Across Problems

Many problems are variations of Two Sum. The only difference is how the complement is computed.

ProblemWhat You HaveComplement Formula
Two SumCurrent numtarget - num
Subarray Sum = KCurrent prefix sumcurrent_sum - k
4Sum IITwo nums from A,B-(a+b) in map of c+d
Count pairs diff = kCurrent numnum + k or num - k
Continuous Subarray Sumcurrent_sum % kSame remainder seen before

When a Hash Map Is a Good Fit

Six problem signals point toward a hash map solution. Recognizing the signal early narrows the design space before you start coding.

  • Repeated lookup. The brute force has an inner loop that searches for something on every iteration. A hash map stores the answers so each search costs O(1) instead of O(n).
  • Complement search. You can compute exactly what the matching element must be. Instead of checking every pair, compute the complement and look it up directly.
  • Frequency counting. The answer depends on how many times elements appear. Build a count map in one pass, then query it.
  • Grouping by canonical key. Equivalent items share a computable property. The hash map groups them by that property in a single pass.
  • Prefix sum memory. Subarray sum problems with possible negative values cannot use sliding window. A hash map stores prefix sums so that complement lookup finds subarrays with a target sum in O(1).
  • Auxiliary state inside another pattern. Sliding window uses a frequency map for window contents. DFS and BFS use a visited map for metadata. DP uses a hash map for memoization. In each case, the hash map provides O(1) state queries that the outer pattern depends on.

Hash Map Templates

Five templates cover the vast majority of hash map interview problems.

Complement Lookup

One-pass check-then-store. For each element, compute what would complete it, check if that exists, then store the current element for future lookups. Check-then-store ordering prevents an element from matching itself. One pass suffices because if (a, b) is a valid pair, whichever you reach second will find the other already in the map.

When to use: Finding pairs with a specific relationship (sum, difference, product).

Complement Lookup Template
1def complement_lookup(nums, target):
2 seen = {} # value -> index (or count)
3 for i, num in enumerate(nums):
4 complement = target - num # compute what we need
5 if complement in seen: # check BEFORE storing
6 return [seen[complement], i]
7 seen[num] = i # only then add current element
8 return []

Common variations:

  • • Two Sum (complement = target - num)
  • • Pair with difference k (complement = num + k or num - k)
  • • 4Sum II: precompute all sums a+b from two arrays into a count map, then for each c+d pair check if -(c+d) exists. Splits O(n⁴) into two O(n²) passes.

Frequency Counting

Build a count map in one pass, then query it. Two separate phases (count everything, then query) work because you cannot answer frequency questions until you have seen all the data.

When to use: Any problem asking about frequencies, majorities, top-k, or comparing character distributions.

Find Majority Element

Current
-
Threshold
> 3
Majority
-
Action
START
nums[]2211122freqempty

7 elements. Majority = appears more than 3 times. One pass with a frequency map.

Frequency Counting Template
1from collections import Counter
2
3def frequency_pattern(nums):
4 freq = Counter(nums) # Phase 1: count everything first
5 for num, count in freq.items(): # Phase 2: now you can answer questions
6 if count > threshold:
7 return num

Common variations:

  • • Majority Element (count > n/2)
  • • Top K Frequent: use freq.most_common(k), or build a bucket array where index = frequency and scan from the highest bucket down. The bucket approach avoids sorting and runs in O(n).
  • • Valid Anagram (Counter(s) == Counter(t))

Grouping by Key

Compute a canonical key for each item, then group items with the same key. defaultdict(list) handles first-time keys automatically, avoiding the if-key-not-in-dict boilerplate.

When to use: Grouping equivalent items: anagrams, isomorphic strings, items sharing a computed property.

Grouping by Key Template
1from collections import defaultdict
2
3def group_by_key(items):
4 groups = defaultdict(list) # auto-creates list for new keys
5 for item in items:
6 key = compute_key(item) # what makes items "equivalent"?
7 groups[key].append(item)
8 return list(groups.values())

Common variations:

  • • Group Anagrams: key = tuple(sorted(s)). Sorting "eat" and "tea" both produce ('a','e','t'), so they land in the same bucket. An alternative key is a 26-element character count tuple, which runs in O(m) per string instead of O(m log m).
  • • Isomorphic Strings: "egg" and "add" are isomorphic because each character in one maps to exactly one character in the other (e maps to a, g maps to d). Key = tuple of first-occurrence positions. "egg" produces (0, 1, 1), "add" produces (0, 1, 1). Two strings are isomorphic when their position patterns match.
  • • Group Shifted Strings: "abc" and "bcd" differ by a constant shift of 1. Key = tuple of differences between consecutive characters mod 26. "abc" produces (1, 1), "bcd" produces (1, 1). Mod 26 handles wraparound, so "xyz" and "yza" also group together.

Prefix Sum + Hash Map

Running prefix sum with complement lookup. The {0: 1} initialization accounts for subarrays starting at index 0, where the prefix sum itself equals k.

When to use: Count or find contiguous subarrays with a target sum, or sum divisible by k.

Prefix Sum + Hash Map Template
1def prefix_sum_pattern(nums, k):
2 prefix_count = {0: 1} # handles subarrays starting at index 0
3 current_sum = 0
4 count = 0
5 for num in nums:
6 current_sum += num
7 complement = current_sum - k # what prefix sum would give subarray = k?
8 count += prefix_count.get(complement, 0)
9 prefix_count[current_sum] = prefix_count.get(current_sum, 0) + 1
10 return count

Common variations:

  • • Subarray Sum Equals K (complement = current_sum - k)
  • • Continuous Subarray Sum: key = current_sum % k. Two prefix sums with the same remainder mod k means the subarray between them sums to a multiple of k. Store the first index for each remainder to check the length >= 2 requirement.
  • • Contiguous Array: treat 0 as -1, then the problem becomes "find longest subarray with sum 0." Store the first index where each prefix sum appears. The longest subarray is the maximum gap between matching prefix sums.

Sliding Window State

Frequency map tracking window contents. Increment on entry, decrement on exit, delete zero-count keys to keep len(freq) equal to the number of distinct elements in the window. Most validity checks depend on that count being accurate.

When to use: Sliding window problems where you need to track distinct elements, character frequencies, or window validity.

Sliding Window State Template
1def sliding_window_state(s, k):
2 freq = {} # element -> count in current window
3 left = 0
4 best = 0
5 for right, char in enumerate(s):
6 freq[char] = freq.get(char, 0) + 1
7 while len(freq) > k: # window has too many distinct elements
8 freq[s[left]] -= 1
9 if freq[s[left]] == 0:
10 del freq[s[left]] # remove so len() reflects true distinct count
11 left += 1
12 best = max(best, right - left + 1)
13 return best

Common variations:

  • • Longest Substring with K Distinct Characters
  • • Minimum Window Substring: maintain two frequency maps. One for the required characters (need), one for the current window (have). Shrink from the left once all required characters are covered, tracking the smallest valid window seen so far.
  • • Longest Substring Without Repeating Characters

Group Anagrams: Key Design

Problem: Given an array of strings, group anagrams together. Return a list of groups.

What makes "eat" and "tea" the same thing in this problem is that they contain the same characters. What makes "eat" and "bat" different is that they do not. The exact property is the multiset of characters. The challenge is expressing that property as a single hashable value Python can use as a dictionary key. The raw string does not work because "eat" and "tea" are different strings. The string's length does not work because "eat" and "bat" both have length 3 but are not anagrams.

Group Anagrams

String
-
Key (sorted)
-
Groups
0
Action
START
Input Strings
eat
tea
tan
ate
nat
bat
Grouped by Key
(no groups yet)

Anagrams share the same sorted characters. Sort each string to get a canonical key.

The Discovery Process

Given: ["eat", "tea", "tan", "ate", "nat", "bat"]

Step 1: What makes "eat" and "tea" equivalent? Same characters. "tan" and "nat" also share the same characters. "bat" is alone.

Step 2: How do I capture "same characters" as an identical, hashable value?

  • Can't use the string itself (different order)
  • Can't use length (too broad, "bat" also has length 3)
  • Sorted characters: "eat" sorts to "aet", "tea" sorts to "aet". Identical.

Step 3: Key = tuple(sorted(s)). Why tuple? Lists aren't hashable.

Group Anagrams
1from collections import defaultdict
2
3def group_anagrams(strs):
4 groups = defaultdict(list)
5 for s in strs:
6 key = tuple(sorted(s)) # Canonical form
7 groups[key].append(s)
8 return list(groups.values())

key = tuple(sorted(s)): Sorting removes the arbitrary character ordering that makes "eat" different from "tea" while preserving the exact character identity that makes "eat" different from "bat." After sorting, all anagrams of "eat" become "aet." The tuple wrapper is required because Python lists are not hashable and cannot serve as dictionary keys. Without this: using the raw string as key puts "eat," "tea," and "ate" into three separate groups, and using a list instead of tuple crashes at runtime.

groups = defaultdict(list): A defaultdict with list as the factory function creates an empty list automatically when a new key is first accessed. Without it, you would need to check whether the key already exists before appending, or use setdefault on every iteration. The defaultdict removes that boilerplate and makes the intent clear: we are grouping items.

groups[key].append(s): The original string is appended, not the sorted key. The key is the grouping mechanism. The values stored in each group are the original inputs, because that is what the problem asks you to return. Storing the sorted key instead would lose the original strings entirely.

Why It Works

A good key satisfies two requirements: same result for items that belong together, and different results for items that don't. Sorting removes the arbitrary character ordering while preserving the exact character set.

Alternative key: Character count as tuple. O(n) instead of O(n log n) but more complex. For interviews, sorted is fine.

Common Mistake
Why length is a bad grouping key

String length seems like a reasonable grouping property at first glance. Anagrams do have the same length, so grouping by length puts all anagrams into the same bucket. But "eat" and "bat" both have length 3 and are not anagrams. Length groups too broadly. It is a necessary condition for anagram equivalence but not a sufficient one, so unrelated strings end up in the same group.

The opposite mistake is grouping by first character. This splits "eat" and "ate" into different groups even though they are anagrams, because their first characters differ. First character groups too narrowly in one dimension and misses the property that actually defines anagram equivalence.

The right key captures exactly the property that makes items equivalent and nothing more. For anagrams, that property is the multiset of characters. Sorted characters express this precisely: same output for all anagrams of a word, different output for any non anagram. No spurious collisions, no spurious splits.

Key Insight
The Key Defines the Group
Every grouping problem asks: "What property do these items share that makes them the same?" That property, expressed as a hashable value, is your key.
Key Insight
When to use grouping
If a problem asks you to group items by a shared property, the challenge is designing a canonical key that maps equivalent items to the same value. If two items are equivalent when their elements are reordered, sorted characters or a character count tuple works as the canonical form. Any time you need a hashable key from a sequence in Python, use tuple because lists and dictionaries are not hashable.

Subarray Sum Equals K: Prefix Sum

Problem: Given an array of integers and a target k, count the number of contiguous subarrays that sum to exactly k.

Your first instinct might be sliding window. Sliding window relies on monotonicity: adding an element always increases the window sum. When the array contains negative numbers, that assumption breaks, so sliding window cannot solve this problem in general.

Prefix sums provide the path forward. If prefix[j] minus prefix[i] equals k, the subarray between positions i and j sums to k. Rearranging gives prefix[i] = prefix[j] minus k. This is a complement lookup with the same structure as Two Sum, but the "elements" are prefix sums and the "target" is k.

Subarray Sum = 5

prefix_sum
0
Complement
-
In Map?
-
Total Count
0
Action
INIT
nums[]203112-1354prefix_count0cnt: 1

Start with {0: 1}. When prefix_sum equals 5, the complement is 0. This seed catches subarrays starting from index 0.

Prefix Sums as Complements

If prefix[j] - prefix[i] = k, then the subarray [i+1...j] has sum k. Rearranging: prefix[i] = prefix[j] - k. This is a complement lookup on prefix sums.

Subarray Sum Equals K
1def subarray_sum(nums, k):
2 prefix_count = {0: 1} # Critical: empty prefix
3 current_sum = 0
4 count = 0
5
6 for num in nums:
7 current_sum += num
8 complement = current_sum - k
9 count += prefix_count.get(complement, 0)
10 prefix_count[current_sum] = prefix_count.get(current_sum, 0) + 1
11
12 return count

prefix_count = {0: 1}: This represents the "empty prefix," a prefix sum of zero that exists once before any element is processed. When the running sum equals k, the complement is zero. If zero is not already in the map, the algorithm misses every subarray that starts at index 0 and sums to k. Without this: on [1, 2, 3] with k = 3, the subarray [1, 2] starting at index 0 has a running sum of 3 and a complement of 0, so the lookup fails and that subarray is never counted.

complement = current_sum - k: This is Two Sum applied to prefix sums. You are asking: "is there an earlier position where the running sum was exactly current_sum minus k?" If so, the subarray between that earlier position and the current position sums to k. The algebra is prefix[j] minus prefix[i] equals k, rearranged to prefix[i] equals prefix[j] minus k.

count += prefix_count.get(complement, 0): Multiple earlier positions can have the same prefix sum, and each one defines a different subarray ending at the current position with sum k. You must count all of them, not just check whether one exists. Without this: on [1, -1, 1, -1, 1] with k = 0, multiple prefix sums collide, and checking only existence misses valid subarrays.

prefix_count[current_sum] = ...: Store the current prefix sum after checking, not before. Same principle as Two Sum. If you store first, you could find the current prefix sum as its own complement when k equals zero. The store after check ordering prevents a position from pairing with itself.

Common Mistake
Why sliding window fails with negatives

Sliding window works by expanding the right boundary to include more elements and contracting the left boundary to exclude them. The key assumption is monotonicity: adding an element always increases the window sum, and removing one always decreases it. When that holds, you can stop expanding once the sum exceeds k and start shrinking instead. The window converges on the right answer.

Negative numbers destroy this assumption. Adding the next element might decrease the sum. Shrinking from the left might increase or decrease it depending on the sign of the removed element. The window loses its monotonic behavior, and you can no longer make any guarantees about when to expand or contract.

Consider nums = [1, -1, 1, 1, 1] with k = 3. After processing the first two elements, the sum is 0. A sliding window would keep expanding, never knowing whether to shrink. The subarray [1, 1, 1] starting at index 2 is the answer, but the window has no way to discover that shrinking past the -1 is the right move.

The prefix sum approach does not depend on monotonicity at all. It depends on complement lookup, which works regardless of the signs in the array. That is why prefix sum plus hash map is the correct tool whenever the array can contain negatives or zeros. The rule: if all elements are guaranteed positive, sliding window works for subarray sum problems. If negatives are possible, use prefix sum plus hash map.

Key Insight
Prefix Sum as Two Sum
Subarray sum = k is Two Sum in disguise. The "elements" are prefix sums. The "target" is k. The "complement" is current_sum - k. The same check-then-store structure applies.
Key Insight
Store Counts, Not Presence
If a problem involves contiguous subarray sums and the array may contain negative numbers, sliding window on the raw values does not work. Use prefix sum plus hash map instead. If the problem says "count subarrays," the map must store counts, not just presence. The {0: 1} initialization is required whenever a subarray starting at index 0 could be a valid answer.

LRU Cache: Hash Map + Linked List

Problem: Design a data structure that supports get(key) and put(key, value) in O(1), evicting the least recently used item when capacity is exceeded.

This problem requires two operations in constant time: looking up a value by key, and evicting the least recently used item. A hash map gives O(1) lookup by key but cannot tell you which entry was accessed least recently without scanning every entry. A linked list can maintain access order directly but cannot find a node by key without walking the list from head to tail.

Even when you already hold a reference to a node in a singly linked list, removing that node is O(n) because you need access to the predecessor, which requires walking from the head. A doubly linked list solves this by storing a prev pointer at every node.

Two Structures, One Goal

Neither data structure alone gives O(1) for both operations. A hash map gives O(1) lookup by key but can't track recency order. A linked list gives O(1) removal/insertion at known positions but can't find nodes by key. Combining them: the hash map stores key to node pointer, and the doubly linked list maintains recency order.

Worked Example

Capacity = 2:

OperationCache State (MRU to LRU)Return
put(1, "A")[1:A]-
put(2, "B")[2:B, 1:A]-
get(1)[1:A, 2:B] (1 moved to front)"A"
put(3, "C")[3:C, 1:A] (2 evicted)-
get(2)[3:C, 1:A] (2 not found)-1
LRU Cache
1class Node:
2 def __init__(self, key=0, val=0):
3 self.key, self.val = key, val
4 self.prev = self.next = None
5
6class LRUCache:
7 def __init__(self, capacity):
8 self.cap = capacity
9 self.cache = {} # key -> Node
10 self.head = Node() # dummy head (MRU side)
11 self.tail = Node() # dummy tail (LRU side)
12 self.head.next = self.tail
13 self.tail.prev = self.head
14
15 def _remove(self, node):
16 # Detach node from its current position in the recency list
17 node.prev.next = node.next
18 node.next.prev = node.prev
19
20 def _add_front(self, node):
21 # Place node right after head, marking it as most recently used
22 node.next = self.head.next
23 node.prev = self.head
24 self.head.next.prev = node
25 self.head.next = node
26
27 def get(self, key):
28 if key not in self.cache:
29 return -1
30 node = self.cache[key]
31 self._remove(node) # Detach from current position
32 self._add_front(node) # Promote to most recent
33 return node.val
34
35 def put(self, key, value):
36 if key in self.cache:
37 self._remove(self.cache[key]) # Remove stale node first
38 node = Node(key, value)
39 self._add_front(node)
40 self.cache[key] = node
41 if len(self.cache) > self.cap:
42 lru = self.tail.prev # Node just before tail is the oldest
43 self._remove(lru)
44 del self.cache[lru.key] # Evict from both structures

self.cache = {}: The hash map stores key to node pointers, not key to value. Storing the actual node reference lets you jump directly to any node in the linked list for O(1) removal. If you stored only the value, finding the node to move would require walking the list, costing O(n).

self.head = Node(); self.tail = Node(): These are dummy sentinel nodes that never hold real data. Their purpose is to eliminate edge cases. With dummies, every real node always has valid prev and next pointers, so _remove and _add_front work uniformly. Without this: you need "if self.head is None" checks in every method, which is tedious and error prone in a live interview.

self._remove(node); self._add_front(node): Accessing a key makes it the most recently used. The node must move from wherever it currently sits in the list to the front. The hash map reference to the node stays valid because the same node object is reused, only its position in the list changes.

del self.cache[lru.key]: Eviction must happen in both structures, not just one. The node stores its own key as an attribute specifically to make this deletion possible. Without the key stored on the node, you would need a reverse lookup from value to key, requiring either another hash map or an O(n) scan. Without this: the dict grows unbounded, stale entries return wrong values, and capacity tracking breaks.

Why Dummy Head and Tail?

Dummy nodes eliminate edge cases. Without them, adding to an empty list or removing the last node requires special handling. With dummies, every real node always has a valid prev and next, so _remove and _add_front work uniformly.

Why Doubly Linked, Not Singly?

Removing a node from the list means rewiring its predecessor's next pointer to skip over it. That requires access to the predecessor. In a singly linked list, the only way to reach a node's predecessor is to walk from the head, which costs O(n) even when you already hold a direct reference to the node itself. A doubly linked list solves this by storing a prev pointer at every node, so removal becomes four pointer reassignments regardless of where the node sits. The hash map gives you O(1) access to the node. The doubly linked list gives you O(1) removal of that node. Neither structure provides both on its own, and that is the entire reason LRU Cache requires the combination.

Common Mistake
Why no single data structure can solve this

The first tempting approach is a hash map with timestamps. Record the access time for each key, and when the cache is full, scan the entire map to find the entry with the oldest timestamp. Lookup is O(1) but eviction requires scanning every entry to find the minimum timestamp. That scan costs O(n), which violates the O(1) requirement.

The second tempting approach is an ordered list or deque. Insert at the front when a key is accessed, evict from the back. This maintains recency order, but finding a key by value requires walking the list from head to tail. That search costs O(n), making get() linear.

The third approach that actually works in Python is OrderedDict, which is a hash map combined with a doubly linked list internally. It gives O(1) for both operations. But interviewers asking this question want you to implement the underlying mechanism, not call a library. They want to see that you understand why the combination is necessary and how the pointer manipulation works.

O(1) random access by key requires a hash based structure, and O(1) order maintenance requires linked list pointers. No single structure provides both, so the combination is the only way to satisfy both requirements.

Interview Tip
Explain the dual structure
When presenting LRU Cache, lead with why you need both structures: "A hash map alone can't track order. A linked list alone can't find by key. I'll combine them: the map gives O(1) access to any node, and the doubly linked list gives O(1) reordering."
Key Insight
Two Structures Are Required
O(1) key access needs a hash map. O(1) order maintenance needs a doubly linked list. No single structure provides both. When you see a problem requiring O(1) for both lookup and reordering, this combination is the standard answer.

Longest Consecutive Sequence

Problem: Given an unsorted array of integers, find the length of the longest consecutive elements sequence. Must run in O(n) time.

Sorting and scanning for consecutive runs gives O(n log n), but the problem requires O(n). A hash set gives O(1) existence checks without requiring sorted order. The naive approach of starting a while loop from every number is O(n squared) because overlapping sequences cause redundant counting. The key is a guard condition that skips numbers that are not the beginning of a sequence, so that each while loop only fires for sequence starts.

Sequence Start Detection

Put all numbers in a set for O(1) lookup. For each number, only start counting a sequence if num - 1 is not in the set, meaning this number is the beginning of a sequence. Then count upward.

Longest Consecutive Sequence
1def longest_consecutive(nums):
2 num_set = set(nums)
3 best = 0
4
5 for num in num_set:
6 if num - 1 not in num_set: # Only start from beginning
7 length = 1
8 while num + length in num_set:
9 length += 1
10 best = max(best, length)
11
12 return best

num_set = set(nums): A set, not a map, because no associated data is needed. The only operation is "is num + 1 in the set?" which is a pure existence check. Using a set instead of a map communicates intent more clearly and is marginally more memory efficient since it does not store values.

if num - 1 not in num_set: This single line is what makes the algorithm O(n) instead of O(n squared). It ensures that the while loop only runs for numbers that are the start of a sequence. If num minus 1 exists in the set, then num is somewhere in the middle or end of a sequence that will be counted when its starting element is processed. Without this: every number triggers its own while loop, and the total work becomes 1 + 2 + ... + n = O(n squared).

while num + length in num_set: Each number in the entire array is visited by exactly one while loop: the one that starts from its sequence's beginning. A number in the middle of a sequence is never the start of a while loop (because its predecessor exists in the set), and it is counted once by the while loop that started from the sequence head. This one to one correspondence is why the total work is O(n).

best = max(best, length): Track the global maximum across all sequences found. Since each sequence is counted exactly once from its start, this comparison fires once per sequence, not once per element.

Why It's O(n)

The while loop only runs for sequence starts. Each number belongs to exactly one sequence and is counted exactly once by the while loop. The outer for loop skips non-starts in O(1). Total work: O(n).

Common Mistake
Why the naive approaches fail

Sorting the array and scanning for consecutive runs gives a correct solution in O(n log n). For many interviews, this is acceptable. But the problem explicitly asks for O(n), and comparison-based sorting cannot achieve that.

The subtler mistake is using a hash set but starting a while loop from every number. You put all numbers in a set, then for each number, expand upward. This produces correct results, but without the sequence start guard, every number triggers its own while loop. Overlapping sequences cause the same numbers to be visited multiple times, and the total work becomes O(n squared).

The sequence start check (num minus 1 not in the set) guarantees that each number is visited by the while loop exactly once, because only the first element of each sequence triggers the loop. It is the mechanism that makes the algorithm linear, not just an optimization.

Key Insight
Sets as Hash Maps Without Values
When you only need existence checks (no associated data), a set is the right tool. Same O(1) lookup as a hash map, but communicates intent more clearly.
Key Insight
Guard conditions for amortized O(n)
If a nested loop appears necessary but the problem requires O(n), look for a guard condition that ensures each element participates in the inner loop at most once across the entire outer loop. The sequence start check in Longest Consecutive Sequence is a textbook example: the while loop fires only for sequence heads, and each number is counted by exactly one while loop.

Two-Pass vs One-Pass

One-Pass Complement Lookup

One-pass works because finding both elements isn't necessary. When processing the second element of a valid pair, the first element is already in the map.

Processing left to right: when reaching element b, element a (if it exists) was already seen and stored. The lookup succeeds, and the pair is found.

For any valid pair, one element comes before the other. When processing the second, the first is already available. Looking into the "future" is never required.

Problems Requiring Two Passes

One-pass breaks when information about unprocessed elements is required.

Example: "Find two elements with the smallest absolute difference."

When processing element a, whether the closest element comes before or after is unknown. The optimal match might be at the end of the array.

If the answer could depend on any other element (not just ones forming a specific relationship like sum = target), two passes (or sorting) is likely required.

When Two-Pass is Required

Two-pass is required when the answer depends on information you can only know after seeing the entire input. First Unique Character illustrates this directly: when you see 'a' at index 0, you have no idea if 'a' appears again later.

1# Same one-pass pattern from Two Sum above
2def two_sum(nums, target):
3 seen = {}
4 for i, num in enumerate(nums):
5 if target - num in seen:
6 return [seen[target - num], i]
7 seen[num] = i
8
9# Two-pass required: need ALL counts before finding first unique
10def first_unique_char(s):
11 freq = {}
12 for c in s:
13 freq[c] = freq.get(c, 0) + 1
14 for i, c in enumerate(s):
15 if freq[c] == 1:
16 return i
17 return -1
Interview Tip
The One Pass Test
Determine whether a match must have come before the current element or could come after. If it must come before (Two Sum: the complement was encountered earlier), one pass works. If it could come after (find closest value), two passes or a different approach is needed.

Hash Maps Inside Other Patterns

Hash maps provide the O(1) state lookup that makes other patterns efficient. Sliding window uses them to track window contents. DFS/BFS uses them to store visited metadata. DP uses them for memoization. In each case, the hash map turns what would be O(n) state queries into O(1) operations.

Hash Map + Sliding Window

Sliding window maintains a "current window" that expands and contracts. The question is: how do you track what's in the window efficiently?

For "longest substring with k distinct characters," the window slides through the string. At each position, the algorithm needs to know: how many distinct characters are currently in the window? Without a hash map, this requires scanning the entire window, which is O(n) per step and O(n²) total. With a frequency map tracking character counts, checking distinct count is O(1).

Longest Substring with K Distinct
1def longest_k_distinct(s, k):
2 freq = {} # char -> count in window
3 left = 0
4 best = 0
5
6 for right, char in enumerate(s):
7 freq[char] = freq.get(char, 0) + 1
8
9 while len(freq) > k: # O(1) to check distinct count
10 left_char = s[left]
11 freq[left_char] -= 1
12 if freq[left_char] == 0:
13 del freq[left_char] # Remove to keep len(freq) accurate
14 left += 1
15
16 best = max(best, right - left + 1)
17
18 return best

The hash map tracks window state. When a character enters, increment its count. When it leaves, decrement. When count hits zero, remove from the map. The map's size is the distinct count, always O(1) to check.

Hash Map + DFS/BFS

Basic graph traversal uses a "visited" set. But many problems need more than just "have I been here?" Clone Graph needs to map original nodes to their copies. Word Ladder needs to track distance from start. Course Schedule needs to track processing state (unvisited, in-progress, done).

When the visited check needs to return more than true/false, upgrade from a set to a map. The key is the node (or state), the value is whatever metadata the algorithm needs.

Clone Graph
1def clone_graph(node):
2 if not node:
3 return None
4
5 cloned = {} # original node -> cloned node
6
7 def dfs(n):
8 if n in cloned:
9 return cloned[n]
10
11 copy = Node(n.val)
12 cloned[n] = copy # Store BEFORE recursing to handle cycles
13
14 for neighbor in n.neighbors:
15 copy.neighbors.append(dfs(neighbor))
16
17 return copy
18
19 return dfs(node)

Notice the critical line: cloned[n] = copy happens before recursing. This handles cycles. If the recursion returns to this node, the clone already exists and gets returned instead of creating duplicates.

Hash Map + Dynamic Programming

Memoization is a hash map pattern at its core. The recursive function's arguments define a state. The hash map caches results for states already computed, preventing redundant work.

Without memoization, recursive solutions re-solve overlapping subproblems exponentially. The hash map collapses exponential time to polynomial by ensuring each state is computed at most once.

Memoization with Hash Map
1def coin_change(coins, amount):
2 memo = {} # state -> result
3
4 def dp(remaining):
5 if remaining == 0:
6 return 0
7 if remaining < 0:
8 return float('inf')
9 if remaining in memo:
10 return memo[remaining] # O(1) lookup
11
12 result = min(dp(remaining - c) for c in coins) + 1
13 memo[remaining] = result # Cache for future calls
14 return result
15
16 ans = dp(amount)
17 return ans if ans != float('inf') else -1

Memoization follows three steps: check cache, compute if missing, store before returning. Python's @lru_cache decorator automates this, but understanding the underlying hash map mechanism is valuable for interviews.

Choosing the Right Key

A good key satisfies two requirements: it produces the same result for items that belong together, and different results for items that don't. Violating the first splits groups incorrectly. Violating the second merges unrelated items.

Consider a key based only on string length: "eat" and "bat" both have length 3, so they'd be grouped together incorrectly. The key is too broad. A key based on the first character: "eat" and "ate" would be split despite being anagrams. The key is too narrow. The right key captures exactly the property that defines equivalence.

Anagrams: Sorted Canonical Form

Two strings are anagrams if they contain the same characters. Sorting removes arbitrary ordering: "eat" and "tea" both become "aet". Key: tuple(sorted(s)). Alternative: character count tuple for O(n) instead of O(n log n).

Isomorphic Strings: Position Pattern

"egg" and "add" are isomorphic (e maps to a, g maps to d). The equivalence property is the pattern of first occurrences. "egg" has pattern (0, 1, 1): first char is new (0), second is new (1), third matches second (1). "add" has the same pattern (0, 1, 1).

Isomorphic String Key
1def encode_pattern(s):
2 mapping = {}
3 pattern = []
4 for c in s:
5 if c not in mapping:
6 mapping[c] = len(mapping)
7 pattern.append(mapping[c])
8 return tuple(pattern)
9
10# "egg" -> (0, 1, 1)
11# "add" -> (0, 1, 1) -> same group
12# "foo" -> (0, 1, 1) -> same group
13# "bar" -> (0, 1, 2) -> different group

Shifted Strings: Difference Pattern

"abc" and "bcd" are shifted (each character +1). The equivalence property is the differences between consecutive characters. "abc" has differences (1, 1). "bcd" also has differences (1, 1). "acf" has differences (2, 3), a different group.

Shifted String Key
1def shift_key(s):
2 if len(s) <= 1:
3 return ()
4 return tuple((ord(s[i+1]) - ord(s[i])) % 26
5 for i in range(len(s) - 1))
6
7# "abc" -> (1, 1)
8# "bcd" -> (1, 1) -> same group
9# "xyz" -> (1, 1) -> same group (wraps around)
10# "acf" -> (2, 3) -> different group
Interview Tip
Finding the Key
When stuck on the key, ask: what property stays the same across equivalent items? For anagrams, character counts. For isomorphic strings, position patterns. For shifted strings, character differences. That shared property, expressed as a hashable value, becomes the key.

Hash Map Recognition Checklist

This checklist summarizes the five hash map problem shapes. Use it after you have seen the patterns in action above. Three diagnostic questions identify whether a problem needs a hash map and which template to use:

  1. What am I repeatedly searching for? This identifies the key.
  2. What information do I need when I find a match? This determines the value.
  3. Is the search happening once per element (single pass) or over subarrays (prefix sum)? This picks the template.

1. Complement Lookup

Can you compute what the matching element must be?

Signal phrases: "find two numbers that sum to," "pair with difference k," "find the matching element."

For each element, you can compute what its partner must be. Instead of searching all other elements for that partner (O(n) per element), store elements as you go and check in O(1). Two Sum is the canonical example, but 4Sum II, pair-with-difference, and continuous subarray sum all use the same structure.

2. Frequency Counting

Does the answer depend on how many times something appears?

Signal phrases: "most frequent," "appears more than k times," "valid anagram," "majority element."

Count everything once in O(n), then answer questions about those counts in O(1). The map stores element to count. Problems like Majority Element, Top K Frequent, and Valid Anagram all follow the same two-phase structure: build the frequency map, then query it.

3. Grouping by Key

Do equivalent items share a computable property?

Signal phrases: "group all X that share property Y," "anagrams," "equivalent strings."

Compute a canonical key from each item that's identical for equivalent items. The map stores key to list of items. The entire challenge is designing the right key: it must be the same for items that belong together and different for items that don't.

4. Seen Before / Deduplication

Does the answer depend on whether something appeared earlier?

Signal phrases: "contains duplicate," "first repeating element," "within distance k."

The answer depends on whether something appeared earlier. Use a set when you only need existence ("have I seen this?"). Upgrade to a map when you need associated data like the index or count of the previous occurrence.

5. Prefix Sum + Hash Map

Are you looking for a contiguous subarray with a target sum?

Signal phrases: "subarray with sum k," "count subarrays," "contiguous subarray divisible by."

If prefix[j] - prefix[i] = k, then the subarray between i and j sums to k. This is a complement lookup on prefix sums. The map stores prefix_sum to count of times that sum has been seen.

Key Insight
Choosing What to Store

The key is what gets looked up. The value is what gets returned when the key is found. The most common mistake is storing the wrong value type.

Value TypeWhen to UseExample
index (first)Need earliest occurrenceFirst Unique Character
index (last)Need most recent occurrenceContains Nearby Duplicate
countNeed frequencyMajority Element, Valid Anagram
list of indicesNeed all occurrences(rare)
node referenceNeed O(1) access to linked structureLRU Cache, Clone Graph
running totalNeed aggregate computationPrefix sum count

Before writing code: "When a match is found, what question needs answering?" That answer is the value.

If you see...Think...KeyValueTemplate
"Find pair that sums to target"Complement lookupelement valueindexComplement Lookup
"Most frequent / count of X"Frequency countingelementcountFrequency Counter
"Group items by shared property"Canonical key designcanonical formlist of itemsGrouping by Key
"Have I seen this before?"Set or map for existenceelementindex or booleanSeen Before
"Subarray with sum / divisible by"Prefix sum + complementprefix sumcount or first indexPrefix Sum + Hash Map
Key Insight
The Diagnosis Process
For any new problem, run the three diagnostic questions in order. (1) What am I repeatedly searching for? If nothing, it may not be a hash map problem. (2) What do I need when I find the match? This picks between a set and a map, and determines the value type. (3) Is the search over individual elements or subarrays? Individual elements point to complement lookup, frequency counting, grouping, or seen-before. Subarrays point to prefix sum + hash map.

Common Pitfalls

Bug 1: KeyError on Missing Keys

Broken code
1freq = {}
2for num in nums:
3 freq[num] += 1 # KeyError on first occurrence

The trap: Accessing a key that doesn't exist throws KeyError. The first time any value is seen, it's not in the dict yet.

Fix: Use .get() with a default, or defaultdict:

1# Option 1: .get() with default
2freq[num] = freq.get(num, 0) + 1
3
4# Option 2: defaultdict
5from collections import defaultdict
6freq = defaultdict(int)
7freq[num] += 1 # No KeyError, defaults to 0

Rule: Never assume a key exists. Always use .get(key, default) or defaultdict.

Bug 2: Modifying Dict While Iterating

Broken code
1for key in freq:
2 if freq[key] == 0:
3 del freq[key] # RuntimeError: dictionary changed size during iteration

The trap: Python dicts cannot change size while being iterated. Deleting or adding keys during a loop raises RuntimeError.

Fix: Collect keys first, then delete:

1# Collect keys to delete, then delete separately
2to_delete = [k for k in freq if freq[k] == 0]
3for k in to_delete:
4 del freq[k]
5
6# Or iterate over a copy
7for key in list(freq.keys()):
8 if freq[key] == 0:
9 del freq[key]

Bug 3: Store Before Check in Two Sum

Broken code
1def two_sum(nums, target):
2 seen = {}
3 for i, num in enumerate(nums):
4 seen[num] = i # Store first
5 if target - num in seen: # Then check
6 return [seen[target - num], i]

The trap: Storing before checking means the current element is already in the map. If target = 6 and num = 3, the complement is also 3. You'll match the element with itself, returning [i, i].

Fix: Check first, then store:

1def two_sum(nums, target):
2 seen = {}
3 for i, num in enumerate(nums):
4 if target - num in seen: # Check first
5 return [seen[target - num], i]
6 seen[num] = i # Then store

Rule: In lookup problems, always check for the complement before adding the current element.

Bug 4: Missing Base Case in Prefix Sum

Broken code
1def subarray_sum(nums, k):
2 prefix_count = {} # Missing {0: 1}
3 total = 0
4 count = 0
5 for num in nums:
6 total += num
7 if total - k in prefix_count:
8 count += prefix_count[total - k]
9 prefix_count[total] = prefix_count.get(total, 0) + 1
10 return count

The trap: When total == k, the complement is total - k = 0. Without {0: 1} in the map, subarrays starting at index 0 are missed.

On [1, 2, 3] with k = 3, this misses [1, 2] (sum 3 starting at index 0) and [3] (element 3 alone).

Fix: Initialize with the base case:

1prefix_count = {0: 1} # "Empty prefix" has sum 0, seen once

Rule: Prefix sum problems need {0: 1} to count subarrays that start at index 0.

Bug 5: Not Deleting Zero-Count Keys

Broken code
1# Tracking distinct characters in sliding window
2freq = {}
3for right, char in enumerate(s):
4 freq[char] = freq.get(char, 0) + 1
5 while len(freq) > k: # Want at most k distinct
6 freq[s[left]] -= 1
7 left += 1 # Forgot to delete when count hits 0

The trap: len(freq) counts all keys, including those with count 0. Characters that left the window still appear as keys, so distinct count is wrong.

Fix: Delete keys when their count reaches 0:

1freq[s[left]] -= 1
2if freq[s[left]] == 0:
3 del freq[s[left]]
4left += 1

Rule: When using dict size to track distinct elements, delete keys at count 0.

Complexity Analysis

OperationAverageWorstWhen Worst Happens
InsertO(1)O(n)All keys hash to same bucket
LookupO(1)O(n)Many collisions
DeleteO(1)O(n)Many collisions
SpaceO(n)O(n)Always linear in stored elements

O(1) amortized. Worst case is O(n) if all keys collide, but Python's dict uses a well-optimized hash table that avoids this in practice.

Interview Tip
If asked about hash collisions
"Hash map operations are O(1) average case. Worst case is O(n) if all keys collide, but with a good hash function this doesn't happen in practice. Python's built-in dict uses a well-optimized hash table. In an interview, I'd state O(1) amortized and mention the theoretical worst case if asked." Most interviewers don't push further. If they do, mention chaining vs open addressing.
Interview Tip
When the interviewer asks for O(1) space
It depends on the problem. For Two Sum, sorting + two pointers uses O(1) extra space but is O(n log n) time. For frequency counting, Boyer-Moore voting finds the majority element in O(1) space. But for problems like Subarray Sum = K, the hash map is essential. There is no known O(1) space, O(n) time solution. Knowing which problems have space-optimal alternatives shows depth.

Practice Problems

Hash map problems cluster into lookup, frequency counting, grouping, and prefix sum patterns. The skill is recognizing which pattern applies and what to use as the key.

Foundational

ProblemDifficultyApproach
Two SumEasyComplement lookup: for each element, compute target minus num and check the map. Check before storing to prevent self-matching. The map stores value to index because the problem asks for positions, not just existence. O(n) time, O(n) space.
Valid AnagramEasyCount character frequencies for the first string, then decrement for the second. If all counts reach zero, they are anagrams. You can also sort both strings and compare, but that costs O(n log n) instead of O(n). Strings of different length are never anagrams, so check that first.
Contains DuplicateEasyAdd each element to a set as you iterate. If the element is already present, return true immediately. A set gives O(1) lookup, making this O(n) time with O(n) space. You can also sort and check adjacent elements for O(n log n) time with O(1) space.
First Unique CharacterEasyTwo passes. First pass counts character frequencies, second pass finds the first character with count 1. You need two passes because you cannot know a character is unique until you have scanned the entire string. O(n) time, O(1) space since the alphabet is fixed at 26 letters.

Frequency Counting

ProblemDifficultyApproach
Majority ElementEasyA hash map counting frequencies works in O(n) time and O(n) space. Boyer-Moore voting gets O(1) space by maintaining a candidate and a count. Increment for matches, decrement for mismatches. When count hits zero, switch to the current element as the new candidate. The majority element always survives because it appears more than n/2 times, so it can never be fully canceled.
Top K Frequent ElementsMediumCount frequencies with a hash map, then extract the top k. Three approaches: sort by frequency for O(n log n), use a min-heap of size k for O(n log k), or bucket sort by frequency for O(n). Bucket sort works here because the maximum possible frequency is n. Create n+1 buckets indexed by count and collect elements starting from the highest bucket downward.
Sort Characters By FrequencyMediumCount character frequencies, then rebuild the string with characters ordered by count from highest to lowest. The bucket sort approach groups characters by their frequency and iterates from the top bucket down, giving O(n) time. Any ordering among characters with equal frequency is valid, so ties do not matter.

Grouping

ProblemDifficultyApproach
Group AnagramsMediumThe key must produce the same value for all anagrams and different values for non-anagrams. Sorted characters work: tuple(sorted(s)) maps all anagrams to an identical tuple. A character count tuple is O(n) per string instead of O(n log n) for sorting. Use defaultdict(list) to collect groups.
Isomorphic StringsEasyTwo strings are isomorphic if there is a consistent bidirectional mapping between their characters. Track two maps: one from s chars to t chars, one from t chars to s chars. If a mapping conflict appears in either direction, return false. You can also encode both strings as position-of-first-occurrence patterns and compare directly. "egg" and "add" both produce (0, 1, 1).

Prefix Sum + Hash Map

ProblemDifficultyApproach
Subarray Sum Equals KMediumPrefix sum complement lookup. Store prefix sum counts (not just existence) because multiple earlier positions can produce the same prefix sum, and each one defines a different valid subarray. Initialize with {0: 1} so subarrays starting at index 0 are counted. Check then store ordering prevents a position from pairing with itself.
Continuous Subarray SumMediumSubarray sum divisible by k means two prefix sums share the same remainder mod k. Store each remainder's first index (not count) because you need the earliest occurrence to maximize subarray length. The subarray must have length at least 2, so check that current index minus stored index is greater than 1. Initialize with {0: -1}.
Contiguous ArrayMediumTreat 0 as -1. Equal counts of 0s and 1s means the transformed subarray sums to zero. Store each running sum's first occurrence index. When the same sum appears again, the subarray between those positions has equal 0s and 1s. Track the maximum length across all repeated sums. Initialize with {0: -1} so subarrays starting at index 0 are caught.

Advanced

ProblemDifficultyApproach
LRU CacheMediumHash map stores key to node pointer for O(1) lookup. Doubly linked list maintains recency order for O(1) reordering and eviction. Sentinel head and tail nodes eliminate edge cases so every real node always has valid prev and next pointers. When evicting, remove from both structures. The node stores its own key so the hash map entry can be deleted during eviction.
Longest Consecutive SequenceMediumPut all numbers in a set. Only start counting from sequence beginnings where num minus 1 is not in the set. This guard makes the algorithm O(n) despite the nested while loop, because each number is visited by exactly one while loop across the entire outer iteration.
4Sum IIMediumSplit four arrays into two groups. Compute all A[i]+B[j] sums and store their counts in a map. For each C[k]+D[l] pair, look up the negation in the map. This turns O(n^4) brute force into O(n^2) time and O(n^2) space. The map stores sum to count because multiple (i,j) pairs can produce the same sum.

Reference Templates

Use when you can compute what the matching element must be.

Complement Lookup
1def complement_lookup(nums, target):
2 seen = {}
3 for i, num in enumerate(nums):
4 complement = target - num
5 if complement in seen:
6 return [seen[complement], i]
7 seen[num] = i
8 return []

Use when the answer depends on how many times elements appear.

Frequency Counting
1from collections import Counter
2
3def frequency_pattern(nums):
4 freq = Counter(nums)
5 for num, count in freq.items():
6 if count > threshold:
7 return num

Use when equivalent items share a computable canonical form.

Grouping by Key
1from collections import defaultdict
2
3def group_by_key(items):
4 groups = defaultdict(list)
5 for item in items:
6 key = compute_key(item)
7 groups[key].append(item)
8 return list(groups.values())

Use when counting subarrays with a target sum, especially with negative values.

Prefix Sum + Hash Map
1def prefix_sum_pattern(nums, k):
2 prefix_count = {0: 1}
3 current_sum = 0
4 count = 0
5 for num in nums:
6 current_sum += num
7 count += prefix_count.get(current_sum - k, 0)
8 prefix_count[current_sum] = prefix_count.get(current_sum, 0) + 1
9 return count

Use when a sliding window needs to track distinct elements or character frequencies.

Sliding Window State
1def sliding_window_state(s, k):
2 freq = {}
3 left = 0
4 best = 0
5 for right, char in enumerate(s):
6 freq[char] = freq.get(char, 0) + 1
7 while len(freq) > k:
8 freq[s[left]] -= 1
9 if freq[s[left]] == 0:
10 del freq[s[left]]
11 left += 1
12 best = max(best, right - left + 1)
13 return best

Pattern Recap

Replacing the inner loop

Complement lookup stores seen values so each element's partner can be found in O(1) instead of scanning the rest of the array. One pass works because for any valid pair, whichever element you reach second will find the first already stored.

Check-then-store ordering prevents an element from matching itself when the complement equals the element's own value.

Counting and querying

Frequency counting builds a complete count map before answering questions about the distribution. Two phases are necessary because frequency questions cannot be answered until all data has been seen.

One-pass counting works only when the query does not depend on unprocessed elements. First Unique Character requires two passes because uniqueness depends on the full count.

Grouping by equivalence

A canonical key maps equivalent items to the same bucket. The key must capture exactly the equivalence property and nothing more.

A key that is too broad merges unrelated items (string length for anagrams). A key that is too narrow splits equivalent items (first character for anagrams).

Prefix sum complement lookup

Subarray sum becomes subtraction of two prefix sums. The hash map finds matching earlier prefix sums in O(1), turning subarray sum problems into the same complement structure as Two Sum.

The {0: 1} base case is required for subarrays starting at index 0. Omitting it is the most common prefix sum bug.

Tracking window state

A frequency map summarizes the current window. Increment on entry, decrement on exit, delete at zero to keep the map's size equal to the true distinct element count.

Forgetting to delete zero-count keys corrupts the distinct element count and breaks validity checks that depend on len(freq).

If the problem does not fit one of these five forms, the hash map is likely serving as auxiliary state inside another pattern (sliding window, DFS/BFS, dynamic programming) rather than being the primary technique.

Rebuilding the Pattern

When a hash map problem feels unclear, start by identifying the repeated search. The brute force almost always has an inner loop or a nested scan that checks the same kind of question on every iteration. That repeated question is the hash map target: store the answers so the question costs O(1) instead of O(n).

The next decision is what to store as the key and what to store as the value. The key is what gets looked up. The value is what gets returned when the lookup succeeds. If the problem asks for indices, store indices. If it asks for counts, store counts. If it only asks whether something exists, a set may be sufficient and communicates that intent more clearly than a map.

If the problem involves subarray sums, the repeated search is over prefix sums, not raw array values. The complement is current_sum minus k. The {0: 1} base case accounts for subarrays that start at index 0. If the problem says "count subarrays," the map must store how many times each prefix sum has appeared, not just whether it has appeared.

The pattern is recoverable from three decisions: what repeated search to eliminate, what to store as key and value, and whether the lookup is over individual elements or prefix sums. Once those decisions are clear, the code follows the same check-then-store structure regardless of which specific problem you are solving.

Check Your Understanding

Use these questions to check whether you can reason through the pattern without looking at the template.

Take Assessment

12 questions covering all hash map patterns.

Practice this pattern

Apply the guide to complete interview problems with explanations and code.

Learn Hash Maps in a guided sequence

The Interview Course connects this pattern to its prerequisites, worked lessons, and progressively harder problems.

Explore the course