The Brute Force Trap

You need to find two numbers in an array that add up to a target. The obvious approach: for each number, scan the rest of the array for its complement. Two nested loops — one comparison per pair, every pair touched.

This approach feels natural because it mirrors the problem statement word for word. “Find two numbers that sum to the target” becomes “try every pair until one works.” There is no leap of insight required — just translation. That is exactly what makes it a trap. The approach is correct, but its cost grows with the square of the array length, and because it is so easy to reach for, most people never ask whether a smarter structure exists.

The trap does not show its teeth at small n. Four elements, six pair checks — fine. But the cost curve is quadratic, and quadratic curves stay polite right up until they don't. Drag the slider below from 4 to 1,000 and watch the comparison count climb.

FIG. 1 — BRUTE-FORCE COST AT SCALE

Two loops, every pair

CONTROL
8

DRAG FROM 4 TO 1,000 · COSTS UPDATE LIVE

BRUTE FORCE · n(n−1)/228COMPARISONS
HASH MAP · n8CHECKS
BRUTE COSTS4×MORE THAN HASH
PAIR GRID · UPPER TRIANGLE

1 TILE = 1 PAIR

DRAG THE SLIDER TO A FEW DIFFERENT VALUES
— Drag n. Watch the comparisons explode. —

That gap is the brute-force trap. Every pair checked is a comparison the algorithm cannot skip — and the inner loop has no way to use what the outer loop already learned. Index 0 compares against indices 1, 2, 3, …, n-1. Then index 1 starts over and re-compares against 2, 3, …, n-1 — values index 0 already touched. Each iteration starts fresh, blind to everything the previous iterations knew. That redundancy is baked into the structure of nested iteration, and no amount of cleverness inside the loops can fix it. The fix requires changing the structure itself.

Remember as You Go

You felt the wall: a thousand elements, half a million comparisons, all because the inner loop keeps re-discovering values the outer loop already saw. The waste is re-scanning.

So here is a question with no obvious answer yet — what would it take to never re-scan? You cannot remove the second loop unless the work it does becomes unnecessary. And that work is “find a value.” Suppose, instead of scanning backward through values you have already passed, you wrote each number down as you encountered it. Not in a list — that would just be another thing to scan. Somewhere you could ask “have I seen this value?” without looking. Try it below: walk the array forward, one element at a time, and decide what to do at each step.

FIG. 2 — HASH-MAP RESCUE · ONE PASS

Build the map, find the pair

TARGET5
STEPS0
3
0
8
1
1
2
4
3
CODE · twoSum.body
1
for (const val of arr) {
2
  const complement = target - val;
3
  if (map.has(complement)) return true;
4
  map.set(val, true);
5
}
— Walk forward, write each value down, and ask the map at every step. —

That is the lookup trick. The structure you were filling in is a hash map — a key/value store where "is k already a key?" is one operation, regardless of how many keys are inside. The algorithm became: at each element, compute the complement (target minus current value), ask the map whether the complement is already there, and if not, store the current value with its index. Four steps, one pass, zero wasted comparisons. Where brute force was poised to check up to n*(n-1)/2 pairs, the hash map approach finishes in at most n steps — each step doing one constant-time lookup and one constant-time insert.

The change is structural, not cosmetic. Brute force is comparison-based: it asks “do these two values sum to the target?” by testing every pair. The hash map approach is lookup-based: it asks “have I already seen the value I need?” by querying a data structure. That single shift — from pairwise comparison to single lookup — collapses O(n²) into O(n). For 1,000 elements, brute force checks up to 499,500 pairs. The hash map finishes in at most 1,000 steps.

Predict the Lookup

You built the map step by step on familiar data. Now predict what happens at each step without seeing the map update first. A different array, a different target — same pattern.

