Arrays & Hashing

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?

Topic guide10 problems
The unlock

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.

MENTAL MODEL A map is a bribe: pay space, buy time

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.

The reframe → don't ask “what loop checks this?” Ask “what am I repeatedly looking up, and what key would let me find it instantly?” Put that in a map.

SEE IT The set already holds the answer

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 smell test →if your first instinct is two nested loops where the inner one re-walks elements you've already touched, that inner loop is begging to become a hash lookup.

PREFIX SUMS A range sum is a subtraction of two running totals

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.

One trick, two guises → Two Sum looks up a missing value; subarray-sum looks up a missing prefix total. Both are “is my complement already in the map?”

FREQUENCY Counting turns "same multiset?" into arithmetic

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.

HOW TO THINK The cold-start ladder — run this on any new array problem

Faced with an unfamiliar array/string problem, climb these rungs. The map writes itself by the bottom:

  1. Spot the repeated lookup.Write the brute force in your head. What question does the inner loop keep re-answering? (“is my partner here?”, “how many of these letters?”, “have I seen this prefix?”)
  2. Name the key. What would I look that question up by? The raw value? A complement target − x? A sorted signature? A running prefix sum? The key is whatever future-you will hold in hand.
  3. Name the value. Do I need to know it merely exists (Set),how often it appeared (count Map), or where I saw it (index Map)? That choice is Set vs Map.
  4. Order the two moves: query then insert. For each element, look up what you need first, then file the current element. (Flip it only when the current element must be includable — and pre-seed prefix maps with map.set(0, 1).)
  5. Read off the answer — return the indices, increment a count, extend a run, or hand back the map for a later step.
The one question that unlocks the key →“What am I repeatedly looking up? Put it in a map keyed by that thing.” Get the key right and the rest is bookkeeping.

SAY IT State the map out loud before you type it

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.

  • Two Sum:seen maps value → the index I saw it at.”
  • Group Anagrams:groups maps sorted-letters signature → the list of words with those letters.”
  • Subarray Sum = K:prefixCount maps a running prefix total → how many times that total has occurred.”
The invariant test → at the top of each loop iteration, the map should hold exactly the elements strictly before the current one. If you can state that and it's true, your query/insert order is correct. If a value can accidentally match itself, you inserted too early.

SHAPE Every hashing solution is this skeleton in disguise

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 wants

The 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.

MNEMONIC Have I seen your other half?

Have I seen your other half?Trade space for time: one scan, remembering each value in a hash map, so every "is the complement here?" check is O(1). Step the Visualize tab to watch the map fill and the answer flash.

PATTERN What this family is really about

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:

  • Hash Set — "seen so far". Membership queries: have I encountered this value before? Gives O(1) duplicate detection, complement lookup, and start-of-run checks.
  • Hash Map — frequency / complement map. Counting or keyed lookup: how many times did X appear? or what index did I last see X at? Powers anagram detection, top-k, and Two Sum.

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.

KEY IDEA The complement trick — reframe "find a pair" as a lookup

The insight → instead of asking "does any previous element pair with the current one?" (O(n) scan), store previous elements in a map and ask "is the complement I need already in there?" — O(1).

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)).

RECIPE Step-by-step approach for any Arrays & Hashing problem

  1. Identify what you need to remember— a value's existence, its count, or its index. That decides Set vs Map.
  2. Decide what to key on — the raw value, a complement, a sorted signature (anagram grouping), or a prefix sum.
  3. Single pass: for each element, query first, then insert (or vice versa if you need to include the current element).
  4. Watch your base case: prefix sum maps always pre-insert 0 → 1 so empty-prefix subarrays are counted.
  5. Return the count, the indices, the map itself, or a derived structure (bucket sort by frequency for top-k).

COST Complexity profile

Brute force
O(n²)
Nested loops: compare every pair.
Hash map / set
O(n)
Single pass + O(n) space for the map.

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.

RUN IT Have I seen your other half?

step 0 / 3
STARTFind two values that sum to 6. Scan left→right, remembering every value in a hash map. Have I seen your other half?
nums3021421374
State
6
target
need
{ }
seen
current valuecomplement foundstored in map
slowfast

