Pattern 1 of 24 · Arrays & strings · 25 min

Hash Maps and Sets

Trade memory for time: remember what you have seen so each lookup is O(1) instead of another scan.

Spot it:have I seen this before?count occurrencesfind a pair or complementgroup by a keyJavaScript you'll use: Map and Set Objects as records and dictionaries Strings and characters
Say it in 30 seconds
  • 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.

0 of 0 sections ticked
30-second answer

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

Spot it in the problem0 of 3

Tap the signals you can see in the problem.

Rememberhave I seen this before?how many times?partnersearchesgrouped

Pick your variant
  • 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.

A database index is a book's edge tabs: jump straight to the section instead of reading every page.

Seen set: find the first value that repeats

nums (6)
4
7
2
9
7
1
seen (size 0)
empty
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
  1. 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.
  2. Is 4 in seen? No, so this is the first time we meet 4.
  3. Add 4. The set now holds the one value passed so far.
  4. Is 7 in seen? No, so this is the first time we meet 7.
  5. Add 7. The set now holds every value at indices 0 to 1.
  6. Is 2 in seen? No, so this is the first time we meet 2.
  7. Add 2. The set now holds every value at indices 0 to 2.
  8. Is 9 in seen? No, so this is the first time we meet 9.
  9. Add 9. The set now holds every value at indices 0 to 3.
  10. 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:

  1. Find the inner search in the brute force

    "An earlier target - x" is the map's question.

  2. Choose what to remember

    Presence: Set. Count or index: value → count or index. Group: key → list.

  3. Choose the key

    Usually the value, or a canonical form like sorted letters.

  4. Pick the order

    Pairing with earlier elements: look up before inserting.

  5. Read off the answer

    Return on the first hit, or rescan when order matters.

Interactive

Two-sum detective

Each number asks the map for its partner, then drops into a bucket.

nums · target 90 lookups · 0 compared · nested loops 213081112431547526bucketchain: value→index01234563→08→111→24→315→47→52→6bucket = value mod 7

Target 9. Each number asks the map for its partner (target − number), then moves in. Press Run or Step.

Your numbers

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

magazine (5)
o
n
i
o
n
note (4)
n
o
o
n
count (size 0)
empty
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
  1. Each magazine letter can be used once. First count how many copies of each letter the magazine supplies.
  2. Magazine letter 'o': its count becomes 1.
  3. Magazine letter 'n': its count becomes 1.
  4. Magazine letter 'i': its count becomes 1.
  5. Magazine letter 'o': its count becomes 2.
  6. Magazine letter 'n': its count becomes 2.
  7. The note needs 'n'. The map says 2 copies are still available: one O(1) lookup, no searching through the magazine.
  8. Spend one 'n': 1 left.
  9. The note needs 'o'. The map says 2 copies are still available: one O(1) lookup, no searching through the magazine.
  10. Spend one 'o': 1 left.
  11. The note needs 'o'. The map says 1 copy is still available: one O(1) lookup, no searching through the magazine.
  12. Spend one 'o': 0 left.
  13. The note needs 'n'. The map says 1 copy is still available: one O(1) lookup, no searching through the magazine.
  14. Spend one 'n': 0 left.
  15. 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

nums (6)
1
5
3
3
3
7
seen (size 0)
empty
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
  1. For each x, its partner must be target - x. The map remembers how many times each value has appeared before the current index.
  2. 1 needs 5, which hasn't appeared yet: no new pairs. If 5 shows up later, it will find 1 then.
  3. Only now record 1: count 1. Later values can pair with it.
  4. 5 needs 1. The map has seen 1 once, so 5 completes 1 new pair: pairs = 1.
  5. Only now record 5: count 1. Later values can pair with it.
  6. 3 needs another 3. The current 3 is not in the map yet, so it can't pair with itself: no new pairs.
  7. Only now record 3: count 1. Later values can pair with it.
  8. 3 needs 3. The map has seen 3 once, so 3 completes 1 new pair: pairs = 2.
  9. Only now record 3: count 2. Later values can pair with it.
  10. 3 needs 3. The map has seen 3 twice, so 3 completes 2 new pairs: pairs = 4.
  11. Only now record 3: count 3. Later values can pair with it.
  12. 7 needs -1, which hasn't appeared yet: no new pairs. If -1 shows up later, it will find 7 then.
  13. Only now record 7: count 1. Later values can pair with it.
  14. 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…KeyCost per item
anagramsletters sortedO(k log k)
anagrams, long words26 counts joinedO(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

try-it.js
// 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
TemplateTimeExtra space
Seen set, frequency mapO(n)O(n)
Complement lookupO(n)O(n)
Group by sorted keyO(n · k log k)O(n · k)
Group by count signatureO(n · k)O(n · k)

JavaScript you'll use

JavaScript refresher: Map and Set Objects as records and dictionaries Strings and characters
Read 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 groups

Interview 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 3

Score 2 out of 3 or better to mark this page complete.

1. In one-pass Two Sum, why do you look up target - nums[i] before storing nums[i] in the map?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

In real systems

This pattern is not only for coding rounds. Here is where it does the work in a design interview.

Next: When the input is sorted, you can often drop the hash map and walk two pointers instead.