Problem overview
The method TwoSum receives an int[] nums and an int target. It must return the indices of two numbers in the array that add up to target, as an int[] of length 2. There is at most one solution, the same index can't be used twice, and when nothing matches the method returns [-1, -1].
For nums = [2, 7, 11, 15] and target = 9, the numbers 2 and 7 at indices 0 and 1 add up to 9. This version builds its answer as the later index followed by the earlier one, so it returns [1, 0], which names the same two positions. The statement asks for O(n) time and O(n) space.
Approach: remember what you have seen
The brute force tries every pair of indices with two nested loops, which is O(n²). To get to O(n) you can afford only one loop, so the search for the partner must take constant time.
Fix the current index i and treat nums[i] as the second number of the pair. Its partner must equal target - nums[i], and it must sit to the left of i, because the loop has not reached anything to the right yet. A hash table can answer "have I seen this value, and where?" in constant time on average. So keep a dictionary indexOfValue from each value seen so far to its index:
- If
target - nums[i]is a key, the partner's index is the stored value, and the pair is complete. - Otherwise, record
nums[i]with its index and move on.
Every pair is found when the loop reaches its right member, so one pass is enough. For the example, at i = 0 the needed partner is 7, which is not stored, so 2 is recorded. At i = 1 the needed partner is 2, which is stored at index 0, so the method returns indices 1 and 0.
Step by step
- Create the dictionary.
indexOfValueis aDictionary<int, int>that maps a number to the index where it was seen. - Loop over every index. A
forloop runsifrom 0 tonums.Length - 1. Eachnums[i]is tried as the second element of the pair. - Look for the complement.
ContainsKey(target - nums[i])says whether the partner appeared earlier. If it did, return a new array holdingiand the index stored under that key. The check comes before the read because indexing a missing key would throw. - Record the current number. After the check, assign
indexOfValue[nums[i]] = i. The indexer adds the key or overwrites an old value. Because this happens after the check, an element is never paired with itself. - Report failure. If the loop ends without returning, no pair adds up to the target, so return
{-1, -1}.
The solution in C#
class Solution {
public int[] TwoSum(int[] nums, int target) {
Dictionary<int, int> indexOfValue = new Dictionary<int, int>();
for (int i = 0; i < nums.Length; ++i) {
if (indexOfValue.ContainsKey(target - nums[i])) {
return new int[]{i, indexOfValue[target - nums[i]]};
}
indexOfValue[nums[i]] = i;
}
return new int[]{-1, -1};
}
}
Some C# details:
- Returning an array.
new int[]{i, j}creates an array with those two elements. Arrays have a fixed length, which suits a pair. - The indexer throws on missing keys.
indexOfValue[x]is safe here only becauseContainsKeywas true just before.TryGetValuewould do the check and the read in a single lookup. - Overwriting duplicates. If a number appears twice,
indexOfValue[nums[i]] = ireplaces the earlier index with the later one. Either index is a valid partner, since the values are equal, and the method returns as soon as a match appears. intarithmetic wraps silently.target - nums[i]does not throw when the result is outside theintrange, it wraps around. The values in this lesson are small, so this does not arise.
Complexity
Time: O(n). The loop runs at most n times, and each iteration does a fixed number of dictionary operations, each O(1) on average.
Space: O(n). In the worst case, no pair is found and the dictionary stores one entry per element.
Edge cases and common mistakes
- Equal values. For
[3, 3]and target 6, the first 3 is recorded at index 0, and ati = 1the complement 3 is found. The method returns[1, 0]. - Pairing an element with itself. For
[3, 4]and target 6, the answer is[-1, -1]. Recordingnums[i]before checking would let the first 3 match itself. - No solution or empty input. For an empty array the loop never runs and the method returns
[-1, -1]. - Negative numbers and zero. The method works unchanged: for
[-3, 4, 3, 90]and target 0, the result is[2, 0]. - Index order. This version always puts the later index first, as in the
[1, 0]for the example. Returning[indexOfValue[...], i]would give the earlier index first instead. - Reading before checking. Using the indexer without
ContainsKeyorTryGetValuethrowsKeyNotFoundExceptionon the first miss. - Overflow at the extremes. Because
intsubtraction wraps, a target nearint.MinValuecombined with a number nearint.MaxValuecan produce a match that does not add up mathematically. For example,[int.MaxValue, 1]withint.MinValuereturns[1, 0]. Do the arithmetic inlongif such inputs are possible.
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].