Problem overview
The method longestSubarray receives a slice nums of positive integers and a limit S. It must return the start and end indices, as a two-element []int, of the longest contiguous subarray whose sum is at most S.
In the example, nums = []int{3, 2, 5, 2, 2, 1, 1, 3, 1, 2} and S = 11. The subarray from index 3 to index 8 is 2, 2, 1, 1, 3, 1. It has six elements and sums to 10. No subarray of seven elements fits under 11, so the answer is [3, 8].
The statement asks for O(n) time and O(1) extra space.
Approach: a window that never moves backwards
The brute-force way tries every pair of start and end indices and adds up the elements between them. That is O(n²) pairs, and O(n³) if each sum is recomputed. It ignores a useful fact: all numbers are positive.
Because the numbers are positive, adding an element can only increase the sum, and removing one can only decrease it. That makes two things true:
- If the window from
firsttolastalready fits, you can try to extend it to the right. If the next element doesn't fit, no longer window starting atfirstdoes either. - If a window starting at
firstreaches as far aslast - 1, then a window starting atfirst + 1can reach at least as far. It has fewer elements to pay for, so its end never needs to go back.
So the right end last only moves forward, and the left end first only moves forward. Each index enters the window once and leaves once, which is O(n) in total, and the only extra memory is a few integers.
In this solution last is the index of the next element to try, so the window holds nums[first] through nums[last - 1], and its sum is kept in sum. Inside the inner loop, right after an element is added, the window ends at last, and that is the moment the length last - first + 1 is measured.
On the example, the windows reached for each value of first are:
first = 0: indices 0 to 2, sum 10 (adding the next 2 would give 12);first = 1: indices 1 to 4, sum 11;first = 2: indices 2 to 6, sum 11;first = 3: indices 3 to 8, sum 10, the first window of six elements;first = 4: indices 4 to 9, sum 10, also six elements, but it isn't longer than the best, so the answer stays at[3, 8].
Step by step
- Start with a placeholder answer,
[]int{0, 0}. It is replaced as soon as a valid subarray is found. - Declare the state in one statement:
maxLen, the best length so far;sum, the window's sum; andlast, the next index to try. All start at 0. - Loop over
first, the start of the window, from 0 to the end. Every start is tried, butlastis declared outside this loop so it keeps its value from one start to the next. - Grow the window with a condition-only
for: whilelastis inside the slice and addingnums[last]keeps the sum at mostS. The bound check comes first so thatnums[last]is never read past the end. - Add the element to
sum. - Record a new best. The window now covers
firstthroughlast, so its length islast - first + 1. If it beatsmaxLen, store the length and the pair of indices. The comparison is strict, so on a tie the earliest window is kept. - Advance
lastto try the next element. - Slide the window once the inner loop stops: subtract
nums[first]fromsum, because the next iteration of the outer loop starts one position later. - Return
answerafter the outer loop.
The solution in Go
type Solution struct{}
func (this Solution) longestSubarray(nums []int, S int) []int {
answer := []int{0, 0}
maxLen, sum, last := 0, 0, 0
for first := 0; first < len(nums); first++ {
for last < len(nums) && sum+nums[last] <= S {
sum += nums[last]
if maxLen < last-first+1 {
maxLen = last - first + 1
answer = []int{first, last}
}
last++
}
sum -= nums[first]
}
return answer
}
The Go details worth knowing:
- No
while. The inner loop is aforwith only a condition, which is Go's way to write a while loop. The outerforis the usual three-part counting loop. - Short-circuit order. In
last < len(nums) && sum+nums[last] <= S, the second part runs only when the first is true. Swapping them would panic with an index out of range error whenlastreacheslen(nums). - Multiple assignment with
:=.maxLen, sum, last := 0, 0, 0declares threeintvariables at once. Every variable declared with:=must be used, or the compiler refuses to build. - Methods on a struct. Go has no classes.
Solutionis an empty struct andlongestSubarrayis a method with a value receiver, called asSolution{}.longestSubarray(nums, S). The receiver is namedthishere, but the name is only a convention. - Fresh slices for the answer.
answer = []int{first, last}allocates a new two-element slice each time, so the returned slice is never shared with anything else. - An element larger than
S. Whennums[first]alone exceedsS, the inner loop adds nothing,laststays put, andsum -= nums[first]makessumnegative. On the next start,lastis behindfirst, and the inner loop first adds that skipped element, which bringssumback to 0 for the empty window. The lengths measured during that catch-up are 0 or negative, so they never replace the answer.
Complexity
Time: O(n). The outer loop runs n times. last goes from 0 to at most n and never decreases, so the inner loop's iterations over the whole run add up to at most n. The total is at most about 2n steps.
Space: O(1). The function uses a few integers and a two-element slice for the answer, whatever the size of the input.
Edge cases and common mistakes
- The whole slice fits.
nums = []int{1, 2, 3}andS = 100returns[0, 2]. - Ties.
nums = []int{2, 2, 2}andS = 4returns[0, 1], because[1, 2]has the same length and doesn't replace it. Withnums = []int{3, 1, 2, 1}andS = 3, the result is[1, 2]. - A single element equal to
S.nums = []int{4}andS = 4returns[0, 0], which is a valid subarray. - No subarray fits. When every element is larger than
S, there is no valid subarray, and the function returns the placeholder:nums = []int{5, 6}andS = 3gives[0, 0]. The same happens for an empty slice. If a program needs to tell "nothing fits" from a real answer, it can test whethermaxLenis still 0 at the end and return a sentinel such as[]int{-1, -1}. - A big element at the start. With
nums = []int{5, 1, 1}andS = 3, the first element never fits, but the window can restart after it: the answer is[1, 2]. - Using
<=instead of<in the best-length check. The answer would move to the last window of the maximum length instead of the first. - Resetting
lastinside the outer loop. That makes the solutionO(n²), because every start would re-scan its window from scratch. - Negative numbers. The reasoning above needs positive numbers. With negatives, a window that doesn't fit could fit after growing further, so this algorithm would give wrong answers.
Testing
nums = []int{3, 2, 5, 2, 2, 1, 1, 3, 1, 2},S = 11gives[3 8].nums = []int{1, 2, 3},S = 100gives[0 2].nums = []int{2, 2, 2},S = 4gives[0 1].nums = []int{5, 1, 1},S = 3gives[1 2].nums = []int{4},S = 4gives[0 0].