Two sum: remember the useful pastLESSON 2.02 · 2 OF 43 IN CHAPTER
PART A / Coding problems and trade-offs
Step 28 of 252
LESSON 2.02 · 2 OF 43 IN CHAPTERTry it, then open the solution

Two sum: remember the useful past

Constructed practice problem; no company attribution. Prerequisites: map lookup.

Candidate brief

THE PROBLEM

A reconciliation tool receives signed transaction amounts. Find two different positions whose amounts total a requested adjustment. Return the first pair found while scanning rightward. What should happen when amounts repeat or no pair exists?

Write this:

def two_sum(nums, target):
    ...
Contract Decision
Input Integer list/tuple nums, integer target; booleans excluded
Output (i, j) with i < j; choose smallest j, then smallest i; otherwise None
Boundaries Empty/singleton inputs cannot form a pair; inputs are not mutated
Invalid input ValueError for invalid container, element, or target type
Excluded Approximate floating-point money and distributed reconciliation
Optional refresher · the underlying tool

A Python map is a dict. Here it remembers earlier values and their positions. A 3 at index 0 can pair with a later 3 to total 6; a one-item [3] cannot reuse itself.

visited = {3: 0}     # value 3 appeared at index 0
print(3 in visited)  # True
print(visited[3])    # 0

Trace the dictionary before processing each position. A key is an earlier value; its stored index is that value's first occurrence. If the contract asks for all pairs, retain every earlier index per value instead. The foundations chapter covers map operations; this problem adds tie order and invalid inputs.

What the interviewer expects

The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.

Done means: (i, j) with i < j; choose smallest j, then smallest i; otherwise None.

Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.

Test-case scenarios to settle before coding

01 · Representative

Input / starting state
[2, 7, 11, 15], target 9
Expected result
(0, 1)

What it is testing: A prior complement should be found.

02 · Repeated value

Input / starting state
[3, 3], target 6
Expected result
(0, 1)

What it is testing: Two positions may hold the same value.

03 · No answer

Input / starting state
[1, 2, 3], target 20
Expected result
None

What it is testing: Absence is part of the return contract.

04 · Empty input

Input / starting state
nums=[], target=9
Expected result
None

What it is testing: There are no positions to pair.

05 · Only one item

Input / starting state
nums=[3], target=6
Expected result
None

What it is testing: One position cannot be reused.

06 · Tie rule

Input / starting state
[1, 4, 2, 3], target 5
Expected result
(0, 1)

What it is testing: Smallest right index wins before later pairs.

07 · Invalid/atomic

Input / starting state
[True, 2], target 3
Expected result
ValueError; input unchanged

What it is testing: Python booleans must not silently count as integers.

For each case, show which branch or state change produces that result.

two_sum([3, 3, 2], 6) == (0, 1): equal values at distinct positions are allowed. two_sum([3], 6) is None: one position cannot be reused. two_sum([True, 2], 3) raises ValueError.

Before opening the explanation, restate the contract, trace the smallest useful example, implement a baseline, and identify the repeated work. Then implement your improvement independently and derive tests from the contract. Say what your state means before saying which data structure stores it.

Worked lesson, changed requirements, and reference

Derive the representation

Start with each possible right endpoint j, then try all earlier i in order. That baseline directly implements the tie rule but performs O(n²) comparisons. A candidate should identify the repeated question: “Did target - nums[j] appear earlier, and where did it first appear?” visited answers that question without rescanning the prefix. Each encountered value maps to its first index.

j / value Look up complement Map before lookup Result / next map
0 / 3 3 {} no pair; remember 3 → 0
1 / 3 3 {3: 0} return (0, 1)

The invariant is that, before processing j, the map contains each earlier value and its first index. Lookup before insertion prevents using j twice. Keeping the first index preserves the second tie rule; returning on the first successful right endpoint preserves the first. If lookup fails, record this value and index unless the value already has an earlier index. If every lookup fails, every eligible pair was ruled out when its right endpoint was visited. Hash lookup has expected constant cost, so validation plus search cost expected O(n) time and O(n) auxiliary space; adversarial hash behavior is not a promised worst-case O(n) bound. No result-size term is needed for one pair.

Follow-up 1: return every index pair

Predict the state change before reading the table. Storing one index now loses answers. Map each encountered value to all earlier indices and emit a pair for each complement match. For [3, 3, 3], target 6:

Right index Stored matching indices Newly emitted pairs
0 none none
1 [0] (0, 1)
2 [0, 1] (0, 2), (1, 2)

The expected bound becomes O(n + p), where p is the number of returned pairs; p can be quadratic. A generator reduces retained output, not required work. Distinct value pairs would require a different deduplication contract.

Follow-up 2: amounts arrive forever

Now accept add(amount) and ask whether a pair exists among the most recent three arrivals. Redraw ownership of history: the old permanent map is incorrect because expired values can produce false matches.

Diagram: Follow-up 2: amounts arrive forever

Counts, rather than a set, preserve duplicates during eviction. If an amount is its own complement, require count at least two. A senior candidate separates index-pair, value-pair, and existence semantics and explains output cost. A lead candidate additionally defines ordering, retention, and backpressure for the streaming API; the in-memory solution supplies no cross-process ordering.

Run and check

From the repository root:

cd curriculum/01-code/02-data-structures-algorithms/problems/01-two-sum
python -m unittest -v test_solution.py

Reference implementation (download file, source below) · Contract and oracle tests (download file, source below). Read the tests after your attempt. A green reference suite verifies the supplied implementation; it does not demonstrate independent transfer. Reimplement one follow-up with the reference closed and explain which old invariant no longer holds.

Reference implementation · solution.py
def _integers(values):
    if not isinstance(values, (list, tuple)) or any(type(x) is not int for x in values):
        raise ValueError("expected a list or tuple of integers, excluding bool")

def two_sum(nums, target):
    _integers(nums)
    if type(target) is not int:
        raise ValueError("target must be an integer")
    visited = {}
    for j, value in enumerate(nums):
        complement = target - value
        if complement in visited:
            return visited[complement], j
        visited.setdefault(value, j)
    return None
Contract and oracle tests · test_solution.py
import itertools
import unittest
from solution import two_sum

class TwoSumTests(unittest.TestCase):
    def test_contract(self):
        self.assertEqual(two_sum([3, 3, 2], 6), (0, 1))
        self.assertEqual(two_sum([1, 1, 3], 4), (0, 2))
        self.assertIsNone(two_sum([3], 6))
        nums = [-3, 8, 4]
        self.assertEqual(two_sum(nums, 5), (0, 1))
        self.assertEqual(nums, [-3, 8, 4])
        for nums, target in [(None, 2), ([True], 2), ([1], 2.0)]:
            with self.assertRaises(ValueError):
                two_sum(nums, target)

    def test_exhaustive_pair_oracle(self):
        for n in range(6):
            for values in itertools.product(range(-1, 2), repeat=n):
                for target in range(-2, 3):
                    expected = next(((i, j) for j in range(n) for i in range(j)
                                     if values[i] + values[j] == target), None)
                    self.assertEqual(two_sum(values, target), expected)

if __name__ == "__main__":
    unittest.main()