Two sum: remember the useful past
Constructed practice problem; no company attribution. Prerequisites: map lookup.
Candidate brief
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], target9- Expected result
(0, 1)
What it is testing: A prior complement should be found.
02 · Repeated value
- Input / starting state
[3, 3], target6- Expected result
(0, 1)
What it is testing: Two positions may hold the same value.
03 · No answer
- Input / starting state
[1, 2, 3], target20- 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], target5- Expected result
(0, 1)
What it is testing: Smallest right index wins before later pairs.
07 · Invalid/atomic
- Input / starting state
[True, 2], target3- 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.
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.
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
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()