TRIGGERS When you see ___ → reach for ___

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 arraycomplement 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

RED FLAGSWhen it's NOT this pattern

  • The array is sorted and you need O(1) space. A sorted input often means two pointers will work — e.g. Two Sum II. Hashing buys nothing over the O(1) pointer approach when order gives you the monotonicity you need.
  • You need a contiguous subarray with a dynamic size constraint.If the problem involves shrinking and growing a window (e.g. "minimum window substring"), that's a sliding window problem — hashing still plays a supporting role, but the outer logic is window expansion/contraction, not a flat single pass.
  • Keys are arrays or objects. JS/TS maps key objects by reference, not value. Stringify composite keys (e.g. JSON.stringify(sorted) for anagram groups, or `${r},${c}` for coordinate pairs) before using them as map keys.
  • Problem asks for in-place or O(1) space. Hash structures allocate O(n) space, which violates strict in-place constraints. Fall back to sorting or index-sign tricks.

TEMPLATE Hash Set — seen so far

When → Membership queries: have I seen this value before? Classic for duplicate detection, set intersection, and start-of-run checks (Longest Consecutive Sequence).

hash-set-seen-so-far.ts
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;
}
O(1) amortised per operation → both .has() and .add() are O(1) average, making the whole loop O(n).

TEMPLATE Frequency map / counter

When → Counting occurrences — anagram detection, top-k elements, or validating that two structures have matching character/element tallies.

frequency-map-counter.ts
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);
}
Bucket sort beats a heap → when you need top-k by frequency and k can be up to n, bucket sort is O(n) vs O(n log k) for a min-heap. Use the heap only when k is much smaller than n or when a streaming answer is needed.

TEMPLATE Prefix sum + complement map

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).

prefix-sum-complement-map.ts
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;
}
Base case is non-negotiable → 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.

TEMPLATE Complement lookup (Two Sum / pair search)

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.

complement-lookup-two-sum-pair-search-.ts
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');
}
Insert after checking → storing 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.

PITFALL Keying objects or arrays by reference

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.

PITFALL Forgetting the prefix-sum base case

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.

PITFALL Using a plain object instead of Map for numeric keys

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.

PITFALL Integer overflow in product problems

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.

PROBLEMS

#1Two SumThe canonical complement-map pattern: one pass, store value → index, look up target − current.#121Best Time to Buy and Sell StockGreedy one-pass: track the running minimum price seen so far and update max profit each step — no hashing needed, but a classic single-pass array scan.#217Contains DuplicateSimplest hash-set application: insert each element and short-circuit the moment .has() returns true.#238Product of Array Except SelfTwo-pass prefix/suffix product arrays: left pass fills prefix products, right pass multiplies in suffix products — O(n) time, O(1) extra space (output array excluded).#128Longest Consecutive SequenceLoad all values into a set, then for each number that has no left-neighbour (num−1 absent) walk the run upward — ensures each element is visited at most twice.#36Valid SudokuThree families of hash sets (one per row, column, and 3×3 box keyed by ⌊r/3⌋,⌊c/3⌋): a single board scan validates all constraints simultaneously.#41First Missing PositiveFind the smallest missing positive integer in O(n) time and O(1) extra space by using the array itself as a hash table: place each value v at index v-1 via cyclic sort, then scan for the first index that does not hold i+1.#169Majority ElementFind the element appearing more than n/2 times in O(n) time and O(1) space with the Boyer-Moore voting algorithm: a single candidate and a count that cancels out every minority vote, leaving the majority standing.#560Subarray Sum Equals KCount contiguous subarrays summing to k in one pass by pairing a running prefix sum with a hash map of seen prefix counts (seeded with {0:1}). Handles negatives, where a sliding window cannot. O(n) time and space.#380Insert Delete GetRandom O(1)Support insert, remove, and getRandom all in average O(1) by pairing a dynamic array with a value→index map: deletion swaps the target with the last element so the array stays gap-free for uniform random access.