A hash map or set converts an O(n) membership search into an O(1) lookup, turning brute-force nested loops into a single pass. Almost every problem in this family is really asking: what have I already seen, and what does it tell me about what I'm looking at now?
A hash set/map is an O(1) memory of “have I seen this before, and where?” The moment you catch yourself re-scanning the array to answer the same little question over and over, stop — write the answer down in a map the first time, and every later lookup is free.
The brute force for almost every array problem is the same sad move: for each element, loop back over everything you've already passed to check something. That inner loop is pure repeated work — you keep re-asking “did anything earlier pair with me / equal me / sum with me?”
A hash map ends the re-scanning. As you walk the array once, you jot down each element keyed by whatever a future element will want to look up. Now that backward loop becomes a single O(1) question: “is it already in the map?”You traded O(n) extra space for an O(n²) → O(n) collapse. The algorithm didn't get cleverer — it just stopped being forgetful.
Two Sum, the cleanest example. We scan once. At each value x we ask the set one thing — “have you seen target − x?” — then file x away for whoever comes next:
nums = [ 2, 7, 11, 15 ] target = 9
x = 2 ─ need 9−2 = 7 ─ seen? ✗ ─ file 2 seen = { 2 }
x = 7 ─ need 9−7 = 2 ─ seen? ✓ ──────────► MATCH! (2 + 7 = 9)
seen = { 2, 7 }
The brute force re-scans the whole left side at every x (O(n²)).
The set already HAS the answer to "did 7's partner come by?" — O(1).When x = 7 arrives, its partner 2 is already in the set from a previous step. No backward scan — the memory we built does the searching for us.
The same “remember it so you never recompute it” idea powers prefix sums. Keep one running total as you go. Then the sum of any range is just the total at the end minus the total before the start — one subtraction, no re-adding the middle:
nums : [ 3 , 1 , 4 , 1 , 5 , 9 ]
prefix : 0 3 4 8 9 14 23
index : 0 1 2 3 4 5 6
▲ ▲
prefix[2] prefix[5]
sum(nums[2..4]) = 4 + 1 + 5 = 10
= prefix[5] − prefix[2] = 14 − 4 = 10
Any range sum = (total up to the end) − (total before the start).
Two array reads. No re-adding the middle.That unlocks the famous combo: “count subarrays summing to k” becomes prefix[j] − prefix[i] = k, i.e. prefix[i] = prefix[j] − k. Now it's a complement lookup on prefix sums — the exact same map trick as Two Sum, just with running totals as the keys.
When a problem cares about how many rather than where— anagrams, top-k, “can I build this from those letters” — a frequency map is the memory. Order evaporates; only the tallies matter:
s = "eat" t = "tea"
tally up s drain down with t
┌───┬───┐ ┌───┬───┐
e → │ e │ 1 │ e → │ e │ 0 │
a → │ a │ 1 │ a → │ a │ 0 │
t → │ t │ 1 │ t → │ t │ 0 │
└───┴───┘ └───┴───┘
all zero ► anagram ✓
Order never mattered. Only the COUNTS did. Same counts = anagram.Tally up one string, drain down with the other. End at all-zero and they're the same multiset. Sorting both strings would also work but costs O(n log n) — counting is a single O(n) pass.
Faced with an unfamiliar array/string problem, climb these rungs. The map writes itself by the bottom:
target − x? A sorted signature? A running prefix sum? The key is whatever future-you will hold in hand.map.set(0, 1).)The highest-leverage habit here: before coding, say what your map means in one sentence — “key → value”with the English spelled out. If you can't finish the sentence, you don't have the structure yet.
seen maps value → the index I saw it at.”groups maps sorted-letters signature → the list of words with those letters.”prefixCount maps a running prefix total → how many times that total has occurred.”Strip away the specific problem and almost every Arrays & Hashing answer is the same three moves: scan once → look up → file away. Read it as a sentence, not code:
for each element x as you scan left → right:
need = the thing I keep looking up # partner / count / prefix
if need is already in my map: # O(1) — I filed it earlier
use it (return, count++, extend the run, ...)
file x away in the map # so a LATER one finds it
# keyed by what future me wantsThe only things that change between problems are what you look up (the key) and what you remember (existence, count, or index). The single-pass shape never changes — which is why this family becomes reflexive fast.
Arrays & Hashing problems all share one theme: trading memory for speed. The input is an unsorted (or unordered) array and a nested O(n²) brute force is obvious — but by storing information about elements we've already processed into a hash structure, we can answer queries in O(1) and finish in a single linear pass.
There are two dominant sub-patterns:
Prefix sums are a close cousin: precompute cumulative totals so any range query is O(1), and pair them with a complement map to count subarrays.
This converts every pair-search into a single-pass set/map lookup. The same reframing works for subarrays: subarray sum = k becomes prefix[j] − prefix[i] = k, i.e. prefix[i] = prefix[j] − k — a hash map lookup on the prefix sum seen so far.
The key discipline: populate the map before or after checking the current element depending on whether you need to avoid self-matching (Two Sum) or allow zero-length subarrays (prefix sum base case map.set(0,1)).
0 → 1 so empty-prefix subarrays are counted.The tradeoff is always O(n) extra space. In the rare case where you can't afford that space, check whether sorting first (O(n log n) time, O(1) extra) unlocks a two-pointer solution instead.
6. Scan left→right, remembering every value in a hash map. Have I seen your other half?Reach for a hash map or set when the problem is fundamentally about membership, counting, or pairing within an unsorted array, and a nested loop is the obvious-but-slow solution. Prefix sums enter when the problem mentions subarrays, ranges, or cumulative totals.
| "find two numbers that sum to target" — unsorted array | complement map: store value → index, look up (target − current) on each step |
| "does the array contain a duplicate?" | hash set: add each element; return true the moment .has() fires |
| "group by anagram" / "are two strings anagrams?" | frequency map or sorted-string as map key to bucket equivalent words |
| "count subarrays that sum to k" / "longest subarray with sum k" | prefix sum + complement map: count (or index) of prefix[j] − k seen before j |
| "find the longest consecutive sequence" | hash set + start-of-run check: only iterate from nums where num−1 is absent |
| "top k most frequent elements" | frequency map then bucket-sort (or a size-k min-heap) for O(n) or O(n log k) |
| "product of array except self" / "prefix × suffix" | two-pass prefix/suffix product arrays — no division, no extra map needed |
JSON.stringify(sorted) for anagram groups, or `${r},${c}` for coordinate pairs) before using them as map keys.When → Membership queries: have I seen this value before? Classic for duplicate detection, set intersection, and start-of-run checks (Longest Consecutive Sequence).
function hasDuplicate(nums: number[]): boolean {
const seen = new Set<number>();
for (const n of nums) {
if (seen.has(n)) return true; // already encountered → duplicate found
seen.add(n);
}
return false;
}.has() and .add() are O(1) average, making the whole loop O(n).When → Counting occurrences — anagram detection, top-k elements, or validating that two structures have matching character/element tallies.
function topKFrequent(nums: number[], k: number): number[] {
// 1. Count occurrences — O(n)
const freq = new Map<number, number>();
for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1);
// 2. Bucket sort by frequency — O(n), avoids O(n log n) heap
const buckets: number[][] = Array.from({ length: nums.length + 1 }, () => []);
for (const [val, cnt] of freq) buckets[cnt].push(val);
// 3. Collect from highest-frequency bucket downward
const result: number[] = [];
for (let i = buckets.length - 1; i >= 0 && result.length < k; i--) {
result.push(...buckets[i]);
}
return result.slice(0, k);
}When → Count or locate subarrays whose elements sum to a target. The prefix sum turns any range query into a subtraction; the map makes the complement lookup O(1).
function subarraySum(nums: number[], k: number): number {
// prefix[i] = sum of nums[0..i-1]; prefix[0] = 0
const prefixCount = new Map<number, number>([[0, 1]]); // base case
let count = 0, runningSum = 0;
for (const n of nums) {
runningSum += n;
// If (runningSum - k) was a previous prefix sum, the gap between
// that earlier index and now is a subarray that sums to k.
count += prefixCount.get(runningSum - k) ?? 0;
prefixCount.set(runningSum, (prefixCount.get(runningSum) ?? 0) + 1);
}
return count;
}map.set(0, 1)before the loop handles subarrays that start at index 0 (where the "previous prefix" is the empty prefix with sum 0). Omitting it silently under-counts.When → Find a pair of indices whose values satisfy a relationship (sum, product, difference). Build the map as you go so you never match an element with itself.
function twoSum(nums: number[], target: number): [number, number] {
const seen = new Map<number, number>(); // 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); // record AFTER checking — avoids self-match
}
throw new Error('no solution');
}nums[i] into the map only after checking for its complement ensures the pair uses two distinct indices, even when the same value appears twice.In JavaScript/TypeScript, new Map() uses reference equality for object and array keys. Two arrays [1,2,3] and [1,2,3] are different keys even if they look identical. Always convert composite keys to a string or primitive — e.g. arr.sort().join(',') for anagram grouping — before inserting into a map.
The complement map for subarray-sum problems must be initialised with map.set(0, 1)before the loop. Without it, any subarray that starts at index 0 and sums to k is missed, because there is no "previous prefix" entry to match against.
Plain JS objects coerce numeric keys to strings and iterate them in numeric-ascending order, which can cause subtle bugs. For frequency counting or prefix-sum maps where keys are numbers, use new Map<number, number>() — it preserves insertion order and handles negative keys correctly, unlike a plain object.
Product of Array Except Self and similar problems accumulate products across up to 10⁵ elements. JavaScript numbers are IEEE-754 doubles (safe integers up to 2⁵³), so overflow is rare in practice, but if a problem uses a typed language or large values, keep intermediate results modulo a prime or use BigInt. Always check the constraints before assuming products fit in a standard integer.