Start learning free

LESSON 216 OF 348 · PARTIAL SUMS #2

Subarray of Given Sum in TypeScript: Prefix Sums in O(n)

Practice O(n) Read the full explanation

Given an array of integers and a number target, find a continous subarray whose sum is equal to target.

Return the start and end indices denoting this subarray. If there are multiple solutions, you can return any of them.


Example:

Input: nums = [1, 1, 5, 2, 1, 3, 10, 2, 1], k = 21
Output: [2, 6]
Explanation: 5 + 2 + 1 + 3 + 10 = 21

Notes:

1. The array can consist of positive and negative numbers.

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


Video Solution - Live Mock Coding Interview:
STEP 1 OF 10
    Ctrl + Enter to check

    TYPESCRIPT · PARTIAL SUMS #2

    Subarray Of Given Sum in TypeScript, explained

    Problem overview

    The method subarraySum receives an array nums of integers, which may be positive or negative, and a number target. It must find a continuous subarray whose elements add up to target and return its first and last index as [start, end]. If several subarrays work, any of them is accepted. If none does, the method returns [-1, -1].

    For nums = [1, 1, 5, 2, 1, 3, 10, 2, 1] and target = 21, the subarray at indices 2 to 6 is [5, 2, 1, 3, 10], and 5 + 2 + 1 + 3 + 10 = 21. The answer is [2, 6].

    The statement asks for O(n) time and allows O(n) extra space. Because negative numbers are allowed, a growing and shrinking window can't work, so the solution trades memory for speed with a lookup table.

    Approach: remember every prefix sum

    Let the prefix sum at some point be the sum of all elements before it. The sum of the subarray from index a to index i equals the prefix sum after i minus the prefix sum before a. So a subarray ending at i sums to target exactly when

    prefix after i - prefix before a = target
    

    which can be rearranged to prefix before a = prefix after i - target. While scanning left to right, we know the current prefix sum, so we know which earlier prefix sum we are looking for. If we stored every earlier prefix sum in a lookup table, together with the index where the subarray would start, the question "is there such a start?" costs one lookup.

    On the example, the prefix sums after each element are 1, 2, 7, 9, 10, 13, 23. At index 6 the sum is 23, and 23 - 21 = 2. The prefix sum 2 was reached after index 1, so the subarray starts at index 2. That gives [2, 6].

    Step by step

    1. Create the lookup table. indexOfValue is a const object used as a map, typed { [key: number]: number }: number keys and number values. For each prefix sum it will hold the index where a subarray that leaves that prefix out would start. A lookup takes constant time on average. const only fixes which object the name refers to; entries can still be added.
    2. Add the empty prefix. indexOfValue[0] = 0: before any element the sum is 0, and a subarray that leaves nothing out starts at index 0. Without this entry, no subarray starting at index 0 could ever be found.
    3. Keep a running sum, sum, starting at 0.
    4. Loop over each possible last index i.
    5. Add nums[i] to sum, so it now covers indices 0 to i.
    6. Compute the prefix we need. diff is sum - target, a fresh const in every iteration.
    7. Look it up with the in operator, which checks whether the object has that key.
    8. On a hit, return [indexOfValue[diff], i]: the stored start and the current end.
    9. Otherwise record the current prefix: indexOfValue[sum] = i + 1. The next index is where a subarray that leaves out everything up to i would start. Do this after the check. If it came first and target were 0, diff would equal sum and match the entry just added, which stands for an empty subarray.
    10. Return [-1, -1] if the loop ends with no hit.

    The solution in TypeScript

    class Solution {
        subarraySum(nums: number[], target: number): number[] {
            const indexOfValue: { [key: number]: number } = {};
            indexOfValue[0] = 0;
            let sum = 0;
            for (let i = 0; i < nums.length; i++) {
                sum += nums[i];
                const diff = sum - target;
                if (diff in indexOfValue) {
                    return [indexOfValue[diff], i];
                }
                indexOfValue[sum] = i + 1;
            }
            return [-1, -1];
        }
    }
    

    Some TypeScript details:

    • Object keys are strings. The index signature [key: number]: number lets you write indexOfValue[sum] with a number, but at run time JavaScript converts the number to a string key. So the prefix sum -3 is stored under the key "-3", and -3 in indexOfValue finds it, because the in operator converts the left side the same way.
    • Annotations vanish. The type { [key: number]: number } helps the compiler check reads and writes, but it is erased in the JavaScript output.
    • A Map works too. Map<number, number> with has, get and set keeps numbers as numbers and has no inherited keys. The plain object is enough here because every key is a number.
    • Overwriting. If the same prefix sum shows up twice, the second indexOfValue[sum] = i + 1 replaces the first. That's fine because any valid subarray is accepted, and the later start still gives a non-empty subarray ending at a later index.

    Complexity

    Time: O(n). One pass over the array, with one lookup and at most one insertion per element. Each of those takes constant time on average.

    Space: O(n). In the worst case, every prefix sum is different, and the table holds n + 1 entries.

    Edge cases and common mistakes

    • A subarray starting at index 0. For [1, 2, 3] and target = 6, the answer [0, 2] comes from the starting entry indexOfValue[0] = 0.
    • Negative numbers. For [4, -2, 3] and target = 1, the answer is [1, 2], since -2 + 3 = 1. A solution that shrinks a window when the sum gets too large would fail here.
    • Target 0. For [1, -1] the answer is [0, 1]. For [0] it is [0, 0]. For [5] there's no subarray with sum 0, and the method returns [-1, -1], which is why the table is updated after the check.
    • No match. For [1, 2, 3] and target = 7, the answer is [-1, -1]. An empty array also gives [-1, -1].
    • Storing i instead of i + 1. The stored value is the start of the subarray, which is one after the prefix's last element. Storing i would include one element too many.
    • Testing the value, not the key. Writing if (indexOfValue[diff]) fails when the stored start is 0, because 0 is falsy. The in check avoids that.

    Testing

    • [1, 1, 5, 2, 1, 3, 10, 2, 1] with target = 21 gives [2, 6].
    • [4, -2, 3] with target = 1 gives [1, 2].
    • [1, 2, 3] with target = 6 gives [0, 2].
    • [1, 2, 3] with target = 7 gives [-1, -1].
    • [1, -1] with target = 0 gives [0, 1].
    Keep going with TypeScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free