Problem overview
The method subarraySum receives an array nums of integers, which can be positive, negative or zero, and a number target. It must find a continuous subarray whose sum equals target and return its first and last index as [start, end]. When several subarrays work, any one is accepted. When none does, it returns [-1, -1].
In the example, nums = [1, 1, 5, 2, 1, 3, 10, 2, 1] and target = 21. The elements at indices 2 to 6 are 5, 2, 1, 3, 10, and they add up to 21, so the answer is [2, 6].
The statement asks for O(n) time and O(n) extra space. Because the array may contain negative numbers, a sliding window cannot be used: adding an element no longer means the sum only grows. A dictionary of prefix sums works regardless of signs.
Approach: look up the prefix that would complete the sum
The prefix sum at index i is the sum of nums from index 0 to i. The sum of a subarray is the difference between two prefix sums: the one at its end, minus the one just before its start.
So for a subarray that ends at i to add up to target, the prefix sum just before its start must equal sum - target, where sum is the prefix sum at i. Call that value diff. Instead of searching the earlier positions for diff, remember every prefix sum already seen in a dictionary. A dictionary lookup takes constant time, so the whole pass is linear.
The dictionary stores, for each prefix sum, the index right after the prefix that produced it. That is exactly where a subarray that leaves the prefix out would start. Before the loop it holds one entry: the empty prefix has sum 0, and the matching subarray would start at index 0. That entry is what makes subarrays starting at the very beginning findable.
For the example, with target = 21, the loop goes like this:
| i | sum | diff | found in dictionary? |
|---|---|---|---|
| 0 | 1 | -20 | no |
| 1 | 2 | -19 | no |
| 2 | 7 | -14 | no |
| 3 | 9 | -12 | no |
| 4 | 10 | -11 | no |
| 5 | 13 | -8 | no |
| 6 | 23 | 2 | yes, start 2 |
At i = 6 the prefix sum is 23, and 23 - 21 = 2. The prefix sum 2 was recorded after index 1, so it maps to start 2. The method returns [2, 6].
Step by step
- Create
indexOfValueas an empty[Int: Int]()dictionary, withvarbecause entries will be added. Its keys are prefix sums and its values are start indices. - Store the empty prefix: key 0, value 0. Without it, a subarray beginning at index 0 could never be found.
- Create
sumat 0 as avar. It is the running prefix sum. - Loop over each possible end
ifrom 0 up to but not includingnums.count. - Add
nums[i]tosum. It now covers indices 0 toi. - Compute
diffassum - target, withlet. This is the earlier prefix sum that would complete the target. - Look up
diffwithif let start = indexOfValue[diff]. A dictionary read returns an optional, and the body only runs when the key exists. - On a hit, return
[start, i]. - Otherwise record the current prefix:
indexOfValue[sum] = i + 1. This happens after the check, so a prefix is never matched with itself. - Return
[-1, -1]if the loop ends without a hit.
The solution in Swift
class Solution {
func subarraySum(_ nums: [Int], _ target: Int) -> [Int] {
var indexOfValue = [Int: Int]()
indexOfValue[0] = 0
var sum = 0
for i in 0..<nums.count {
sum += nums[i]
let diff = sum - target
if let start = indexOfValue[diff] {
return [start, i]
}
indexOfValue[sum] = i + 1
}
return [-1, -1]
}
}
The Swift details worth knowing:
- Dictionary subscripts return optionals.
indexOfValue[diff]has the typeInt?, and it isnilwhen the key is missing. A missing key is not an error and does not return 0. That is why the code unwraps the value withif let. if letscope. The namestartexists only inside the braces of theif, where it is a plainInt.- Assigning through a subscript.
indexOfValue[sum] = i + 1inserts the key if it is new and replaces its value if it already exists. - Type annotation by initializer.
[Int: Int]()creates an empty dictionary whose type is clear from the expression, so no further annotation is needed. - Early
returninside a loop. The method ends as soon as a match is found, and the finalreturn [-1, -1]is only reached when nothing matched.
Complexity
Time: O(n). The loop makes one pass. Each step does one addition, one dictionary lookup and at most one dictionary insertion, and these take constant time on average.
Space: O(n). The dictionary can hold one entry per prefix sum, so up to n + 1 entries.
Edge cases and common mistakes
- A subarray at the very start.
[4, 1, 2]withtarget = 4returns[0, 0]. Ati = 0the prefix sum is 4, sodiffis 0, and the entry for the empty prefix supplies start 0. - Negative numbers.
[-3, 1, 2, -4]withtarget = -4returns[3, 3]. A sliding window fails on inputs like this one, while the dictionary does not care about signs. - Zero as the target.
[5, -5, 3]withtarget = 0returns[0, 1]. If the current prefix were stored before the lookup,diffwould match the prefix that was just stored and report an empty subarray. - Repeated prefix sums. When the same prefix sum appears again, its dictionary entry is replaced by the later start. That is safe: a later start gives a shorter subarray with the same sum, and any valid subarray is accepted.
- No match.
[1, 2, 3]withtarget = 100returns[-1, -1], and so does[2, 3]withtarget = 0. - Storing the wrong index. The dictionary must hold
i + 1, the first index after the prefix, noti. Storingiwould start the subarray one element too early, at the last element of the prefix.
Testing
nums = [1, 1, 5, 2, 1, 3, 10, 2, 1],target = 21gives[2, 6].nums = [4, 1, 2],target = 3gives[1, 2].nums = [5, -5, 3],target = 0gives[0, 1].nums = [-3, 1, 2, -4],target = -4gives[3, 3].nums = [1, 2, 3],target = 100gives[-1, -1].
