Instead of searching the array again and again, walk it once and write down what you need in a Map or Set. Then every "have I seen this?" or "how many so far?" question takes one step.
Think of it like this: a coat check. You hand over your coat and get a ticket. Later the attendant goes straight to that ticket number. They don't look through every coat on the rail.
Why it matters
Many first solutions use two nested loops: for each item, look through all the other items. With 10 items that's 100 checks. With 100,000 items it's 10 billion checks, which takes minutes.
A hash map is a structure that finds a value by its key in roughly one step, however many items it holds. In JavaScript that's Map (key → value) and Set (keys only). "Hash" refers to how they work inside. The key is turned into a number that says where to store it, so there's no searching. The Arrays, Maps and Sets lesson in Foundations covers their methods.
So the trade is: spend O(n) memory on a Map to save an entire inner loop of time.
Three jobs a map does
Almost every hashing problem uses a Map or Set for one of these:
- Seen before? A
Setof values you've passed. "Is there a duplicate?", "Have I already visited this?" - Count. A
Mapfrom value to how many times it appeared. "Most frequent", "same letters", "appears exactly once". - Remember something about it. A
Mapfrom value to an index, or from a key to a list of items. "Where did I see it?", "group items that share a property".
Before writing code, decide which job you need and what the key and value are. Saying "key: the word, value: how many times I've seen it" out loud is half the solution.
How to spot it
Words and constraints in a problem that point to this pattern:
- “Find two numbers that add up to…”, or any “does a matching item exist?” question
- Counting: “how many times”, “most frequent”, “appears exactly once”, “same letters in a different order”
- “Duplicate”, “seen before”, “unique”, “first to repeat”
- “Group the items that share…”
- Your first idea is a nested loop where the inner loop searches for one specific value
Watch it work
A page-view log: ["home", "cart", "home", "shop", "home", "cart"]. Which page was viewed most? We walk the log once and keep a count per page in a Map. Then we scan the Map for the biggest count. The Map is much smaller than the log.
The template
Two shapes cover most hashing problems. Seen before: one loop, look up first, then store. Count: one loop to build counts, then use them.
// Shape 1: "seen before": one pass, check, then remember
function seenBefore(items: number[]): boolean {
const seen = new Set<number>(); // values we've already passed
for (const x of items) { // visit each item once
const wanted = /* the value that would complete the answer for x */ x;
if (seen.has(wanted)) { // O(1) question instead of an inner loop
return true; // or build the answer you need here
}
seen.add(x); // remember x AFTER the check
}
return false; // walked everything, no match
}
// Shape 2: "count": build a frequency map, then read it
function countThings(items: string[]): Map<string, number> {
const counts = new Map<string, number>(); // key: the item, value: how many times
for (const x of items) {
counts.set(x, (counts.get(x) ?? 0) + 1); // missing key counts as 0
}
return counts; // second pass reads these counts
}
Worked example
Problem: given a string, return the index of the first character that appears only once in the whole string, or -1 if there isn't one. For "swiss" the answer is 1 (w).
Start with the direct idea: for each character, scan the whole string and count how many times it appears. Return the first one whose count is 1. That's a loop inside a loop: O(n²).
for (let i = 0; i < s.length; i++) { let count = 0; for (let j = 0; j < s.length; j++) { if (s[j] === s[i]) count++; } if (count === 1) return i; } return -1;Spot the waste. In
"swiss", the inner loop counts the lettersthree separate times, once for eachs. The counts never change, so we're redoing the same work.Pick the job: this is counting. Key: the character. Value: how many times it appears. Build the counts in one pass first.
Then make a second pass over the string, in order, and return the first index whose count is 1. Order matters here: we loop over the string, not the Map, so "first" means first in the string. Two separate loops: O(n) + O(n) = O(n).
function firstUniqueIndex(s: string): number { const counts = new Map<string, number>(); 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; }Check the edge cases.
"aabb": every count is 2, the second loop finds nothing, return -1."": both loops do nothing, return -1."z": count ofzis 1, return 0.
Common mistakes
- Storing before checking in a "seen before" loop, so every item matches itself. For example, the first element gets reported as a duplicate.
- Writing
counts.set(x, counts.get(x) + 1)without?? 0. The first time,undefined + 1isNaN, and every count becomesNaN. - Checking
if (map.get(k))when the stored value can be0, such as an index or a count.0is falsy, so a real entry looks missing. Usemap.has(k). - Looping over the Map to find "the first" in the original order when the order you need is the input's. Loop over the input for the second pass.
- Using an array as a key (
map.set([1, 2], …)). Arrays compare by reference, somap.get([1, 2])isundefined. Build a string key such as"1,2".
Cost
Each element is read once and each Map or Set operation takes about one step, so the walk is O(n) time. In the worst case the Map holds an entry for every element: O(n) space. You trade memory for removing the inner loop.
In interviews
Say the nested-loop idea first in one sentence, with its O(n²) cost. Then say what the inner loop is really doing ("it's searching for one value" or "it's recounting") and that a Map makes that one step. Name your key and value out loud before you code.