Problem overview
TwoSum receives an int[] nums and an int target. It returns the indices of two numbers that add up to target, or {-1, -1} if no pair exists. There is at most one solution, and the same index can't be used twice, although two equal values at different indices are fine.
For nums = {2, 7, 11, 15} and target = 9, the values at indices 0 and 1 are 2 and 7, which add up to 9, so the result is {0, 1}. The statement also sets a budget of O(n log n) time and O(n) extra space.
Approach
The brute force checks every pair of indices, which is O(n²). A hash-based approach reaches O(n). This lesson uses a third idea: sort, then squeeze from both ends.
Once the values are in ascending order, put one pointer, first, on the smallest value and another, last, on the largest. Look at their sum:
- If it equals
target, you found the pair. - If it is too small, the current smallest value fails even with the largest remaining partner, so no pairing can work for it. Move
firstright to a bigger value. - If it is too large, the current largest value fails even with the smallest remaining partner. Move
lastleft to a smaller value.
Each step discards one candidate for good, so the scan takes at most n steps. The sort is the expensive part, at O(n log n).
Sorting has one catch. It moves the values, but the answer needs original indices. So before sorting, every value is paired with its index. The pair {value, index} travels together through the sort, and the indices are read from the pairs at the end.
Step by step
- Allocate the pairs.
sortedNumsis a jaggedint[][]with one row per element.new int[nums.Length][]creates the outer array only, so each row starts asnull. - Fill each row with
new int[]{nums[i], i}: index 0 holds the value and index 1 the original position. - Sort by value. An
int[]has no natural order, soArray.Sorttakes a comparison lambda,(first, second) => first[0] - second[0]. A negative result putsfirstbeforesecond. - Declare both pointers in one statement:
firstat 0 andlastatsortedNums.Length - 1. - Loop while
first < last. When they meet, every pair has been ruled out. - Add the two values,
sortedNums[first][0]andsortedNums[last][0], intosum. - On a match, return the original indices
sortedNums[first][1]andsortedNums[last][1]. - Too small:
++first. Too large:--last. - Fall back to
{-1, -1}after the loop.
The solution in C#
class Solution {
public int[] TwoSum(int[] nums, int target) {
int[][] sortedNums = new int[nums.Length][];
for (int i = 0; i < nums.Length; ++i) {
sortedNums[i] = new int[]{nums[i], i};
}
Array.Sort(sortedNums, (first, second) => first[0] - second[0]);
int first = 0, last = sortedNums.Length - 1;
while (first < last) {
int sum = sortedNums[first][0] + sortedNums[last][0];
if (sum == target) {
return new int[]{sortedNums[first][1], sortedNums[last][1]};
} else if (sum < target) {
++first;
} else {
--last;
}
}
return new int[]{-1, -1};
}
}
C# details:
- Jagged array.
int[][]is an array whose elements are themselves arrays, and the inner arrays can be created one at a time. That fits here because every row is a two-element pair. - The lambda is a
Comparison<int[]>.Array.Sort(array, comparison)sorts in place and calls the lambda to order two elements. It returns negative, zero or positive, likeCompareTo. - Two things called
first. The lambda's parameterfirstand the local variablefirstdeclared a few lines later are unrelated. The code compiles with the current compiler, but different names would be clearer. int first = 0, last = ...declares two variables of the same type in one statement.- The sort is not stable. Equal values may end up in either order, which doesn't matter here, since any pair of equal values that works is a valid answer.
The subtraction comparator can overflow
first[0] - second[0] is a classic shortcut, and it is only safe while the difference fits in an int. C# int arithmetic wraps around silently by default. For example, 2,000,000,000 minus -2,000,000,000 gives -294,967,296 instead of 4,000,000,000, so the sort treats the larger value as smaller. Running this code on {2000000000, -2000000000, 1, 3} with target 4 leaves the values in the order 2000000000, -2000000000, 1, 3, which is not sorted, and it returns {-1, -1} although indices 2 and 3 are a valid pair. Comparing with first[0].CompareTo(second[0]) has no such problem. The same wrap-around applies to sum for huge values. For modest inputs, the lesson's version is fine.
Complexity
Time: O(n log n). Building the pairs is O(n), the sort is O(n log n), and the two-pointer scan moves one pointer per iteration, so it takes at most n - 1 iterations.
Space: O(n). There are n small arrays, one per element, plus the outer array. The sort works on the array in place.
Edge cases and common mistakes
- Fewer than two elements. With an empty array,
lastis -1, so the loop doesn't run. With one element, the pointers are equal. Both return{-1, -1}. - Equal values.
{3, 3}with target 6 returns{0, 1}. The two 3s are different positions, so this is allowed. - The same element twice.
{3, 4}with target 6 returns{-1, -1}. The pointers never share an index, since the loop stops when they meet. - Returning sorted positions.
firstandlastare positions in the sorted array. The answer comes from the second element of each pair. - Order of the result. The index of the smaller value comes first. For
{7, 2}and target 9 the code returns{1, 0}. - Sorting only the values. If you sort a plain copy of
nums, the indices are lost.
Testing
{2, 7, 11, 15}, target 9 gives{0, 1}.{3, 9, -2, 5}, target 3 gives{2, 3}.{3, 4}, target 6 gives{-1, -1}.{}, target 9 gives{-1, -1}.