303. Range Sum Query - Immutable - Explanation

Problem Link

Description

You are given an integer array nums, handle multiple queries of the following type:

  1. Calculate the sum of the elements of nums between indices left and right inclusive where left <= right.

Implement the NumArray class:

  • NumArray(int[] nums) Initializes the object with the integer array nums.

  • int sumRange(int left, int right) Returns the sum of the elements of nums between indices left and right inclusive (i.e. nums[left] + nums[left + 1] + ... + nums[right]).

Example 1:

Input: ["NumArray","sumRange","sumRange","sumRange"]
[[[-2,0,3,-5,2,-1]],[0,2],[2,5],[0,5]]

Output: [null,1,-1,-3]

Explanation: NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return (-2) + 0 + 3 = 1
numArray.sumRange(2, 5); // return 3 + (-5) + 2 + (-1) = -1
numArray.sumRange(0, 5); // return (-2) + 0 + 3 + (-5) + 2 + (-1) = -3

Constraints:

  • 1 <= nums.length <= 10,000
  • -100,000 <= nums[i] <= 100,000
  • 0 <= left <= right < nums.length
  • At most 10,000 calls will be made to sumRange.

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Arrays - Basic array traversal and element access for computing range sums
  • Prefix Sums - Precomputing cumulative sums to enable constant-time range queries
  • Segment Trees (Optional) - Alternative data structure for range queries, though overkill for immutable arrays

1. Brute Force

Intuition

The simplest approach is to iterate through the array from left to right and add up all the elements. This works correctly but is inefficient when we need to answer many queries, since each query requires scanning through the entire range.

Algorithm

  1. Store the original array.
  2. For each sumRange(left, right) query:
    • Initialize res = 0.
    • Loop through indices from left to right.
    • Add each element to res.
  3. Return res.
class NumArray:
    def __init__(self, nums):
        self.nums = nums

    def sumRange(self, left, right):
        res = 0
        for i in range(left, right + 1):
            res += self.nums[i]
        return res

Time & Space Complexity

  • Time complexity: O(n)O(n) for each sumRange()sumRange() query.
  • Space complexity: O(1)O(1) since we only make a reference to the input array.

2. Prefix Sum - I

Intuition

We can precompute a prefix sum array where prefix[i] stores the sum of all elements from index 0 to i. To find the sum of any range [left, right], we take prefix[right] and subtract prefix[left - 1] (if left > 0). This gives us constant-time queries after a linear-time preprocessing step.

Algorithm

  1. Build a prefix sum array during construction:
    • Maintain a running sum cur.
    • For each element, add it to cur and store in prefix.
  2. For each sumRange(left, right) query:
    • Get rightSum = prefix[right].
    • If left > 0, set leftSum = prefix[left - 1], otherwise leftSum = 0.
    • Return rightSum - leftSum.
class NumArray:
    def __init__(self, nums):
        self.prefix = []
        cur = 0
        for num in nums:
            cur += num
            self.prefix.append(cur)

    def sumRange(self, left, right):
        rightSum = self.prefix[right]
        leftSum = self.prefix[left - 1] if left > 0 else 0
        return rightSum - leftSum

Time & Space Complexity

  • Time complexity: O(1)O(1) for each sumRange()sumRange() query, O(n)O(n) for building the prefix sum array.
  • Space complexity: O(n)O(n)

3. Prefix Sum - II

Intuition

This is a cleaner variation of the prefix sum approach. By creating an array of size n + 1 where prefix[0] = 0, we avoid the edge case when left = 0. The value prefix[i + 1] represents the sum of the first i + 1 elements. The range sum becomes simply prefix[right + 1] - prefix[left].

Algorithm

  1. Create a prefix array of size n + 1 initialized to 0.
  2. Build the prefix sums:
    • For each index i, set prefix[i + 1] = prefix[i] + nums[i].
  3. For each sumRange(left, right) query:
    • Return prefix[right + 1] - prefix[left].
class NumArray:
    def __init__(self, nums):
        self.prefix = [0] * (len(nums) + 1)
        for i in range(len(nums)):
            self.prefix[i + 1] = self.prefix[i] + nums[i]


    def sumRange(self, left, right):
        return self.prefix[right + 1] - self.prefix[left]

Time & Space Complexity

  • Time complexity: O(1)O(1) for each sumRange()sumRange() query, O(n)O(n) for building the prefix sum array.
  • Space complexity: O(n)O(n)

4. Segment Tree

Intuition

A segment tree is a binary tree where each node stores the sum of a range of elements. The root contains the sum of the entire array, and each leaf contains a single element. While this is overkill for an immutable array (prefix sums are simpler and faster), segment trees become essential when updates are needed. For this immutable version, queries still run in logarithmic time.

Algorithm

  1. Build the segment tree:
    • Store leaf values at indices n to 2n - 1.
    • Compute internal nodes bottom-up: tree[i] = tree[2*i] + tree[2*i + 1].
  2. For each sumRange(left, right) query:
    • Shift left and right to leaf positions.
    • While left < right:
      • If left is odd, add tree[left] to the result and increment left.
      • If right is odd, decrement right and add tree[right] to the result.
      • Move both pointers to their parents.
  3. Return the accumulated sum.
class SegmentTree:
    def __init__(self, nums):
        self.n = len(nums)
        self.tree = [0] * (2 * self.n)
        for i in range(self.n):
            self.tree[self.n + i] = nums[i]
        for i in range(self.n - 1, 0, -1):
            self.tree[i] = self.tree[2 * i] + self.tree[2 * i + 1]

    def query(self, left, right):
        left += self.n
        right += self.n + 1
        res = 0
        while left < right:
            if left % 2 == 1:
                res += self.tree[left]
                left += 1
            if right % 2 == 1:
                right -= 1
                res += self.tree[right]
            left //= 2
            right //= 2
        return res

class NumArray:
    def __init__(self, nums):
        self.segTree = SegmentTree(nums)

    def sumRange(self, left, right):
        return self.segTree.query(left, right)

Time & Space Complexity

  • Time complexity: O(logn)O(\log n) for each sumRange()sumRange() query, O(n)O(n) for building the Segment Tree.
  • Space complexity: O(n)O(n)

Common Pitfalls

Off-by-One Error When Left Index Is Zero

When using a prefix sum array where prefix[i] represents the sum from index 0 to i, computing sumRange(0, right) requires special handling. Attempting to access prefix[-1] causes an error. The fix is either to check if left > 0 before subtracting, or use a prefix array of size n + 1 where prefix[0] = 0 so that prefix[right + 1] - prefix[left] always works without edge cases.

Modifying the Original Array After Preprocessing

Since prefix sums are precomputed during initialization, any subsequent changes to the original array will not be reflected in query results. This approach only works for immutable arrays. If updates are needed, a different data structure like a segment tree or Fenwick tree must be used instead of static prefix sums.