Running the algorithm on new data matters because the brute-force trap is easy to fall into again the moment the numbers change. The problem always looks like it needs pair comparison — “find two numbers” practically begs you to write two loops. The only way to internalize the lookup pattern is to run it yourself on unfamiliar input, where you cannot rely on memory of the previous example. If you can predict each step correctly here, the pattern has moved from something you watched to something you own.

Pay attention to the mental process at each step. You will compute a complement, scan the map for it, and decide whether the answer has been found. The moment that decision flips from “not found — store and continue” to “found — return the pair,” the algorithm is done. Predicting that transition correctly is the core skill.

Array: [6, 2, 9, 3], target = 11.

FIG. 3 — PREDICT THE LOOKUP

Round 1 of 4

ARRAY · TARGET 11
6
0
2
1
9
2
3
3

Processing 6. Complement is 5. Is 5 in the map?

CODE · twoSum.body
1
for (const val of arr) {
2
  const complement = target - val;
3
  if (map.has(complement)) return true;
4
  map.set(val, true);
5
}
— Hold the map in your head. Decide before the reveal. —

You ran the algorithm in your head on new data and got it right. The pattern is always the same: compute the complement, check the map, store if not found. Three operations per element, each O(1). The brute-force approach would have checked up to 6 pairs — the hash map found the answer in 3 steps.

Notice that the algorithm stopped early: element 3 at index 3 was never even visited. The moment the complement was found in the map, the answer was returned. Brute force has no such shortcut — it keeps grinding through pairs until it either finds a match or exhausts every combination. Early termination is a bonus, not the main win. Even in the worst case — where the answer is the very last element — the hash map approach still processes each element exactly once. The real power is that every step does constant-time work regardless of where the answer hides.

Write the Pattern

You traced the algorithm. Now write it.

Tracing the algorithm on paper is different from writing it in code. When you traced, you could see the map state and compute the complement mentally. In code, each operation has to be explicit — and the order matters. A single misplaced line can turn a correct algorithm into a subtle bug that passes most test cases and fails on one.

Three blanks stand between you and the complete implementation. Each blank tests whether you understand what the hash map stores, what it checks, and why the order of operations matters. Try them before reading on — one of the blanks hides a subtle ordering trap that only shows up when you reason about it on the page.

FIG. 4 — WRITE THE PATTERN

Compile the loop in your head

1
function twoSum(nums: number[], target: number): number[] {
2
const map = new Map<number, number>();
3
for (let i = 0; i < nums.length; i++) {
4
const complement = ___complement___;
5
if (map.has(___has-check___)) {
6
return [map.get(complement)!, i];
7
}
8
map.set(___store-key___, i);
9
}
10
return [];
11
}
— Three blanks. Each one tests a different piece of the loop. —

The check-then-store order is not arbitrary — it is the reason this pattern works. You check the map first because the complement might already be stored from an earlier iteration. You store after checking because storing first would let an element pair with itself: if nums[i] is exactly half of target, the complement equals the value, and a stored-then-checked map would happily return [i, i] — wrong answer. The order encodes a constraint: each element can only pair with elements that came before it.

This pattern — compute what you need, check if you have seen it, store what you have — is the backbone of dozens of hash map problems. Contains Duplicate is map.has(nums[i]) — you only need to know whether the value exists, not where. Valid Anagram is frequency comparison — two maps, same keys, same counts. Group Anagrams is sorted-key bucketing — the map key is a canonical form, and the value is a list of strings that share it.

The deeper principle is a space-time trade: you spend O(n) extra memory on the hash map to eliminate O(n) redundant scans. Every nested-loop-to-hash-map refactor follows this trade. The extra memory is almost always worth it — an array of 10,000 elements costs 10,000 map entries (trivial), but the nested loop it replaces would have cost up to 50 million comparisons.

When you catch yourself writing two loops to match values, pause and ask: can I store earlier values in a map and look them up in O(1)? If the answer is yes, the nested loop is the brute-force trap, and the hash map is the way out. Recognizing that moment — the moment where you choose a data structure over a second loop — is the skill this lesson exists to build.