Skip to content

Day 6: Hash Maps & Sets

DAY 6 / 30 · WEEK 1

  • Use Map and Set fluently for average O(1) lookup, insert, and delete.
  • Recognize the frequency-counting pattern: “have I seen this?” / “how many times does X appear?”
  • Turn an O(n²) nested-loop search into O(n) by trading space for time.

A hash map is like a coat check counter with numbered tickets. Without it, finding your coat means checking every coat on the rack one by one — O(n). With a ticket number, the attendant jumps straight to your coat — O(1) on average. A hash map does this by running each key through a hash function that turns it into a number, then uses that number to jump directly to a storage slot, instead of scanning.

This is the biggest lever you’ll pull all month: almost any “have I seen this before?” or “how many times does X appear?” question can go from an O(n²) nested loop (checking every element against every other element) to a single O(n) pass, by remembering what you’ve seen in a Map or Set as you go — at the cost of O(n) extra memory. That trade — memory for speed — is the single most common move in interview solutions.

Is 7 in this collection?3917← found (4 checks)Array: check element 1, 2, 3, 4… up to n checks in the worst case. O(n).hash(7)7← found (1 check) Scanning “abcb” once, building a count as we goabcb← seen before, count++Map after the scan:{ a: 1, b: 2, c: 1 }One pass, O(1) work per character — O(n) total, instead of aseparate counting pass for every distinct character.

Running example: nums = [3, 2, 4], target = 6

Press Play or Step to begin.

map entries: 0

Step 0 / 4

Running example: s = "swiss"

Press Play or Step to begin.

pass: 1 · character: —

Step 0 / 3

// --- Two Sum, the O(n) way (Day 2 showed the O(n²) brute force) ---
function twoSum(nums, target) {
const seen = new Map(); // value -> index
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (seen.has(complement)) return [seen.get(complement), i];
seen.set(nums[i], i);
}
return [-1, -1];
}
// --- Frequency counting ---
function firstUniqChar(s) {
const counts = new Map();
for (const ch of s) counts.set(ch, (counts.get(ch) || 0) + 1);
for (let i = 0; i < s.length; i++) {
if (counts.get(s[i]) === 1) return i;
}
return -1;
}

Line by line:

  • twoSum — for each number, first check whether its complement (what it would need to reach the target) is already in the map. Only if not, add the current number. This single pass replaces the nested loop entirely — no sorting needed, unlike Day 4’s two-pointer version.
  • firstUniqChar — two passes, both O(n): the first builds a full frequency count; the second finds the first character whose count is exactly 1. You can’t combine these into one pass, because a character’s final count isn’t known until the whole string has been scanned.
i nums[i] complement In map? Action
0 3 3 No seen.set(3, 0){3: 0}
1 2 4 No seen.set(2, 1){3: 0, 2: 1}
2 4 2 Yes (index 1) return [1, 2]

Found in one pass, no sorting, works even though the array isn’t sorted — the exact case Day 4’s two-pointer version couldn’t handle directly.

Operation Time Space Why
map.get / set / has O(1) average, O(n) worst case O(1) per call Worst case: every key collides into the same slot, degrading to a linear scan.
Two Sum, brute force (Day 2) O(n²) O(1) Check every pair.
Two Sum, hash map (today) O(n) O(n) One entry per number seen so far — genuinely unbounded, since numbers can be arbitrary.
Frequency counting a string/array O(n) O(min(n, alphabet size)) One count per distinct element. For a fixed small alphabet (e.g. lowercase English letters, 26 possible keys) this is really O(1) space, not O(n) — it only grows with n when the value range itself is unbounded.
  • Map vs. a plain object — when do you use which? Prefer Map by default: any value can be a key (not just strings), it preserves insertion order reliably, has a real .size, and has no prototype to accidentally collide with (a key literally named "toString" is safe in a Map, not in a plain object).

  • Why is hash lookup “O(1) average” instead of just “O(1)”? Different keys can hash to the same slot (a collision). A well-implemented hash map handles this gracefully and stays close to O(1) in practice, but a pathological set of keys can degrade it toward O(n).

    • ↳ Common follow-up: “How would you handle collisions yourself?” — chaining (each slot holds a small list) or open addressing (probe for the next free slot) are the two classic answers.
  • What’s the classic trap with frequency-counting problems? Trying to do it in one pass when the answer genuinely depends on the final count of something (like “first unique character”) — you can’t know a character is unique until you’ve seen the whole string.

    • Trap: don’t fight this — two clean O(n) passes is still O(n) overall, and far less buggy than a contorted attempt at one pass.

How to talk through it out loud: name the trade explicitly — “I’ll use a hash map to remember what I’ve seen, which costs O(n) extra space but drops the time from O(n²) to O(n).” Interviewers want to hear that you know it’s a trade, not a free win — mentioning the space cost unprompted is a strong signal.

EasyLeetCode: Two Sum

Given an array of integers and a target, return the indices of the two numbers that add up to the target. (You wrote the O(n²) brute force for this on Day 2 — now write the O(n) version.)

Show hint

For each number, ask “have I already seen the number that would complete this pair?” before adding the current number to the map.

Show solution
function twoSum(nums, target) {
const seen = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (seen.has(complement)) return [seen.get(complement), i];
seen.set(nums[i], i);
}
return [-1, -1];
}

EasyLeetCode: Valid Anagram

Given two strings, return true if the second is an anagram of the first (same letters, same counts, any order).

Show hint

Build a frequency count of the first string, then walk the second string subtracting from those counts. If anything goes negative, or the lengths differ, they’re not anagrams.

Show solution
function isAnagram(s, t) {
if (s.length !== t.length) return false;
const counts = new Map();
for (const ch of s) counts.set(ch, (counts.get(ch) || 0) + 1);
for (const ch of t) {
if (!counts.get(ch)) return false; // missing, or already at 0
counts.set(ch, counts.get(ch) - 1);
}
return true;
}

MediumLeetCode: Group Anagrams

Given an array of strings, group the anagrams together (any order).

Show hint

Anagrams share the same sorted letters. Use that sorted string as a map key, and group original words under it.

Show solution
function groupAnagrams(strs) {
const groups = new Map();
for (const word of strs) {
const key = word.split('').sort().join('');
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(word);
}
return [...groups.values()];
}