Problem overview
The method twoSum receives an array of numbers, nums, and a target. It returns the indices of two different positions whose values add up to target, or [-1, -1] when there is no such pair. There is at most one solution, and the same index can't be used twice, although two equal values at different indices are fine.
In the example, nums = [2, 7, 11, 15] and target = 9. The values 2 and 7, at indices 0 and 1, add up to 9.
This time the statement asks for O(n) time and O(n) space.
Approach: remember what you have seen
The brute-force solution tries every pair of indices with two nested loops, which is O(n²). Sorting plus two pointers brings it to O(n log n). A hash map brings it down to a single pass.
The key is the complement. If the current element is nums[i], its partner must be exactly target - nums[i]. Treat nums[i] as the second element of the pair, so the partner must already have been seen at an earlier index. Looking up whether a value has been seen is exactly what a hash map does in constant time.
A set of seen values would answer yes or no, but the task wants the partner's index. So the map stores each value together with its index: value → index. For each element:
- Look up
target - nums[i]in the map. If it is there, the pair is the stored index andi. - Otherwise store
nums[i]with indexiand move on.
On [2, 7, 11, 15] with target 9: at index 0 the complement is 7, which isn't stored yet, so 2 is stored. At index 1 the complement is 2, which is stored with index 0, so the method returns a pair made of 1 and 0.
Step by step
- Create the map. Declare
indexOfValuewithconstas an empty object typed{ [key: number]: number }, an index signature from numbers to numbers.constonly fixes the variable, so entries can still be added. - Loop over the array with an index
ifrom 0 tonums.length - 1. Eachnums[i]is tried as the second element of the pair. - Check for the partner. Test whether
target - nums[i]is a key ofindexOfValuewith theinoperator. If it is, it appeared to the left ofi, so returnitogether with the stored index, read with square brackets. - Store the current element. After the
if, still inside the loop, setindexOfValue[nums[i]] = i. Doing this after the check means an element can't be paired with itself. - Return
[-1, -1]after the loop, for the case where no pair was found.
The solution in TypeScript
class Solution {
twoSum(nums: number[], target: number): number[] {
const indexOfValue: { [key: number]: number } = {};
for (let i = 0; i < nums.length; i++) {
if (target - nums[i] in indexOfValue) {
return [i, indexOfValue[target - nums[i]]];
}
indexOfValue[nums[i]] = i;
}
return [-1, -1];
}
}
The TypeScript details worth a closer look:
- An object as a hash map. The type
{ [key: number]: number }describes an object whose number keys map to number values. It exists only at compile time. At run time the object is a plain JavaScript object, empty until entries are added. - Keys are strings underneath. JavaScript converts the number 7 to the string
"7"when it is used as a key. Reading, writing andinall apply the same conversion, so negative numbers and zero work too. - The
inoperator tests whether a key exists, even if its stored value is 0. That matters here, because index 0 is a legitimate stored value. A check likeif (indexOfValue[x])would treat index 0 as "not found". - Precedence. In
target - nums[i] in indexOfValue, the subtraction is evaluated first, so the left side ofinis the complement. - The return order. The method returns the current index first and the earlier index second. For the example it returns
[1, 0]: the same two positions as[0, 1], in the opposite order. - Overwriting entries. When a value occurs twice, the second store replaces the first index. This is safe, because if the duplicate were part of a solution (for example
[3, 3]with target 6), the pair would already have been found before the overwrite.
Complexity
Time: O(n). The loop visits each element once, and each in test, read and write on the object takes constant time on average.
Space: O(n). In the worst case, when no pair exists, the object stores every element.
Edge cases and common mistakes
- Same element twice.
[3, 4]with target 6 gives[-1, -1]. The 3 is stored only after the check, so it is never its own partner. - Duplicates that form the answer.
[3, 3]with target 6 gives[1, 0]. The first 3 is stored, and the second finds it. - Zero and negatives.
[0, 4, 0]with target 0 gives[2, 0], and[-1, -2, -3]with target -5 gives[2, 1]. - Short input. An empty array, or an array with a single element, has no pair, so the loop ends without a match and the method returns
[-1, -1]. - Storing before checking. Putting
indexOfValue[nums[i]] = iabove theiflets an element match itself, for example 3 with target 6. - Testing the stored index for truthiness. Index 0 is falsy, so a partner at position 0 would be missed. The
inoperator avoids this. - Storing the index as the key. The map must go from value to index, because the lookup starts from the complement value.
Testing
nums = [2, 7, 11, 15],target = 9gives[1, 0].nums = [3, 2, 4],target = 6gives[2, 1].nums = [3, 3],target = 6gives[1, 0].nums = [3, 4],target = 6gives[-1, -1].nums = [1, 2, 3],target = 100gives[-1, -1].