“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman

Two Sum

Interview Prep

Warm-upArrays & Hashingarrayshashmap

The problem

Given an array of integers nums and a target sum, return the indices of the two numbers that add up to the target. Each input has exactly one solution, and you can return the indices in any order.

The lesson: hashmaps exist

You know, if I had to sum up this problem (pun partially intended), I would describe it as an exercise in — hey, did you know that hashmaps exist? Before I learned this problem I don't think I really knew what a hashmap was. I just knew python-dictionary-lets-me-index-stuff-with-words-instead-of-numbers.

Brute force

Try every pair: not a bad solution if you don't know hashmaps exist. Seriously, I worked mostly with arrays and thought O(n²) is all you're getting, unless I prove O(n) = O(n²). And no, I'm not secretly trying to prove that theorem. It didn't look too promising after I tried proving the lemma that 2 = 4, and that 1 = 1 still holds.

def two_sum(nums: list[int], target: int) -> list[int]:
    n = len(nums)
    for i in range(n):
        for j in range(i + 1, n):
            if nums[i] + nums[j] == target:
                return [i, j]
    return []

Time O(n²). Space O(1). For n = 10⁵ this is already past most online-judge time limits.

Optimal

Walk the array once. At each index i, compute what the complement would be (target - nums[i]). If we've seen that complement before, we have our pair. If not, remember the current value for someone later to find.

def two_sum(nums: list[int], target: int) -> list[int]:
    seen: dict[int, int] = {}          # value -> index
    for i, x in enumerate(nums):
        complement = target - x
        if complement in seen:
            return [seen[complement], i]
        seen[x] = i
    return []

Time O(n) — one pass. Space O(n) — the hashmap.

Walkthrough

nums = [2, 7, 11, 15], target = 9

i = 0  x = 2   complement = 7   seen = {}            → not found, store 2:0
i = 1  x = 7   complement = 2   seen = {2: 0}        → hit, return [0, 1]

Read the loop in your head: at each step ask "is the complement already there?" before remembering the current value. Order matters — storing first would let an element pair with itself.

Edge cases

Try First

Each prompt asks a checkable question about the working code or math above — predict an output, derive a sign, state an invariant, find a bug. Commit to an answer before clicking "reveal." That commitment is the whole point: if your answer matched, you understand the piece you were looking at; if it didn't, that's the part worth re-reading.

invariant
At the top of iteration i in the optimal solution — before the if-check runs — state precisely what seen contains. Be specific about which prefix of nums is represented and which direction the mapping goes.
predict
Predict two_sum([3, 3, 3, 3], 6). There are six unordered pairs of indices that all satisfy the constraint. Which one does the function return, and why that one?
find the bug
I changed complement = target - x to complement = x - target. The code still runs and still returns lists of integers. Find an input where it now returns a pair whose values do not sum to target — and explain why the if complement in seen check failed to catch it.
what if
Now imagine the input arrives as a stream: you see nums[0], then nums[1], then so on, and at any point the caller may ask "has any pair seen so far summed to target?". What state does the function need to maintain to answer in O(1) per query? What state is it OK to drop?

Hack This

The code above works. Don't reinvent it — pull it into an editor, run it, and try the modifications below. Each one is small. Each one will change the behavior in a specific way; the question is which way.

trivial
Swap target - x for target / x in the optimal version (pretend integer division works out — use positive ints and a target that divides cleanly). You've turned Two Sum into Two Product. The structural question: addition is its own inverse (the complement of x is target - x); multiplication's inverse is division. The hashmap pattern works for any operation whose inverse you can compute. Construct an input where the SUM-of-pairs and the PRODUCT-of-pairs interpretations return different index pairs, and use it as a sanity check that the "operation-agnostic" hashmap trick really is operation-agnostic.
trivial
Move seen[x] = i to run before the if complement in seen check. The walkthrough warns that "order matters." Confirm by finding the smallest input where the reordered version returns a pair the original explicitly rejects. Then state, in one sentence, the invariant the original code maintains that the reordered version violates.
trivial
Replace the dict with a set. Try to keep returning indices. You'll find the function can no longer answer the original question — a set remembers that a value was seen, not where. What is the dict actually carrying that a set isn't? Now: what's the minimum change to the function's signature that makes the set-based version correct and meaningful?
small
Modify the function to return both the answer AND the final seen dict. Now use that returned dict to instantly answer a follow-up question: "given the same input and target, what's the SECOND pair that sums to target (lexicographically by left index)?" Does the dict alone suffice, or do you need to re-process the array? Use this to articulate exactly what information the original function throws away on exit.
small
Write a three_sum(nums, target) that reuses two_sum as a subroutine — don't write any new dictionary logic. The reduction: for each candidate first element nums[i], call two_sum(nums[i+1:], target - nums[i]). Then ask the harder question: this reduction gives you O(n²) total time. The dedicated three_sum algorithms in the wild are also O(n²). Why doesn't the reduction make three_sum somehow worse? And what does this tell you about the cost of building a generic k_sum?
small
Bug-hunt in the brute force: change range(i + 1, n) to range(i, n). The function still runs. Find an input where the new version returns a pair the problem statement forbids, then state — in terms of the original constraint, not in terms of the bug — what just went wrong.

Related