Problem overview
The method twoSum receives a list of integers, nums, and an integer target. It must find two numbers in the list that add up to target and return their indices, not the numbers themselves. There is at most one solution, and the same index cannot be used twice. If no pair works, the method returns (-1, -1).
With nums = [2, 7, 11, 15] and target = 9, the values at indices 0 and 1 are 2 and 7, and 2 + 7 = 9. The answer is the pair of indices 0 and 1. In this lesson's solution the pair comes back as a tuple, so it prints as (0, 1).
The lesson sets a budget of O(n^2) time and O(1) extra space. That is the budget of the brute force solution: try every pair.
Approach: check every pair of indices
Brute force means trying every candidate answer. Here a candidate is a pair of positions (i, j), and testing it is a single comparison, nums[i] + nums[j] == target.
The loops should not generate the same pair twice or pair a position with itself. If i runs over every index and j only over the indices after i, each unordered pair is visited exactly once. For four numbers the visited pairs are (0, 1), (0, 2), (0, 3), (1, 2), (1, 3) and (2, 3), six in total. For n numbers that is n * (n - 1) / 2 pairs, which is where O(n^2) comes from.
The first pair that works is returned immediately. Since the problem has at most one solution, there is no reason to keep searching. If both loops finish without a return, every pair has been tried and none matched.
Step by step
- Loop over the first index.
for i in range(len(nums))lets every position be the first element of the pair. - Loop over the second index.
for j in range(i + 1, len(nums))starts right afteri, so a pair is not tried twice and an element is not paired with itself. - Test the pair. If
nums[i] + nums[j] == target, return(i, j). Returning from inside the loops ends the whole search at once. - Return the failure value after the loops.
return (-1, -1)runs only when the outer loop has finished without finding anything.
The solution in Python
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for i in range(len(nums)):
for j in range(i + 1, len(nums)):
if nums[i] + nums[j] == target:
return (i, j)
return (-1, -1)
Some details a Python learner should know:
- The result is a tuple. The annotation says
List[int], but Python does not enforce annotations, and the method returns tuples. A tuple unpacks like a list:i, j = Solution().twoSum([2, 7, 11, 15], 9)givesi = 0andj = 1. A tuple and a list are not equal, though:(0, 1) == [0, 1]isFalse, so compare like with like. range(i + 1, len(nums))is empty wheniis the last index, so the inner loop simply does nothing there.- No overflow. Python integers grow as needed, so adding two large numbers is always safe.
Complexity
The nested loops test up to n * (n - 1) / 2 pairs, each in constant time, so the time is O(n^2). The method stores only the two loop indices, so the extra space is O(1).
Brute force is fine for a few thousand numbers and slow beyond that. The faster version uses a dictionary. For each number x, the partner it needs is target - x. If that value was already seen, the answer is its stored index and the current one. If not, remember x and its index:
def two_sum_fast(nums, target):
seen = {}
for i, x in enumerate(nums):
if target - x in seen:
return [seen[target - x], i]
seen[x] = i
return [-1, -1]
This runs in O(n) time with O(n) extra space, a trade of memory for speed. The lesson stays with the nested loops, and later lessons cover the faster versions.
Edge cases and common mistakes
- Using the same element twice. For
[3]withtarget = 6the answer must be(-1, -1), because 3 + 3 would need the same index twice. Startingjatiwould wrongly return(0, 0). - Equal values at different indices are fine. For
[3, 3]andtarget = 6the answer is(0, 1). - No solution.
[1, 2, 3]withtarget = 7returns(-1, -1). An empty list does too. - Negative numbers. Nothing special is needed:
[-1, -2, -3, -4, -5]withtarget = -8returns(2, 4), since -3 + -5 = -8. Do not skip values just because they are negative or larger than the target. - Returning the values instead of the indices. The answer for the example is
(0, 1), not(2, 7). - Returning inside the wrong place. Putting
return (-1, -1)inside the outer loop would give up after the first element. It belongs after both loops.
Testing
[2, 7, 11, 15]with target 9 returns(0, 1).[3, 2, 4]with target 6 returns(1, 2), not(0, 0).[3, 3]with target 6 returns(0, 1).[5]with target 10 returns(-1, -1).
