Hash Maps and Sets
Trade memory for time: remember what you have seen so each lookup is O(1) instead of another scan.
- O(n)Time: one pass instead of O(n²)
- O(n)Space: the map holds the elements seen so far
- O(1) averagePer lookup; O(n) if many keys collide
Show what to say
“When a problem asks for a pair, a complement or a count in unsorted data, I reach for a hash map: I remember what I have seen, look up before I insert, and one O(n) pass replaces the nested loop. I pay for it with O(n) memory.”
Trade-offs
- Hash map vs sorting: O(n) expected time and O(n) memory, against O(n log n) with little memory and a predictable worst case.
- Sorted-letters key vs count signature for anagrams: O(k log k) per word against O(k), but the signature needs a separator between counts.
- Map vs array of 26: for lowercase letters the array is faster and O(1) space; a Map handles any key.
Bring it up before they ask
Look up before you insert, or an element pairs with itself ([3, 4] with target 6 returns [0, 0]), and use has() rather than a truthy get(), because a stored index can be 0.
A hash map is a notebook of what you have seen. One look costs one step, so a nested loop becomes one pass: O(n) time, O(n) memory.
When to reach for it
Tap the signals you can see in the problem.
Rememberhave I seen this before?how many times?partnersearchesgrouped
- Seen set
Rememberswhich values appeared
contains duplicate - Frequency map
Remembersvalue → count
valid anagramtop k frequent - Complement map
Remembersvalue → index or count
two sumsubarray sum equals k - Group by key
Rememberskey → members
group anagrams
The core idea: replace the inner search with a lookup
Think of a book with edge tabs.
Read more · 94 words
Hunting for a chapter page by page is slow. Flipping to its tab is instant. A hash map is the tab.
A brute force scans the other elements for a match, again and again. Instead, write down what each element tells you as you pass. Later elements ask the notebook, not the list.
One rule: look up first, then write down. An element can't pair with itself, and every pair is found at its second element. Watch a set catch the first repeat:
The price is O(n) memory. "O(1)" means on average.
Seen set: find the first value that repeats
- x
- null
The set starts empty. It will hold every value already passed, so asking whether x appeared earlier is one O(1) lookup instead of a scan.
function firstRepeat(nums) {
const seen = new Set();
for (const x of nums) {
if (seen.has(x)) return x;
seen.add(x);
}
return null;
}All steps as text
- The set starts empty. It will hold every value already passed, so asking whether x appeared earlier is one O(1) lookup instead of a scan.
- Is 4 in seen? No, so this is the first time we meet 4.
- Add 4. The set now holds the one value passed so far.
- Is 7 in seen? No, so this is the first time we meet 7.
- Add 7. The set now holds every value at indices 0 to 1.
- Is 2 in seen? No, so this is the first time we meet 2.
- Add 2. The set now holds every value at indices 0 to 2.
- Is 9 in seen? No, so this is the first time we meet 9.
- Add 9. The set now holds every value at indices 0 to 3.
- Is 7 in seen? Yes: it was added at index 1. So 7 is the first value to appear twice, found without ever scanning backwards.
Play with it
Two sum on a real hash table. Drag Buckets down to 1 and watch the comparisons climb toward the nested loop's.
Read more · 72 words
The recipe:
Find the inner search in the brute force
"An earlier
target - x" is the map's question.Choose what to remember
Presence:
Set. Count or index: value → count or index. Group: key → list.Choose the key
Usually the value, or a canonical form like sorted letters.
Pick the order
Pairing with earlier elements: look up before inserting.
Read off the answer
Return on the first hit, or rescan when order matters.
Templates
Template 1: seen set
When only presence matters, a Set is enough.
The same lines detect duplicates or visited nodes. Longest Consecutive SequenceMedium starts counting only where set.has(x - 1) is false.
// Template 1: a Set of everything already passed. Answers "have I seen this before?" in O(1) on average,
// so the inner "search the earlier elements" loop of the brute force disappears.
export function firstRepeat(items) {
const seen = new Set();
for (const x of items) {
if (seen.has(x)) return x; // second sighting: the first value to repeat
seen.add(x);
}
return null; // every value was distinct
}Template 2: frequency map
A frequency map answers "how many?". It is also a budget: spend counts until one runs out.
Valid AnagramEasy counts one word up and the other down.
Frequency map: count the supply, then spend it
- ch
- null
Each magazine letter can be used once. First count how many copies of each letter the magazine supplies.
function canConstruct(note, magazine) {
const count = new Map();
for (const ch of magazine) {
count.set(ch, (count.get(ch) ?? 0) + 1);
}
for (const ch of note) {
const left = count.get(ch) ?? 0;
if (left === 0) return false;
count.set(ch, left - 1);
}
return true;
}All steps as text
- Each magazine letter can be used once. First count how many copies of each letter the magazine supplies.
- Magazine letter 'o': its count becomes 1.
- Magazine letter 'n': its count becomes 1.
- Magazine letter 'i': its count becomes 1.
- Magazine letter 'o': its count becomes 2.
- Magazine letter 'n': its count becomes 2.
- The note needs 'n'. The map says 2 copies are still available: one O(1) lookup, no searching through the magazine.
- Spend one 'n': 1 left.
- The note needs 'o'. The map says 2 copies are still available: one O(1) lookup, no searching through the magazine.
- Spend one 'o': 1 left.
- The note needs 'o'. The map says 1 copy is still available: one O(1) lookup, no searching through the magazine.
- Spend one 'o': 0 left.
- The note needs 'n'. The map says 1 copy is still available: one O(1) lookup, no searching through the magazine.
- Spend one 'n': 0 left.
- Every letter of the note was paid for, so return true. Two linear passes: O(m + n) time.
// Template 2: a frequency map. Count once, then answer "how many?" for any value in O(1).
export function countAll(items) {
const count = new Map();
for (const x of items) count.set(x, (count.get(x) ?? 0) + 1);
return count;
}
// Counting as a budget: can `note` be written using each letter of `magazine` at most once?
export function canConstruct(note, magazine) {
const count = countAll(magazine);
for (const ch of note) {
const left = count.get(ch) ?? 0;
if (left === 0) return false; // this letter has run out
count.set(ch, left - 1); // spend one copy
}
return true;
}Template 3: complement lookup
Two numbers must add up to a target?
Read more · 45 words
Fix the second and ask the notebook for the first. Stored counts handle repeats: each x adds one pair per earlier copy of target - x.
Two SumEasy stores value → index and returns at the first hit. Store running totals instead and you are in Prefix Sums.
Complement lookup: count pairs with a target sum
- target
- 6
- pairs
- 0
For each x, its partner must be target - x. The map remembers how many times each value has appeared before the current index.
function countPairs(nums, target) {
const seen = new Map(); // value → times seen so far
let pairs = 0;
for (const x of nums) {
pairs += seen.get(target - x) ?? 0;
seen.set(x, (seen.get(x) ?? 0) + 1);
}
return pairs;
}All steps as text
- For each x, its partner must be target - x. The map remembers how many times each value has appeared before the current index.
- 1 needs 5, which hasn't appeared yet: no new pairs. If 5 shows up later, it will find 1 then.
- Only now record 1: count 1. Later values can pair with it.
- 5 needs 1. The map has seen 1 once, so 5 completes 1 new pair: pairs = 1.
- Only now record 5: count 1. Later values can pair with it.
- 3 needs another 3. The current 3 is not in the map yet, so it can't pair with itself: no new pairs.
- Only now record 3: count 1. Later values can pair with it.
- 3 needs 3. The map has seen 3 once, so 3 completes 1 new pair: pairs = 2.
- Only now record 3: count 2. Later values can pair with it.
- 3 needs 3. The map has seen 3 twice, so 3 completes 2 new pairs: pairs = 4.
- Only now record 3: count 3. Later values can pair with it.
- 7 needs -1, which hasn't appeared yet: no new pairs. If -1 shows up later, it will find 7 then.
- Only now record 7: count 1. Later values can pair with it.
- Every pair was counted exactly once, when its second element was reached. Return 4.
// Template 3: complement lookup. For each x, the partner it needs is target - x: look it up among the values
// already passed, THEN record x. Looking up first means x never pairs with itself.
export function countPairs(nums, target) {
const seen = new Map(); // value → how many times it has appeared so far
let pairs = 0;
for (const x of nums) {
pairs += seen.get(target - x) ?? 0; // x pairs with every earlier copy of its complement
seen.set(x, (seen.get(x) ?? 0) + 1);
}
return pairs;
}Template 4: group by a canonical key
Two items get the same key exactly when they belong together.
Read more · 36 words
| To group… | Key | Cost per item |
|---|---|---|
| anagrams | letters sorted | O(k log k) |
| anagrams, long words | 26 counts joined | O(k) |
| grid cells, points | `${r},${c}` | O(1) |
Group AnagramsMedium is this template plus the right key.
// Template 4: group by a canonical key. Items that should end up together must map to the same key:
// a length, a sorted string, a count signature, an encoded coordinate…
export function groupBy(items, keyOf) {
const groups = new Map(); // key → items with that key, in input order
for (const item of items) {
const key = keyOf(item);
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(item);
}
return [...groups.values()];
}Try it
// Edit the inputs and press Run. These are the four templates on this page.
console.log(firstRepeat([4, 7, 2, 9, 7, 1])); // 7
console.log(countAll('banana')); // Map(3) { 'b' => 1, 'a' => 3, 'n' => 2 }
console.log(canConstruct('tooth', 'photo')); // false: only one t to spend
console.log(countPairs([1, 5, 3, 3, 3, 7], 6)); // 4
const sorted = (w) => [...w].sort().join('');
console.log(groupBy(['tab', 'cat', 'bat', 'act'], sorted)); // [['tab', 'bat'], ['cat', 'act']]Complexity
Read more · 40 words
| Template | Time | Extra space |
|---|---|---|
| Seen set, frequency map | O(n) | O(n) |
| Complement lookup | O(n) | O(n) |
| Group by sorted key | O(n · k log k) | O(n · k) |
| Group by count signature | O(n · k) | O(n · k) |
JavaScript you'll use
JavaScript refresher: Map and Set Objects as records and dictionaries Strings and charactersRead more · 41 words
Arrays are keys by reference: seen.has([1, 2]) is false after adding [1, 2]. Use strings.
count.set(x, (count.get(x) ?? 0) + 1); // increment, starting from 0
if (seen.has(need)) return [seen.get(need), i]; // has(), not get(): an index can be 0
const counts = new Array(26).fill(0);
counts[ch.charCodeAt(0) - 97]++; // letter counter without a Map
const key = [...word].sort().join(''); // canonical key for anagrams
return [...groups.values()]; // Map values → array of groupsInterview questions
Hash map or sorting: which would you pick?
Hashing: O(n) time, O(n) memory. Sorting: O(n log n), little memory. Pick sorting when memory is tight.
What is the worst case of a hash map lookup?
O(n) when many keys collide. Say "O(1) on average" unless asked about attackers.
How do you group anagrams when the words are very long?
Use 26 counts joined with a separator (1#0#2#…) as the key: O(k). Without the separator, 1,11 and 11,1 look the same.
The data doesn't fit in memory. Now what?
Split it by hash into files that fit, then map each file. For estimates, use a or .
Check yourself
Question 1 of 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Contains DuplicateDecide whether any value appears more than once in an array.Easy
- Two SumFind the two positions in an unsorted array whose values add up to a target, in one pass.Easy
- Valid AnagramDecide whether two words use exactly the same letters the same number of times.Easy
- First Unique CharacterReturn the index of the first character that appears exactly once in a string.Easy
- Group AnagramsPut words that are anagrams of each other into the same group.Medium
- Longest Consecutive SequenceFind the length of the longest run of consecutive integers in an unsorted array, in O(n) time.Medium
- Encode and Decode StringsPack a list of strings into one string and recover the exact list, whatever characters the strings contain.Medium
- Subarray Sum Equals KCount the contiguous subarrays whose sum is exactly k, when the array may contain negative numbers.Medium
- Happy NumberRepeatedly replace a number by the sum of the squares of its digits and decide whether it reaches 1 or loops forever.Easy
- Top K Frequent ElementsReturn the k values that appear most often in an array, in any order.Medium
- Single NumberEvery number appears twice except one. Find the loner in one pass with O(1) extra space.Easy
- Missing NumberAn array holds n distinct numbers from the range 0 to n. Find the one that is missing.Easy
- Design HashMapImplement a map from integer keys to integer values using only arrays and your own hashing, with no built-in map or object.Easy
- Insert Delete GetRandom O(1)Build a collection that adds, removes and returns a uniformly random member, each in constant time.Medium
- LRU CacheBuild a fixed-size cache that throws out the entry nobody has touched for the longest time, with O(1) reads and writes.Medium
- LFU CacheBuild a fixed-size cache that evicts the entry used the fewest times, breaking ties by the oldest use, in O(1) per call.Hard
- Valid SudokuCheck that the filled cells of a 9 x 9 puzzle break no row, column or 3 x 3 box rule.Medium
In real systems
This pattern is not only for coding rounds. Here is where it does the work in a design interview.
- Sharding StrategiesHash sharding is one hash and one modulo to pick a shard: the O(1) lookup idea, spread across machines.
- Caching TechniquesA cache is a hash map with an eviction policy and a freshness problem.
- Design a Distributed CacheA distributed cache is a hash map spread over machines, and rendezvous hashing is the hash that picks the node for a key.