Problem overview
The function twoSum receives a slice of integers, nums, and a target. It must return 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.
For nums = [2, 7, 11, 15] and target = 9, the values at indices 0 and 1 are 2 and 7, and they add up to 9, so the answer is [0, 1]. The statement asks for O(n log n) time and O(n) extra space.
Approach: sort, then squeeze from both ends
Trying every pair costs O(n²). Sorting makes a faster scan possible. In an ascending list, put one pointer, first, on the smallest value and another, last, on the largest, and look at their sum:
- If the sum equals
target, the pair is found. - If the sum is too small, moving
lastleft could only make it smaller, so the smallest value can't be part of any answer with the current largest or anything smaller. The only useful move isfirst++, to a larger value. - If the sum is too large, the same reasoning says
last--.
Each step discards one candidate, so the scan takes at most n - 1 steps. The sort costs O(n log n), which dominates.
There is one catch: sorting moves values away from their positions, and the answer needs the original indices. So each value is stored together with its index as a two-element slice {value, index}, and the sort compares only the values.
Example with nums = [3, 9, -2, 5] and target = 3: the sorted pairs are {-2, 2}, {3, 0}, {5, 3}, {9, 1}. The sum of the ends, -2 + 9, is 7, which is too large, so last moves left. Then -2 + 5 equals 3, and the answer is the stored indices, [2, 3].
Step by step
- Allocate
sortedNumswithmake([][]int, len(nums)), one slot per element. Each slot will hold a{value, index}pair. - Fill it. Range over
sortedNumsand set elementito[]int{nums[i], i}. - Sort by value with
sort.Slice. Its second argument is a "less" function that takes two positions and returns whether the first element should come before the second, here comparing[0], the value. - Place the pointers with one statement:
firstat 0 andlastat the final position. - Loop while
first < last. When they meet, every candidate pair has been ruled out, and the same element is never used twice. - Compute
sumof the two values, element[0]of each pair. - If
sum == target, return the original indices, element[1]of each pair. - If the sum is smaller, do
first++. - Otherwise the sum is larger, so do
last--. - After the loop, return
[]int{-1, -1}.
The solution in Go
func twoSum(nums []int, target int) []int {
sortedNums := make([][]int, len(nums))
for i := range sortedNums {
sortedNums[i] = []int{nums[i], i}
}
sort.Slice(sortedNums, func(i, j int) bool {
return sortedNums[i][0] < sortedNums[j][0]
})
first, last := 0, len(sortedNums) - 1
for first < last {
sum := sortedNums[first][0] + sortedNums[last][0]
if sum == target {
return []int{sortedNums[first][1], sortedNums[last][1]}
} else if sum < target {
first++
} else {
last--
}
}
return []int{-1, -1}
}
The Go details worth a closer look:
- The
sortpackage.sort.Sliceneedsimport "sort"at the top of a standalone file. Without the import, the program doesn't compile. - A "less" function, not a comparator. The function passed to
sort.Slicereturns abool, not a negative, zero or positive number. Its parameters are positions in the slice being sorted, so the body indexessortedNumswith them. It closes oversortedNums, which is allowed because function literals can use variables from the enclosing function. - Not stable.
sort.Slicedoesn't promise to keep equal elements in their original order. With duplicates, the two equal pairs may swap places, which only matters for the order of the two indices in the answer.sort.SliceStablekeeps the order. - Slices of slices. Each element of
sortedNumsis its own small slice. That is simple, though a struct with two fields would be clearer in a larger program. - Short declaration of two variables.
first, last := 0, len(sortedNums) - 1declares both at once. - If / else if / else. The three branches cover equal, smaller and larger. Go has no parentheses around conditions, and
elsemust sit on the same line as the closing brace. - Index order in the answer. The index of the smaller value comes first: for
[7, 2]and target 9, the function returns[1, 0].
Complexity
Time: O(n log n). Building the pairs is O(n), sorting is O(n log n), and the two-pointer scan is O(n), since every step moves one pointer and they can travel at most n positions in total.
Space: O(n). There is one small slice per element. A hash map would give O(n) time with the same space, but this lesson practises sorting with two pointers.
Edge cases and common mistakes
- No pair.
[1, 2, 3]with target 100 returns[-1, -1]. - Empty or one-element input.
first < lastis false at once, so the function returns[-1, -1]. - Equal values.
[3, 3]with target 6 returns[0, 1], since the two 3s sit at different indices. - Same element twice.
[3, 4]with target 6 returns[-1, -1], because the pointers never cross over to use one element for both ends. - Negative numbers. The reasoning about sorted sums doesn't depend on signs:
[-3, 4, 3, 90]with target 0 returns[0, 2]. - Returning sorted positions.
firstandlastare positions in the sorted slice. The answer comes from element[1]of each pair. - Returning a difference from the less function. It must return a bool, so
return a - bdoesn't compile.
Testing
nums = [2, 7, 11, 15],target = 9gives[0, 1].nums = [3, 9, -2, 5],target = 3gives[2, 3].nums = [3, 2, 4],target = 6gives[1, 2].nums = [3, 4],target = 6gives[-1, -1].nums = [],target = 6gives[-1, -1].