Problem overview
The method subarraySum receives an Array[Int] called nums and an integer target. The numbers can be positive, negative or zero. The method must find a continuous subarray whose sum equals target and return its start and end indices. If several subarrays qualify, any one of them is accepted. When none exists, it returns [-1, -1].
For nums = [1, 1, 5, 2, 1, 3, 10, 2, 1] and target = 21, the answer is [2, 6], because 5 + 2 + 1 + 3 + 10 = 21.
The statement asks for O(n) time and O(n) extra space. Since negative numbers are allowed, a two-pointer window doesn't work here: adding an element no longer always increases the sum. Prefix sums with a map do.
Approach: look up the prefix that must have come earlier
Let sum be the prefix sum at index i, the sum of all elements from index 0 to i. A subarray that ends at i and starts at some index s has the sum sum - (the prefix sum just before s). For that to equal target, the earlier prefix sum has to be sum - target.
So at every index the question becomes: has a prefix with the sum sum - target already appeared? A map answers it in constant time. Its keys are the prefix sums seen so far, and each value is the index where the subarray that leaves that prefix out would start, which is one past the prefix's last index.
On the example the prefix sums are 1, 2, 7, 9, 10, 13, 23, 25, 26. At index 6 the prefix sum is 23 and 23 - 21 = 2. The prefix sum 2 was recorded after index 1, so the map holds the start index 2. The answer is [2, 6].
Step by step
- Create
indexOfValueas avalholding a mutableHashMap[Int, Int]. Scala's defaultMapis immutable, so the code names the mutable class by its full path,scala.collection.mutable.HashMap. The map stays the same object, and only its contents change, which is whyvalis enough. - Store the key 0 with the value 0:
indexOfValue(0) = 0. The empty prefix has sum 0, and a subarray that leaves it out starts at index 0. Without this entry, no subarray starting at index 0 could ever be found. - Declare
sumas avarequal to 0. It is the running prefix sum. - Loop
iovernums.indices. Each index is a possible last index of the subarray. - Add
nums(i)tosum. - Compute
diff = sum - targetas aval. It is the prefix sum we hope to find in the map. - Check
indexOfValue.contains(diff). - On a hit, return
Array(indexOfValue(diff), i). The stored value is the start of the subarray, andiis the end. - Otherwise record the current prefix:
indexOfValue(sum) = i + 1. A subarray that leaves out the prefix ending atistarts ati + 1. - If the loop ends, no subarray matched, so return
Array(-1, -1).
The order of the check and the store matters. With target = 0, storing first would let diff match the entry for the current prefix, and the method would report an empty subarray.
The solution in Scala
class Solution {
def subarraySum(nums: Array[Int], target: Int): Array[Int] = {
val indexOfValue = scala.collection.mutable.HashMap[Int, Int]()
indexOfValue(0) = 0
var sum = 0
for (i <- nums.indices) {
sum += nums(i)
val diff = sum - target
if (indexOfValue.contains(diff)) {
return Array(indexOfValue(diff), i)
}
indexOfValue(sum) = i + 1
}
return Array(-1, -1)
}
}
The Scala details worth knowing:
- Map update and lookup both use parentheses.
indexOfValue(0) = 0puts a value in the map, andindexOfValue(diff)reads one. Reading a key that isn't in the map throws aNoSuchElementException("key not found"), which is why the code callscontainsfirst. The alternative isget(diff), which returns anOption, andgetOrElse(diff, -1), which gives a default value. - Writing an existing key replaces its value. When the same prefix sum appears twice, the later start index overwrites the earlier one. Any valid subarray is accepted, so this is fine.
returninside afor. The loop body is passed toforeachas a function, and areturnthere leaves the whole method. Scala 3.5 still compiles this, but warns that returns from inside a function body are no longer supported and suggestsscala.util.boundaryandboundary.breakinstead. The warning doesn't stop the program from running. The same method written withboundaryandbreak(Array(indexOfValue(diff), i))returns the same results.Intoverflow. The prefix sums are 32-bitInts, so very large inputs could wrap around silently. Switchingsumand the map keys toLongavoids this.
Complexity
Time: O(n) on average. The loop runs once per element, and each hash map operation takes constant time on average.
Space: O(n). The map holds up to n + 1 prefix sums.
Edge cases and common mistakes
- No matching subarray.
[1, 2, 3]withtarget = 100returns[-1, -1]. - A subarray from index 0.
[3, -1, 2]withtarget = 3returns[0, 0]. The entry for the empty prefix makes it findable.[2, 3]withtarget = 5relies on the same entry to return[0, 1]. - Negative numbers.
[-5, 5, 1]withtarget = 1returns[2, 2]. The prefix sums are -5, 0, 1. The prefix 0 was last stored with start index 2, so the answer is the single element at index 2. The subarray[-5, 5, 1]from 0 to 2 also sums to 1, but the method only has to return one answer. - Zeros.
[0, 1]withtarget = 0returns[0, 0], the single zero. A target of 0 never produces an empty subarray, thanks to the check-then-store order. - Storing the index instead of
i + 1. The answer would start one element too early and include the element at the end of the prefix. - Using a sliding window. It relies on all numbers being positive. With negative numbers the window can't decide whether to grow or shrink.
- An empty array. The loop never runs and the method returns
[-1, -1].
Testing
[1, 1, 5, 2, 1, 3, 10, 2, 1]withtarget = 21gives[2, 6].[1, 2, 3]withtarget = 100gives[-1, -1].[4, -4, 2]withtarget = 0gives[0, 1].[3, -1, 2]withtarget = 3gives[0, 0].[7]withtarget = 7gives[0, 0].
