Start learning free

LESSON 288 OF 348 · SLIDING WINDOW

Longest Subarray with Sum at Most S in Kotlin: O(n) Sliding Window

Practice O(n) Read the full explanation

Given an array of positive integers and a number S, find the longest contiguous subarray having the sum at most S.

Return the start and end indices denoting this subarray.

Example

Input: nums = [3, 2, 5, 2, 2, 1, 1, 3, 1 , 2], S = 11
Output: [3, 8]
Explanation:the subarray nums[3...8] of sum 10

Note:

Your algorithm should run in O(n) time and use O(1) extra space.


STEP 1 OF 9
    Ctrl + Enter to check

    KOTLIN · SLIDING WINDOW

    Longest Subarray With Sum At Most S in Kotlin, explained

    Problem overview

    The function longestSubarray receives an array of positive integers, nums, and a limit S. It must find the longest contiguous stretch of the array whose sum is at most S, and return the start and end indices of that stretch as a two-element IntArray.

    For nums = [3, 2, 5, 2, 2, 1, 1, 3, 1, 2] and S = 11, the answer is [3, 8]. The elements at indices 3 through 8 are 2, 2, 1, 1, 3, 1. They sum to 10, and no longer stretch stays within 11.

    The statement asks for O(n) time and O(1) extra space.

    Approach: a window that never moves left

    The brute force tries every start and extends the end until the sum exceeds S. That is O(n²). The waste is that, after finishing one start, it forgets everything and begins the next start from scratch.

    Because every number is positive, adding an element can only increase a sum, and removing one can only decrease it. That gives the window a useful property. If the window first..last-1 fits within S, then moving the start to first + 1 also fits: the elements that remain can only add up to less. So the right end never has to move back. For each new start, the window keeps its right end and only needs to grow further.

    Here the window is the set of elements from index first up to, but not including, index last. The variable last is the index of the next element to try adding. It only ever increases, so across the whole run it moves at most n times. The start also moves at most n times. The total work is linear.

    Step by step

    1. Prepare the answer. answer is a var holding intArrayOf(0, 0). It is only a placeholder, replaced as soon as a valid stretch is found.
    2. Declare three counters with var: maxLen (the best length so far), sum (the sum of the window) and last (the next index to add), all starting at 0. They are declared outside the loop so that last keeps its position when the start moves.
    3. Loop over each start, first, in nums.indices.
    4. Grow the window with a while loop that runs while last < nums.size and sum + nums[last] <= S. The bounds check comes first: && stops evaluating once the left side is false, so nums[last] is never read past the end.
    5. Add nums[last] to sum.
    6. Record a new best. The window now holds the elements first through last, so its length is last - first + 1. If that is larger than maxLen, update maxLen and set answer to intArrayOf(first, last). The strict < keeps the leftmost stretch when two have the same length.
    7. Move on: last++.
    8. Slide the window. After the while loop, subtract nums[first] from sum, since the next start no longer includes it.
    9. Return answer after the loop.

    The solution in Kotlin

    class Solution {
        fun longestSubarray(nums: IntArray, S: Int): IntArray {
            var answer = intArrayOf(0, 0)
            var maxLen = 0
            var sum = 0
            var last = 0
            for (first in nums.indices) {
                while (last < nums.size && sum + nums[last] <= S) {
                    sum += nums[last]
                    if (maxLen < last - first + 1) {
                        maxLen = last - first + 1
                        answer = intArrayOf(first, last)
                    }
                    last++
                }
                sum -= nums[first]
            }
            return answer
        }
    }
    

    The Kotlin details worth knowing:

    • IntArray and intArrayOf. IntArray is Kotlin's array of primitive int values on the JVM. It is created with intArrayOf(...), and it prints as an object reference rather than its contents, so use answer.toList() when you want to look at it.
    • nums.indices. It is the range 0..nums.size - 1, so the loop is empty for an empty array.
    • var answer and the array. var allows answer to refer to a new array each time a better stretch is found. A fresh intArrayOf(first, last) is created only when the best length improves, which is rare enough not to matter.
    • The parameter name S. A capital letter is legal, and the lesson's code keeps it from the statement.
    • Short-circuit &&. The condition last < nums.size && sum + nums[last] <= S only reads nums[last] when the index is valid. Swapping the two conditions would throw an ArrayIndexOutOfBoundsException at the end of the array.
    • 32-bit sums. sum is an Int, which wraps silently on overflow. That isn't a problem while S and the elements stay far below about 2 billion.

    Complexity

    Time: O(n). The index last only increases, and the inner while loop moves it, so the loop body runs at most n times over the whole call. The outer loop runs n times. Together that is at most about 2n steps.

    Space: O(1). The function keeps a few integers and the two-element answer array, regardless of the size of nums.

    Edge cases and common mistakes

    • An element larger than S. For [5, 1, 1] with S = 3, the element 5 can't join any window. The result is [1, 2]. The subtraction sum -= nums[first] still happens for that start, and the sum goes negative for a moment. When last later passes over that element it is added back, so the sums stay consistent.
    • No valid stretch. If every element exceeds S, as in [5] with S = 3, nothing replaces the placeholder and the function returns [0, 0]. An empty array returns [0, 0] too.
    • Ties. For [4, 4] and S = 4, both stretches have length 1. The result is [0, 0], the leftmost, because of the strict <.
    • Whole array fits. [1, 1, 1] with S = 100 returns [0, 2].
    • Resetting last inside the loop. That restores the O(n²) behaviour. Keeping last outside is what makes it linear.
    • Forgetting to subtract nums[first]. The sum would keep elements that have left the window, and later windows would look too large.
    • Using it with negative numbers. The idea that a smaller window always fits depends on positive values. With negatives, the sum doesn't grow steadily as the window grows, and this approach no longer works.

    Testing

    • nums = [3, 2, 5, 2, 2, 1, 1, 3, 1, 2], S = 11 gives [3, 8].
    • nums = [9, 1, 1, 9, 1, 1], S = 2 gives [1, 2].
    • nums = [7, 1, 2], S = 3 gives [1, 2].
    • nums = [1, 1, 1], S = 100 gives [0, 2].
    • nums = [4, 4], S = 4 gives [0, 0].
    Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free