Problem overview
The function twoSum receives an IntArray called nums and an Int called target. It returns the indices of two different positions whose values add up to target, or [-1, -1] if there is no such pair. There is at most one valid pair, and one 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 answer is [0, 1].
The statement allows O(n log n) time and O(n) extra space. That is more than enough to sort a copy of the data, and sorting is what makes this lesson's idea possible.
Approach: sort, then move two pointers inward
Checking every pair of indices takes O(n²) time. Sorting lets you do much better. In an ascending array, put one pointer at the smallest value and one at the largest, and look at their sum:
- If the sum equals
target, the pair is found. - If the sum is too small, the left value can't be part of any answer with the current right value or anything smaller, so move the left pointer one step right to get a larger value.
- If the sum is too large, move the right pointer one step left to get a smaller value.
Every step discards one element for good, so the pointers meet after at most n - 1 steps. The scan is O(n) and the sort is O(n log n), so the sort sets the total.
Sorting has a catch: the answer needs the original indices, and sorting moves values around. The fix is to sort small pairs, each holding a value and the index it came from. The pair travels with its value wherever the sort puts it.
Take nums = [3, 9, -2, 5] and target = 3. The pairs sorted by value are (-2, 2), (3, 0), (5, 3), (9, 1). The pointers start on -2 and 9, whose sum is 7. That is too big, so the right pointer moves to 5. Now -2 + 5 = 3, which matches, and the answer is the stored indices [2, 3].
A hash map solves the same problem in O(n) time, and a binary search per element is another sorted-array option. This lesson practises the two-pointer scan.
Step by step
- Create the pairs array.
sortedNumshas one row per element, and each row is anIntArray(2)holding a value and its index. - Fill it. Row
ibecomes(nums[i], i), so the original position is recorded before anything moves. - Sort the rows by value.
sortBy { it[0] }uses the first element of each row as the key, and the second element just rides along. - Place the pointers.
firststarts at 0 andlastat the final position. Both arevarbecause they change. - Loop while
first < last. When they meet, no unchecked pair is left, and two pointers on the same spot would reuse one index. - Add the two values. Index 0 of a row is the value, so the sum is
sortedNums[first][0] + sortedNums[last][0]. - On a match, return the original indices. They are in index 1 of each row, not the pointer positions.
- Too small:
first++. Only a larger left value can raise the sum. - Too large:
last--. Only a smaller right value can lower the sum. - Return
[-1, -1]after the loop, because the loop only ends without returning when every candidate has been ruled out.
The solution in Kotlin
class Solution {
fun twoSum(nums: IntArray, target: Int): IntArray {
val sortedNums = Array(nums.size) { IntArray(2) }
for (i in nums.indices) {
sortedNums[i] = intArrayOf(nums[i], i)
}
sortedNums.sortBy { it[0] }
var first = 0
var last = sortedNums.size - 1
while (first < last) {
val sum = sortedNums[first][0] + sortedNums[last][0]
if (sum == target) {
return intArrayOf(sortedNums[first][1], sortedNums[last][1])
} else if (sum < target) {
first++
} else {
last--
}
}
return intArrayOf(-1, -1)
}
}
The Kotlin details worth a closer look:
Array(size) { init }. This constructor calls the lambda once for each position and builds the array from the results. Each call produces a separateIntArray(2), so the rows don't share storage. The loop then replaces each placeholder row with the real pair.Array<IntArray>andintArrayOf.sortedNumsis an array of primitive int arrays, sosortedNums[first][0]is a plainInt. Thevalonly fixes which array the name refers to; the rows can still be replaced.sortBysorts in place. On an array,sortBy { it[0] }reorders the array itself and returns nothing useful.sortedByreturns a new list and leaves the array alone. The sort is stable, so rows with equal values keep their original order.it. In a lambda with one parameter, Kotlin names that parameteritautomatically. Here it is one row, anIntArray.- The empty array. With
nums.size == 0,lastis -1, the conditionfirst < lastis false at once, and the function returns[-1, -1]. Nothing is read from the empty array. Intoverflow.Intis 32 bits and wraps silently. For two values of 2000000000, the sum is -294967296, not 4000000000. If a test target could match a wrapped sum, the function would report a false pair; the lesson's tests don't do this.- No semicolons. The
if/else if/elsechain is a statement here, and each branch contains a single statement.
Complexity
Time: O(n log n). Building the pairs is O(n). Sorting is O(n log n). The loop moves one pointer every iteration, so it runs at most n - 1 times, which is O(n). The sort dominates.
Space: O(n). There is one two-element array per input value, plus a few integers for the pointers and the sum. The input array isn't modified.
Edge cases and common mistakes
- Returning pointer positions.
firstandlastare positions in the sorted array. The answer is in[1]of those rows. For[3, 9, -2, 5]with target 3, the pointers end at positions 0 and 2, but the answer is[2, 3]. - Using
first <= last. When both pointers are on one element, that element would be paired with itself. For[3, 4]and target 6, the function would answer[0, 0]instead of[-1, -1]. - Equal values.
[3, 3]with target 6 returns[0, 1]. The two 3s are different elements with different indices. - Index order. The smaller value's index comes first. For
[7, 2]and target 9 the result is[1, 0]. - Sorting
numsdirectly. That loses the original positions. The pairs are what keep them. - Fewer than two elements. An empty array or a single element returns
[-1, -1].
Testing
nums = [2, 7, 11, 15],target = 9gives[0, 1].nums = [3, 9, -2, 5],target = 3gives[2, 3].nums = [3, 3],target = 6gives[0, 1].nums = [3, 4],target = 6gives[-1, -1].nums = [],target = 9gives[-1, -1].